|
|
|
@ -1,16 +1,75 @@ |
|
|
|
import { Assets, type AssetsBundle } from "pixi.js"; |
|
|
|
import type { AssetsBundle } from "pixi.js"; |
|
|
|
import { AssetGraph } from "@/assets/AssetGraph"; |
|
|
|
import { AssetSession } from "@/assets/AssetSession"; |
|
|
|
import type { AssetInspectorSnapshot } from "@/assets/AssetInspectorSnapshot"; |
|
|
|
import { logger } from "./Logger"; |
|
|
|
|
|
|
|
class AssetManager { |
|
|
|
private referenceCounts: Map<string, number> = new Map(); |
|
|
|
export interface AssetRuntimeAdapter { |
|
|
|
init: () => Promise<void>; |
|
|
|
loadBundle: ( |
|
|
|
name: string, |
|
|
|
onProgress?: (progress: number) => void |
|
|
|
) => Promise<AssetsBundle>; |
|
|
|
unloadBundle: (name: string) => Promise<void>; |
|
|
|
} |
|
|
|
|
|
|
|
async init(): Promise<void> { |
|
|
|
// 硬编码的 /manifest.json 路径是本应用的固定配置
|
|
|
|
const createPixiAdapter = (): AssetRuntimeAdapter => ({ |
|
|
|
init: async () => { |
|
|
|
const { Assets } = await import("pixi.js"); |
|
|
|
await Assets.init({ manifest: "/manifest.json" }); |
|
|
|
}, |
|
|
|
loadBundle: async (name, onProgress) => { |
|
|
|
const { Assets } = await import("pixi.js"); |
|
|
|
return Assets.loadBundle(name, onProgress); |
|
|
|
}, |
|
|
|
unloadBundle: async (name) => { |
|
|
|
const { Assets } = await import("pixi.js"); |
|
|
|
await Assets.unloadBundle(name); |
|
|
|
}, |
|
|
|
}); |
|
|
|
|
|
|
|
export class AssetManager { |
|
|
|
private adapter: AssetRuntimeAdapter; |
|
|
|
private bundleRefCounts: Map<string, number> = new Map(); |
|
|
|
private bundleCache: Map<string, AssetsBundle> = new Map(); |
|
|
|
private sessionBundleRefs: Map<string, Map<string, number>> = new Map(); |
|
|
|
private graph: AssetGraph = new AssetGraph(); |
|
|
|
private sessionIdSeed = 0; |
|
|
|
private defaultSession: AssetSession; |
|
|
|
|
|
|
|
constructor(adapter?: AssetRuntimeAdapter) { |
|
|
|
this.adapter = adapter ?? createPixiAdapter(); |
|
|
|
this.defaultSession = this.createSession("global"); |
|
|
|
} |
|
|
|
|
|
|
|
async init(): Promise<void> { |
|
|
|
await this.adapter.init(); |
|
|
|
logger.debug("AssetManager initialized"); |
|
|
|
} |
|
|
|
|
|
|
|
async loadBundle( |
|
|
|
createSession(owner: string): AssetSession { |
|
|
|
const normalizedOwner = owner.trim(); |
|
|
|
if (normalizedOwner === "") { |
|
|
|
throw new Error("AssetManager: Owner name cannot be empty string"); |
|
|
|
} |
|
|
|
|
|
|
|
this.sessionIdSeed += 1; |
|
|
|
const sessionId = `session-${this.sessionIdSeed}`; |
|
|
|
this.graph.registerSession(sessionId, normalizedOwner); |
|
|
|
this.sessionBundleRefs.set(sessionId, new Map()); |
|
|
|
return new AssetSession(this, sessionId, normalizedOwner); |
|
|
|
} |
|
|
|
|
|
|
|
getInspectorSnapshot(): AssetInspectorSnapshot { |
|
|
|
return this.graph.collectSnapshot(this.bundleRefCounts, this.sessionBundleRefs); |
|
|
|
} |
|
|
|
|
|
|
|
getSessionBundles(sessionId: string): string[] { |
|
|
|
return this.graph.getSessionBundles(sessionId); |
|
|
|
} |
|
|
|
|
|
|
|
async acquireBySession( |
|
|
|
sessionId: string, |
|
|
|
name: string, |
|
|
|
onProgress?: (progress: number) => void |
|
|
|
): Promise<AssetsBundle | null> { |
|
|
|
@ -19,20 +78,32 @@ class AssetManager { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
// 已经加载过,增加引用计数
|
|
|
|
if (this.referenceCounts.has(name)) { |
|
|
|
const count = this.referenceCounts.get(name)! + 1; |
|
|
|
this.referenceCounts.set(name, count); |
|
|
|
logger.debug(`AssetManager: reuse bundle "${name}", refCount = ${count}`); |
|
|
|
const sessionRefs = this.sessionBundleRefs.get(sessionId); |
|
|
|
if (!sessionRefs) { |
|
|
|
logger.warn(`AssetManager: unknown session "${sessionId}"`); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
const totalRefCount = this.bundleRefCounts.get(name) ?? 0; |
|
|
|
const sessionRefCount = sessionRefs.get(name) ?? 0; |
|
|
|
if (totalRefCount > 0) { |
|
|
|
this.bundleRefCounts.set(name, totalRefCount + 1); |
|
|
|
sessionRefs.set(name, sessionRefCount + 1); |
|
|
|
this.graph.trackBundle(sessionId, name); |
|
|
|
logger.debug( |
|
|
|
`AssetManager: reuse bundle "${name}", refCount = ${totalRefCount + 1}` |
|
|
|
); |
|
|
|
onProgress?.(1); |
|
|
|
return Assets.loadBundle(name); |
|
|
|
return this.bundleCache.get(name) ?? this.adapter.loadBundle(name); |
|
|
|
} |
|
|
|
|
|
|
|
// 首次加载
|
|
|
|
logger.debug(`AssetManager: loading bundle "${name}"`); |
|
|
|
try { |
|
|
|
const bundle = await Assets.loadBundle(name, onProgress); |
|
|
|
this.referenceCounts.set(name, 1); |
|
|
|
const bundle = await this.adapter.loadBundle(name, onProgress); |
|
|
|
this.bundleRefCounts.set(name, 1); |
|
|
|
this.bundleCache.set(name, bundle); |
|
|
|
sessionRefs.set(name, 1); |
|
|
|
this.graph.trackBundle(sessionId, name); |
|
|
|
return bundle; |
|
|
|
} catch (error) { |
|
|
|
logger.error(`AssetManager: Failed to load bundle "${name}":`, error); |
|
|
|
@ -40,36 +111,62 @@ class AssetManager { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async unloadBundle(name: string): Promise<void> { |
|
|
|
async releaseBySession(sessionId: string, name: string): Promise<void> { |
|
|
|
if (name === "") { |
|
|
|
logger.warn("AssetManager: Bundle name cannot be empty string"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (!this.referenceCounts.has(name)) { |
|
|
|
const sessionRefs = this.sessionBundleRefs.get(sessionId); |
|
|
|
if (!sessionRefs) { |
|
|
|
logger.warn(`AssetManager: unknown session "${sessionId}"`); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const sessionRefCount = sessionRefs.get(name) ?? 0; |
|
|
|
if (sessionRefCount <= 0) { |
|
|
|
logger.warn(`AssetManager: unloading unloaded bundle "${name}"`); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const count = this.referenceCounts.get(name)! - 1; |
|
|
|
this.referenceCounts.set(name, count); |
|
|
|
if (sessionRefCount === 1) { |
|
|
|
sessionRefs.delete(name); |
|
|
|
this.graph.untrackBundle(sessionId, name); |
|
|
|
} else { |
|
|
|
sessionRefs.set(name, sessionRefCount - 1); |
|
|
|
} |
|
|
|
|
|
|
|
if (count <= 0) { |
|
|
|
const totalRefCount = (this.bundleRefCounts.get(name) ?? 0) - 1; |
|
|
|
this.bundleRefCounts.set(name, totalRefCount); |
|
|
|
|
|
|
|
if (totalRefCount <= 0) { |
|
|
|
logger.debug(`AssetManager: unloading bundle "${name}"`); |
|
|
|
await Assets.unloadBundle(name); |
|
|
|
this.referenceCounts.delete(name); |
|
|
|
await this.adapter.unloadBundle(name); |
|
|
|
this.bundleRefCounts.delete(name); |
|
|
|
this.bundleCache.delete(name); |
|
|
|
} else { |
|
|
|
logger.debug(`AssetManager: decrease refCount for "${name}" to ${count}`); |
|
|
|
logger.debug(`AssetManager: decrease refCount for "${name}" to ${totalRefCount}`); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async loadBundle( |
|
|
|
name: string, |
|
|
|
onProgress?: (progress: number) => void |
|
|
|
): Promise<AssetsBundle | null> { |
|
|
|
return this.defaultSession.loadBundle(name, onProgress); |
|
|
|
} |
|
|
|
|
|
|
|
async unloadBundle(name: string): Promise<void> { |
|
|
|
return this.defaultSession.releaseBundle(name); |
|
|
|
} |
|
|
|
|
|
|
|
isLoaded(name: string): boolean { |
|
|
|
if (name === "") { |
|
|
|
logger.warn("AssetManager: Bundle name cannot be empty string"); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
return this.referenceCounts.has(name) && this.referenceCounts.get(name)! > 0; |
|
|
|
return this.bundleRefCounts.has(name) && this.bundleRefCounts.get(name)! > 0; |
|
|
|
} |
|
|
|
|
|
|
|
getRefCount(name: string): number { |
|
|
|
@ -78,13 +175,16 @@ class AssetManager { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
return this.referenceCounts.get(name) ?? 0; |
|
|
|
return this.bundleRefCounts.get(name) ?? 0; |
|
|
|
} |
|
|
|
|
|
|
|
clearAll(): void { |
|
|
|
// 注意:此方法只会清除本地引用计数,不会从 Pixi 中卸载资源
|
|
|
|
// 因为完全卸载资源在实际应用中很少需要,且可能导致正在使用的资源失效
|
|
|
|
this.referenceCounts.clear(); |
|
|
|
this.bundleRefCounts.clear(); |
|
|
|
this.bundleCache.clear(); |
|
|
|
this.sessionBundleRefs.clear(); |
|
|
|
this.sessionIdSeed = 0; |
|
|
|
this.graph = new AssetGraph(); |
|
|
|
this.defaultSession = this.createSession("global"); |
|
|
|
logger.debug("AssetManager: Reference count map cleared"); |
|
|
|
} |
|
|
|
} |
|
|
|
|