diff --git a/src/asset/core/channel.js b/src/asset/core/channel.js new file mode 100644 index 00000000..2c3b987d --- /dev/null +++ b/src/asset/core/channel.js @@ -0,0 +1,80 @@ +/** @import { AssetId } from '../types/index.js' */ + +/** + * @readonly + * @enum {number} + */ +export const AssetChannelMessageType = { + Acquire: 0, + Release: 1 +} + +/** + * @template T + */ +export class AssetChannel { + + /** + * @private + * @type {AssetChannelMessage[]} + */ + queue = [] + + /** + * Queue a reference acquisition. + * + * @param {AssetId} assetId + */ + acquire(assetId) { + this.queue.push(new AssetChannelMessage(AssetChannelMessageType.Acquire, assetId)) + } + + /** + * Queue a reference release. + * + * @param {AssetId} assetId + */ + release(assetId) { + this.queue.push(new AssetChannelMessage(AssetChannelMessageType.Release, assetId)) + } + + /** + * Drain the queued messages. + * + * @returns {Readonly[]>} + */ + flush() { + const { queue } = this + + if (queue.length) this.queue = [] + + return queue + } +} + +/** + * @template T + */ +export class AssetChannelMessage { + + /** + * @readonly + * @type {AssetChannelMessageType} + */ + type + + /** + * @readonly + * @type {AssetId} + */ + assetId + + /** + * @param {AssetChannelMessageType} type + * @param {AssetId} assetId + */ + constructor(type, assetId) { + this.type = type + this.assetId = assetId + } +} diff --git a/src/asset/core/handle.js b/src/asset/core/handle.js new file mode 100644 index 00000000..0144eb32 --- /dev/null +++ b/src/asset/core/handle.js @@ -0,0 +1,157 @@ +/** @import {AssetId} from '../types/index.js' */ +/** @import {Assets} from '../resources/assets.js' */ +/** @import {Constructor} from '../../type/index.js'*/ + +import { packInto64Int } from '../../algorithms/index.js' +import { typeid } from '../../type/index.js' +import { AssetServer } from '../resources/assetserver.js' +import { AssetChannel } from './channel.js' + +/** + * @template T + */ +export class Handle { + + /** + * @readonly + * @type {Constructor} + */ + type + + /** + * @private + * @type {boolean} + */ + dropped = false + + /** + * @private + * @readonly + * @type {AssetChannel} + */ + channel + + /** + * @readonly + * @type {number} + */ + index + + /** + * @readonly + * @type {number} + */ + generation = 0 + + /** + * @param {AssetChannel} channel + * @param {Constructor} type + * @param {number} index + * @param {number} generation + */ + constructor(channel, type, index, generation) { + this.index = index + this.generation = generation + this.channel = channel + this.type = type + } + + /** + * @returns {AssetId} + */ + id() { + return /** @type {AssetId}*/ (packInto64Int(this.index, this.generation)) + } + + clone() { + const { channel, index, generation } = this + + channel.acquire(this.id()) + + return new Handle(channel, this.type, index, generation) + } + + /** + * Snapshot the handle with the asset server path when available. + * + * @param {import('../../ecs/index.js').World} world + * @returns {HandleSnapshot} + */ + toSnapshot(world) { + const server = world.getResource(AssetServer) + const info = server?.getAssetInfo(this) + + if (info?.path) { + return new HandleSnapshot(this.type, info.path) + } + + return new HandleSnapshot(this.type, this.id()) + } + + drop() { + if (this.dropped) return + + this.channel.release(this.id()) + this.dropped = true + } +} + +/** + * A snapshot of an asset handle. + * + * The snapshot preserves the asset type and id, and stores the asset server + * path when one is registered so the handle can be reloaded by path. + * + * @template T + */ +export class HandleSnapshot { + + /** + * @readonly + * @type {Constructor} + */ + type + + /** + * @readonly + * @type {AssetId | string} + */ + asset + + /** + * @param {Constructor} type + * @param {AssetId | string} asset + */ + constructor(type, asset) { + this.type = type + this.asset = asset + } + + /** + * Restore the live handle from the asset server. + * + * If the asset server knows the path, we reload it by path. Otherwise we + * upgrade the stored asset id against the asset pool. + * + * @param {import('../../ecs/index.js').World} world + * @returns {Handle} + */ + fromSnapshot(world) { + const server = world.getResource(AssetServer) + + if (typeof this.asset === 'string') { + return /** @type {Handle} */ (server.load(this.type, this.asset)) + } + + const assets = /** @type {Assets} */ (server.getAssets(typeid(this.type))) + + // TODO: This is inherently incorrect. When scene resources are added, + // the assetid will point to the wrong asset in the scene due to desync between + // the scene and world when an assets are added/removed from the world or scene. + // Add a mapping between scene assets and world assets and use that to create + // the asset handle. Also ensure the assets are loaded into world before spawning + // the scene into the world. + + return /** @type {Handle} */ (assets.upgrade(this.asset)) + } +} diff --git a/src/asset/core/index.js b/src/asset/core/index.js index aee26d52..fc4c4a95 100644 --- a/src/asset/core/index.js +++ b/src/asset/core/index.js @@ -1,3 +1,4 @@ -export * from './asset.js' +export * from './channel.js' +export * from './handle.js' export * from './exporter.js' export * from './importer.js' diff --git a/src/asset/plugins/asset.js b/src/asset/plugins/asset.js index d64b39de..2f59dcf5 100644 --- a/src/asset/plugins/asset.js +++ b/src/asset/plugins/asset.js @@ -4,9 +4,9 @@ import { App, Plugin } from '../../app/index.js' import { AppSchedule, CoreSystems } from '../../core/index.js' import { EventPlugin } from '../../event/index.js' import { typeid, typeidGeneric } from '../../type/index.js' -import { Assets } from '../core/index.js' +import { Assets } from '../resources/index.js' import { AssetAdded, AssetDropped, AssetModified } from '../events/index.js' -import { registerAssetTypes, registerAssetOnAssetServer, unloadDroppedAssets, updateAssetEvents } from '../systems/index.js' +import { registerAssetTypes, registerAssetOnAssetServer, unloadDroppedAssets, updateAssetChannel, updateAssetEvents } from '../systems/index.js' /** * @template T @@ -44,6 +44,13 @@ export class AssetPlugin extends Plugin { const { asset, events } = this const world = app.getWorld() + app.registerSystem({ + label: `updateAssetChannel<${typeid(asset)}>`, + schedule: AppSchedule.Update, + systemGroup: CoreSystems.End, + system: updateAssetChannel(asset) + }) + if (events) { app .registerPlugin(new EventPlugin({ diff --git a/src/asset/core/asset.js b/src/asset/resources/assets.js similarity index 58% rename from src/asset/core/asset.js rename to src/asset/resources/assets.js index 1fb07bda..3da732f4 100644 --- a/src/asset/core/asset.js +++ b/src/asset/resources/assets.js @@ -1,10 +1,11 @@ /** @import {AssetId} from '../types/index.js' */ -/** @import {Constructor} from '../../type/index.js'*/ -import { packInto64Int, unpackFrom64Int } from '../../algorithms/index.js' +/** @import {Constructor} from '../../type/index.js' */ + +import { unpackFrom64Int } from '../../algorithms/index.js' import { DenseList } from '../../datastructures/index.js' -import { typeid } from '../../type/index.js' import { AssetAdded, AssetDropped, AssetEvent, AssetModified } from '../events/assets.js' -import { AssetServer } from '../resources/assetserver.js' +import { AssetChannel, AssetChannelMessageType } from '../core/channel.js' +import { Handle } from '../core/handle.js' /** * @template T @@ -34,6 +35,12 @@ export class Assets { */ events = [] + /** + * @readonly + * @type {AssetChannel} + */ + channel = new AssetChannel() + /** * @param {Constructor} type */ @@ -192,7 +199,6 @@ export class Assets { * @returns {Handle | undefined} */ getHandleByUUID(uuid) { - return this.uuids.get(uuid)?.clone() } @@ -213,6 +219,8 @@ export class Assets { drop(handle) { const entry = this.getEntry(handle) + if (!entry) return + entry.refCount -= 1 if (entry.refCount <= 0) { @@ -224,11 +232,19 @@ export class Assets { /** * @param {AssetId} assetId + * @returns {Handle | undefined} */ upgrade(assetId) { const [index, generation] = unpackFrom64Int(assetId) + const entry = this.getEntryInternal(index, generation) + + if (!entry) { + return + } + + entry.refCount += 1 - return new Handle(this, index, generation) + return new Handle(this.channel, this.type, index, generation) } /** @@ -241,174 +257,71 @@ export class Assets { if (entry) { entry.generation += 1 + entry.refCount = 1 - return new Handle(this, index, entry.generation) + return new Handle(this.channel, this.type, index, entry.generation) } const newEntry = new AssetEntry(undefined) newEntry.generation += 1 + newEntry.refCount = 1 this.assets.set(index, newEntry) - return new Handle(this, index, newEntry.generation) + return new Handle(this.channel, this.type, index, newEntry.generation) } values() { return this.assets.values() } -} - -/** - * @template T - */ -export class Handle { /** - * @readonly - * @type {Constructor} + * Drain and apply queued handle lifecycle messages. */ - type + update() { + const messages = this.channel.flush() - /** - * @private - * @type {boolean} - */ - dropped = false + for (let i = 0; i < messages.length; i++) { + const message = messages[i] - /** - * This only exists as a channel for reference counting, do not use for any - * other purpose! - * @private - * @readonly - * @type {Assets} - */ - assets - - /** - * @readonly - * @type {number} - */ - index - - /** - * @readonly - * @type {number} - */ - generation = 0 - - /** - * @param {Assets} assets - * @param {number} index - * @param {number} generation - */ - constructor(assets, index, generation) { - this.index = index - this.generation = generation - this.assets = assets - this.type = assets.type - - const entry = assets.getEntry(this) - - if (entry && entry.generation === generation) { - entry.refCount += 1 + if (message.type === AssetChannelMessageType.Acquire) { + this.acquire(message.assetId) + } else if (message.type === AssetChannelMessageType.Release) { + this.release(message.assetId) + } } } /** - * @returns {AssetId} - */ - id() { - return /** @type {AssetId}*/ (packInto64Int(this.index, this.generation)) - } - - clone() { - const { assets, index, generation } = this - - return new Handle(assets, index, generation) - } - - /** - * Snapshot the handle with the asset server path when available. - * - * @param {import('../../ecs/index.js').World} world - * @returns {HandleSnapshot} + * @private + * @param {AssetId} assetId */ - toSnapshot(world) { - const server = world.getResource(AssetServer) - const info = server?.getAssetInfo(this) - - if (info?.path) { - return new HandleSnapshot(this.type, info.path) - } - - return new HandleSnapshot(this.type, this.id()) - } + acquire(assetId) { + const entry = this.getEntryByAssetId(assetId) - drop() { - if (this.dropped) return + if (!entry) return - this.assets.drop(this) - this.dropped = true + entry.refCount += 1 } -} - -/** - * A snapshot of an asset handle. - * - * The snapshot preserves the asset type and id, and stores the asset server - * path when one is registered so the handle can be reloaded by path. - * - * @template T - */ -export class HandleSnapshot { /** - * @readonly - * @type {Constructor} + * @private + * @param {AssetId} assetId */ - type + release(assetId) { + const entry = this.getEntryByAssetId(assetId) - /** - * @readonly - * @type {AssetId | string} - */ - asset + if (!entry) return - /** - * @param {Constructor} type - * @param {AssetId | string} asset - */ - constructor(type, asset) { - this.type = type - this.asset = asset - } + entry.refCount -= 1 - /** - * Restore the live handle from the asset server. - * - * If the asset server knows the path, we reload it by path. Otherwise we - * upgrade the stored asset id against the asset pool. - * - * @param {import('../../ecs/index.js').World} world - * @returns {Handle} - */ - fromSnapshot(world) { - const server = world.getResource(AssetServer) + if (entry.refCount <= 0) { + const [index] = unpackFrom64Int(assetId) - if (typeof this.asset === 'string') { - return /** @type {Handle} */ (server.load(this.type, this.asset)) + entry.asset = undefined + this.assets.recycle(index) + this.events.push(new AssetDropped(this.type, assetId)) } - - const assets = /** @type {Assets} */ (server.getAssets(typeid(this.type))) - - // TODO: This is inherently incorrect. When scene resources are added, - // the assetid will point to the wrong asset in the scene due to desync between - // the scene and world when an assets are added/removed from the world or scene. - // Add a mapping between scene assets and world assets and use that to create - // the asset handle. Also ensure the assets are loaded into world before spawning - // the scene into the world. - - return /** @type {Handle} */ (assets.upgrade(this.asset)) } } diff --git a/src/asset/resources/assetserver.js b/src/asset/resources/assetserver.js index 8575b69c..b00b6695 100644 --- a/src/asset/resources/assetserver.js +++ b/src/asset/resources/assetserver.js @@ -3,7 +3,8 @@ import { typeid } from '../../type/index.js' import { assert, warn } from '../../logger/index.js' import { getFileExtension, swapRemove } from '../../utils/index.js' -import { Assets, Handle, Importer, Exporter } from '../core/index.js' +import { Handle, Importer, Exporter } from '../core/index.js' +import { Assets } from './assets.js' /** * @typedef {number} ImporterId diff --git a/src/asset/resources/index.js b/src/asset/resources/index.js index 67dcb3a0..fe00fcf0 100644 --- a/src/asset/resources/index.js +++ b/src/asset/resources/index.js @@ -1 +1,2 @@ +export * from './assets.js' export * from './assetserver.js' diff --git a/src/asset/systems/channel.js b/src/asset/systems/channel.js new file mode 100644 index 00000000..8ae31606 --- /dev/null +++ b/src/asset/systems/channel.js @@ -0,0 +1,24 @@ +/** @import { SystemFunc } from '../../ecs/index.js' */ +/** @import { Constructor } from '../../type/index.js' */ + +import { typeidGeneric } from '../../type/index.js' +import { Assets } from '../resources/index.js' + +/** + * Drain the queued handle lifecycle messages for a single asset pool. + * + * @template T + * @param {Constructor} asset + * @returns {SystemFunc} + */ +export function updateAssetChannel(asset) { + const assetsId = typeidGeneric(Assets, [asset]) + + return function updateAssetChannel(world) { + + /** @type {Assets} */ + const assets = world.getResourceByTypeId(assetsId) + + assets.update() + } +} diff --git a/src/asset/systems/events.js b/src/asset/systems/events.js index c90c564f..986bd942 100644 --- a/src/asset/systems/events.js +++ b/src/asset/systems/events.js @@ -1,7 +1,7 @@ /** @import { SystemFunc, World } from '../../ecs/index.js' */ /** @import { Constructor } from '../../type/index.js' */ /** @import { AssetEvents } from '../index.js' */ -import { Assets } from '../core/index.js' +import { Assets as ResourceAssets } from '../resources/index.js' import { Events } from '../../event/index.js' import { typeid, typeidGeneric } from '../../type/index.js' import { AssetAdded, AssetDropped, AssetModified, AssetLoadFail, AssetLoadOperation } from '../events/index.js' @@ -14,14 +14,14 @@ import { error, warnOnce } from '../../logger/index.js' * @returns {SystemFunc} */ export function updateAssetEvents(assetType, eventType) { - const assetsId = typeidGeneric(Assets, [assetType]) + const assetsId = typeidGeneric(ResourceAssets, [assetType]) const addEventsId = typeidGeneric(Events, [eventType.added]) const modifiedEventsId = typeidGeneric(Events, [eventType.modified]) const droppedEventsId = typeidGeneric(Events, [eventType.dropped]) return function updateAssetEvents(world) { - /** @type {Assets} */ + /** @type {ResourceAssets} */ const assets = world.getResourceByTypeId(assetsId) /** @type {Events>} */ diff --git a/src/asset/systems/index.js b/src/asset/systems/index.js index add9049d..057b2b3f 100644 --- a/src/asset/systems/index.js +++ b/src/asset/systems/index.js @@ -1,3 +1,4 @@ export * from './events.js' +export * from './channel.js' export * from './server.js' export * from './types.js' diff --git a/src/asset/systems/server.js b/src/asset/systems/server.js index d1d33ab7..9440bcba 100644 --- a/src/asset/systems/server.js +++ b/src/asset/systems/server.js @@ -4,8 +4,7 @@ import { Events } from '../../event/index.js' import { typeidGeneric } from '../../type/index.js' import { TypeRegistry } from '../../reflect/resources/index.js' -import { Assets } from '../core/index.js' -import { AssetServer, LoadState } from '../resources/index.js' +import { AssetServer, Assets, LoadState } from '../resources/index.js' import { AssetLoadFail, AssetLoadOperation, AssetLoadSuccess, AssetSaveSuccess } from '../events/index.js' import { assert } from '../../logger/index.js' diff --git a/src/asset/systems/types.js b/src/asset/systems/types.js index 5b68b6a9..5be7aaa9 100644 --- a/src/asset/systems/types.js +++ b/src/asset/systems/types.js @@ -3,9 +3,9 @@ import { World } from '../../ecs/index.js' import { ArrayInfo, Field, StructInfo } from '../../reflect/core/index.js' import { TypeRegistry } from '../../reflect/resources/index.js' -import { Assets, Handle, HandleSnapshot } from '../core/index.js' +import { AssetServer, Assets } from '../resources/index.js' +import { Handle, HandleSnapshot } from '../core/index.js' import { typeid, typeidGeneric } from '../../type/index.js' -import { AssetServer } from '../resources/index.js' import { AssetLoadFail, AssetSaveSuccess } from '../events/index.js' /** diff --git a/src/asset/tests/asset.test.js b/src/asset/tests/asset.test.js index f9986e53..d920fb4b 100644 --- a/src/asset/tests/asset.test.js +++ b/src/asset/tests/asset.test.js @@ -1,6 +1,7 @@ import { test, describe } from "node:test"; import { deepStrictEqual, strictEqual } from "node:assert"; -import { Assets,Handle } from "../core/index.js"; +import { Handle } from "../core/index.js"; +import { Assets } from "../resources/index.js"; import { AssetAdded, AssetDropped, AssetModified } from "../events/assets.js"; describe("Testing `Assets`", () => { @@ -54,7 +55,7 @@ describe("Testing `Assets`", () => { test('`Assets.get` returns undefined on invalid handle', () => { const assets = new Assets(String) - const handle = new Handle(assets,0, 1) + const handle = new Handle(assets.channel, String, 0, 1) const actual = assets.get(handle) strictEqual(actual, undefined) @@ -182,7 +183,7 @@ describe("Testing `Assets`", () => { test('`Assets.getByAssetId` returns undefined on invalid assetid', () => { const assets = new Assets(String) - const handle = new Handle(assets,0, 1).id() + const handle = new Handle(assets.channel, String, 0, 1).id() const actual = assets.getByAssetId(handle) strictEqual(actual, undefined) @@ -271,8 +272,9 @@ describe("Testing `Assets`", () => { const handle = assets.add(asset) assets.drop(handle) + assets.update() const events = assets.flushEvents() deepStrictEqual(events[1], new AssetDropped(String, handle.id())) }) -}) \ No newline at end of file +}) diff --git a/src/asset/tests/assetserver.test.js b/src/asset/tests/assetserver.test.js index cca5dba2..c8440ee1 100644 --- a/src/asset/tests/assetserver.test.js +++ b/src/asset/tests/assetserver.test.js @@ -1,6 +1,7 @@ import { deepStrictEqual, notDeepStrictEqual } from "assert"; import test, { describe } from "node:test"; -import { Assets, AssetServer, Exporter, Importer } from "../index.js"; +import { AssetServer, Exporter, Importer } from "../index.js"; +import { Assets } from "../resources/index.js"; import { typeid, typeidGeneric } from "../../type/index.js"; import { World } from "../../ecs/index.js"; import { updateAssets } from "../systems/index.js"; diff --git a/src/asset/tests/handle.test.js b/src/asset/tests/handle.test.js index 7736ff6d..c42cda2f 100644 --- a/src/asset/tests/handle.test.js +++ b/src/asset/tests/handle.test.js @@ -1,6 +1,6 @@ import { deepStrictEqual, strictEqual } from "assert"; import test, { describe } from "node:test"; -import { Assets } from "../core/index.js"; +import { Assets } from "../resources/index.js"; import { AssetServer } from "../resources/index.js"; import { World } from "../../ecs/index.js"; @@ -59,6 +59,7 @@ describe('Testing `Handle`',()=>{ const handle = assets.add(asset) handle.clone() handle.clone() + assets.update() const entry = assets.getEntry(handle) strictEqual(entry.refCount, 3) @@ -90,6 +91,7 @@ describe('Testing `Handle`',()=>{ const handle = assets.add(asset) handle.drop() + assets.update() const actual = assets.get(handle) deepStrictEqual(actual, undefined) @@ -106,6 +108,7 @@ describe('Testing `Handle`',()=>{ handle1.drop() handle2.drop() handle3.drop() + assets.update() const actual = assets.get(handle1) @@ -122,6 +125,7 @@ describe('Testing `Handle`',()=>{ handle2.drop() handle3.drop() + assets.update() const actual = assets.get(handle1) @@ -137,6 +141,7 @@ describe('Testing `Handle`',()=>{ handle2.drop() handle2.drop() + assets.update() const actual = assets.get(handle1) @@ -169,8 +174,10 @@ describe('Testing `Handle`',()=>{ const handle1 = assets.add(asset) handle1.drop() + assets.update() const handle2 = assets.add(asset) handle2.drop() + assets.update() const handle3 = assets.add(asset) deepStrictEqual(handle1.index, 0)