From 5d416675735ff843cbc7bccdfafa24fb85a25213 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Tue, 23 Jun 2026 15:12:45 +0200 Subject: [PATCH] =?UTF-8?q?refactor(api)!:=20v0.15=20Welle=201=20=E2=80=94?= =?UTF-8?q?=20System.update(delta),=20serializer=20per-app,=20dead=20barre?= =?UTF-8?q?l=20removed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING: unifies the System.update protocol across all managers and removes a dead internal rendering barrel. 3a — System.update(delta: Time): void across all 5 app systems. InputManager update(): this -> update(_delta: Time): void (fluent return removed); InteractionManager + AudioManager gain the param. Tween/RenderingContext were already conform. 0 external call-sites broke. The param is `_delta` (unused but required by the System contract; lint:strict forbids unused non-_ args, the spec's "lint allows it" assumption was empirically wrong). 3b — registerSerializer + Prefab.from/instantiate gain an optional `registry` param (per-app serialization; default = global). New @internal _resetDefaultSerializers() + SerializationRegistry.clear() close a cross-suite global-registry leak (serialization.test.ts:273); afterEach reset + 2 new tests. 3d — deleted dead internal barrel src/rendering/index.ts (0 static imports; migrated its one dynamic test import to #rendering/public). Documented the abstract-vs-concrete renderer boundary in renderer-sdk.ts + custom-renderers guide. api-mdx regenerated (audio/input/interaction-manager, prefab). 3395 tests pass. --- site/src/content/api/audio-manager.mdx | 6 +- site/src/content/api/input-manager.mdx | 6 +- site/src/content/api/interaction-manager.mdx | 6 +- site/src/content/api/prefab.mdx | 8 +- .../guide/debugging/custom-renderers.mdx | 2 + src/audio/AudioManager.ts | 5 +- src/core/serialization/Prefab.ts | 15 ++- .../serialization/SerializationRegistry.ts | 26 +++- src/core/serialization/serialize.ts | 17 +++ src/input/InputManager.ts | 5 +- src/input/InteractionManager.ts | 3 +- src/renderer-sdk.ts | 7 + src/rendering/index.ts | 121 ------------------ src/rendering/public.ts | 5 +- test/core/scene-systems.test.ts | 11 ++ test/core/serialization.test.ts | 45 ++++++- .../pass/web-gl2-pass-coordinator.test.ts | 4 +- 17 files changed, 138 insertions(+), 154 deletions(-) delete mode 100644 src/rendering/index.ts diff --git a/site/src/content/api/audio-manager.mdx b/site/src/content/api/audio-manager.mdx index 40a06648..2e718448 100644 --- a/site/src/content/api/audio-manager.mdx +++ b/site/src/content/api/audio-manager.mdx @@ -9,7 +9,7 @@ memberCount: 18 tier: "stable" sections: ["Import", "Constructors", "Methods", "Properties", "Events", "Source"] sourcePath: "src/audio/AudioManager.ts" -sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/audio/AudioManager.ts#L24" +sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/audio/AudioManager.ts#L25" --- ## Import @@ -41,7 +41,7 @@ AudioManager.muteOnHidden is enabled. - `play(source: Playable, options?: PlayOptions): Voice` - `registerBus(bus: AudioBus): this` - `unregisterBus(bus: AudioBus): this` -- `update(): void` +- `update(_delta: Time): void` ## Properties @@ -58,4 +58,4 @@ AudioManager.muteOnHidden is enabled. ## Source -[src/audio/AudioManager.ts](https://github.com/Exoridus/ExoJS/blob/main/src/audio/AudioManager.ts#L24) +[src/audio/AudioManager.ts](https://github.com/Exoridus/ExoJS/blob/main/src/audio/AudioManager.ts#L25) diff --git a/site/src/content/api/input-manager.mdx b/site/src/content/api/input-manager.mdx index 239f3c5c..770d7060 100644 --- a/site/src/content/api/input-manager.mdx +++ b/site/src/content/api/input-manager.mdx @@ -9,7 +9,7 @@ memberCount: 38 tier: "stable" sections: ["Import", "Constructors", "Methods", "Properties", "Events", "Source"] sourcePath: "src/input/InputManager.ts" -sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/input/InputManager.ts#L60" +sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/input/InputManager.ts#L61" --- ## Import @@ -44,7 +44,7 @@ automatically — you do not instantiate this class yourself. - `onStart(channel: InputChannel | readonly InputChannel[], callback: object, options?: InputBindingOptions): InputBinding` - `onStop(channel: InputChannel | readonly InputChannel[], callback: object, options?: InputBindingOptions): InputBinding` - `onTrigger(channel: InputChannel | readonly InputChannel[], callback: object, options?: InputBindingOptions): InputBinding` -- `update(): this` +- `update(_delta: Time): void` ## Properties @@ -83,4 +83,4 @@ automatically — you do not instantiate this class yourself. ## Source -[src/input/InputManager.ts](https://github.com/Exoridus/ExoJS/blob/main/src/input/InputManager.ts#L60) +[src/input/InputManager.ts](https://github.com/Exoridus/ExoJS/blob/main/src/input/InputManager.ts#L61) diff --git a/site/src/content/api/interaction-manager.mdx b/site/src/content/api/interaction-manager.mdx index d8346794..6dec5f9c 100644 --- a/site/src/content/api/interaction-manager.mdx +++ b/site/src/content/api/interaction-manager.mdx @@ -9,7 +9,7 @@ memberCount: 7 tier: "stable" sections: ["Import", "Constructors", "Methods", "Source"] sourcePath: "src/input/InteractionManager.ts" -sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/input/InteractionManager.ts#L70" +sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/input/InteractionManager.ts#L71" --- ## Import @@ -42,8 +42,8 @@ this class yourself. - `getHoveredNode(pointerId?: number): RenderNode | null` - `popInputCapture(): void` - `pushInputCapture(root: RenderNode): void` -- `update(): void` +- `update(_delta: Time): void` ## Source -[src/input/InteractionManager.ts](https://github.com/Exoridus/ExoJS/blob/main/src/input/InteractionManager.ts#L70) +[src/input/InteractionManager.ts](https://github.com/Exoridus/ExoJS/blob/main/src/input/InteractionManager.ts#L71) diff --git a/site/src/content/api/prefab.mdx b/site/src/content/api/prefab.mdx index d50dbbc8..117da39c 100644 --- a/site/src/content/api/prefab.mdx +++ b/site/src/content/api/prefab.mdx @@ -9,7 +9,7 @@ memberCount: 4 tier: "stable" sections: ["Import", "Methods", "Source"] sourcePath: "src/core/serialization/Prefab.ts" -sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/core/serialization/Prefab.ts#L26" +sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/core/serialization/Prefab.ts#L27" --- ## Import @@ -35,11 +35,11 @@ for (let i = 0; i \< 10; i++) \{ ## Methods -- `instantiate(loader: Loader | null): SceneNode` +- `instantiate(loader: Loader | null, registry?: SerializationRegistry): SceneNode` - `toJSON(): SerializedNode` -- `from(node: SceneNode, loader: Loader | null): Prefab` +- `from(node: SceneNode, loader: Loader | null, registry?: SerializationRegistry): Prefab` - `fromJSON(descriptor: SerializedNode): Prefab` ## Source -[src/core/serialization/Prefab.ts](https://github.com/Exoridus/ExoJS/blob/main/src/core/serialization/Prefab.ts#L26) +[src/core/serialization/Prefab.ts](https://github.com/Exoridus/ExoJS/blob/main/src/core/serialization/Prefab.ts#L27) diff --git a/site/src/content/guide/debugging/custom-renderers.mdx b/site/src/content/guide/debugging/custom-renderers.mdx index 6b1bccaa..3e590fa4 100644 --- a/site/src/content/guide/debugging/custom-renderers.mdx +++ b/site/src/content/guide/debugging/custom-renderers.mdx @@ -129,6 +129,8 @@ Direct backend access is deliberately unabstracted — you are writing raw WebGP ExoJS resolves a renderer for each drawable type through the `RendererRegistry` (available from `@codexo/exojs/renderer-sdk`). You can register a custom renderer for an existing drawable type via `backend.rendererRegistry.registerRenderer(drawableConstructor, renderer)`. A renderer implements `connect(backend)`, `disconnect()`, `render(drawable)`, and `flush()` — these map to GPU resource acquisition, release, per-drawable recording, and batch submission respectively. +In practice you build a custom renderer by extending one of the **abstract** base renderers from `@codexo/exojs/renderer-sdk` — `AbstractWebGl2Renderer` / `AbstractWebGl2BatchedRenderer` (WebGL2) or `AbstractWebGpuRenderer` (WebGPU) — which is exactly how the `@codexo/exojs-particles` and `@codexo/exojs-tilemap` packages build theirs. The engine's built-in *concrete* renderers (e.g. `WebGl2SpriteRenderer`) are internal: coupled to internal sprite/mesh data paths and intentionally not part of the SDK surface, so don't subclass them directly. + Registering a custom renderer is an advanced extension point. For most custom rendering needs — procedural geometry between draws, a single custom shape that doesn't fit `Mesh` — `CallbackRenderPass` is the simpler and more intentional path. ## When to use which diff --git a/src/audio/AudioManager.ts b/src/audio/AudioManager.ts index 8c916b52..ef3407ec 100644 --- a/src/audio/AudioManager.ts +++ b/src/audio/AudioManager.ts @@ -1,5 +1,6 @@ import { Signal } from '#core/Signal'; import type { System } from '#core/System'; +import type { Time } from '#core/Time'; import { getAudioContext, isAudioContextReady, onAudioContextReady } from './audio-context'; import { AudioBus } from './AudioBus'; @@ -132,8 +133,8 @@ export class AudioManager implements System { }); } - /** Called once per frame from Application.update(). */ - public update(): void { + /** Called once per frame from Application.update(). The frame delta is unused here (hence `_delta`); present for {@link System} contract compliance. */ + public update(_delta: Time): void { this.listener._tick(); // Tick spatial voices and prune ended ones. for (const voice of this._spatial) { diff --git a/src/core/serialization/Prefab.ts b/src/core/serialization/Prefab.ts index a2542076..59cb4517 100644 --- a/src/core/serialization/Prefab.ts +++ b/src/core/serialization/Prefab.ts @@ -1,6 +1,7 @@ import type { SceneNode } from '#core/SceneNode'; import type { Loader } from '#resources/Loader'; +import type { SerializationRegistry } from './SerializationRegistry'; import { deserializeTree, serializeTree } from './serialize'; import type { SerializedNode } from './types'; @@ -28,10 +29,12 @@ export class Prefab { /** * Capture `node`'s subtree as a prefab. Pass the {@link Loader} so texture and - * other asset references resolve to their source keys. + * other asset references resolve to their source keys. Pass `app.serializers` + * as `registry` to resolve app-scoped (extension) serializers; defaults to the + * global registry. */ - public static from(node: SceneNode, loader: Loader | null = null): Prefab { - return new Prefab(serializeTree(node, loader)); + public static from(node: SceneNode, loader: Loader | null = null, registry?: SerializationRegistry): Prefab { + return new Prefab(serializeTree(node, loader, registry)); } /** @@ -46,9 +49,11 @@ export class Prefab { /** * Instantiate a fresh, independent copy of the captured subtree. Referenced * assets must be pre-loaded into `loader`. Call repeatedly for many instances. + * Pass `app.serializers` as `registry` to resolve app-scoped (extension) + * serializers; defaults to the global registry. */ - public instantiate(loader: Loader | null = null): SceneNode { - return deserializeTree(this._descriptor, loader); + public instantiate(loader: Loader | null = null, registry?: SerializationRegistry): SceneNode { + return deserializeTree(this._descriptor, loader, registry); } /** The underlying JSON descriptor (JSON-serialisable). Treat as read-only. The standard `JSON.stringify(prefab)` hook. */ diff --git a/src/core/serialization/SerializationRegistry.ts b/src/core/serialization/SerializationRegistry.ts index 61a8e7ab..efda24ad 100644 --- a/src/core/serialization/SerializationRegistry.ts +++ b/src/core/serialization/SerializationRegistry.ts @@ -93,6 +93,18 @@ export class SerializationRegistry { public hasType(typeName: string): boolean { return this._byName.has(typeName) || (this._fallback?.hasType(typeName) ?? false); } + + /** + * Remove every own registration (both the name and constructor maps). The + * fallback chain is left untouched. Test infrastructure only — used to reset + * the {@link defaultSerializationRegistry} between suites so process-wide + * registrations do not leak across them. + * @internal + */ + public clear(): void { + this._byName.clear(); + this._byCtor.destroy(); + } } /** @@ -103,7 +115,10 @@ export class SerializationRegistry { export const defaultSerializationRegistry = new SerializationRegistry(); /** - * Register a custom {@link NodeSerializer} on the {@link defaultSerializationRegistry}. + * Register a custom {@link NodeSerializer}. Defaults to the + * {@link defaultSerializationRegistry}; pass `app.serializers` to register the + * type only for that {@link Application} (it stays isolated from other apps and + * from the global registry, resolving through the fallback chain). * * Use this to make your own {@link SceneNode} subclasses serializable. Delegate * to a base type's behaviour by composing with the framework helpers, or @@ -116,6 +131,11 @@ export const defaultSerializationRegistry = new SerializationRegistry(); * }); * ``` */ -export function registerSerializer(typeName: string, ctor: SceneNodeConstructor, serializer: NodeSerializer): void { - defaultSerializationRegistry.register(typeName, ctor, serializer); +export function registerSerializer( + typeName: string, + ctor: SceneNodeConstructor, + serializer: NodeSerializer, + registry: SerializationRegistry = defaultSerializationRegistry, +): void { + registry.register(typeName, ctor, serializer); } diff --git a/src/core/serialization/serialize.ts b/src/core/serialization/serialize.ts index 8ea5ec1c..fc49ee07 100644 --- a/src/core/serialization/serialize.ts +++ b/src/core/serialization/serialize.ts @@ -26,6 +26,23 @@ function ensureCoreSerializers(): void { registerCoreSerializers(defaultSerializationRegistry); } +/** + * Reset the process-wide serialization state so test suites do not leak + * registrations into one another. Clears both module-level states: the + * {@link defaultSerializationRegistry} **and** the `_coreRegistered` latch. Both + * are mandatory — clearing the registry alone would leave the latch `true`, so + * the core serializers would never re-register and later round-trips would fail + * with spurious "No serializer registered" errors. + * + * Not exported from the public barrel; import via the direct module path in + * tests. + * @internal — For unit tests only. + */ +export function _resetDefaultSerializers(): void { + _coreRegistered = false; + defaultSerializationRegistry.clear(); +} + function createSerializeContext(loader: Loader | null, registry: SerializationRegistry): SerializeContext { const ctx: SerializeContext = { version: SERIALIZATION_VERSION, diff --git a/src/input/InputManager.ts b/src/input/InputManager.ts index 097bdc85..066d9c9e 100644 --- a/src/input/InputManager.ts +++ b/src/input/InputManager.ts @@ -1,6 +1,7 @@ import type { Application } from '#core/Application'; import { Signal } from '#core/Signal'; import type { System } from '#core/System'; +import type { Time } from '#core/Time'; import { stopEvent } from '#core/utils'; import { Flags } from '#math/Flags'; import { getDistance } from '#math/utils'; @@ -333,7 +334,7 @@ export class InputManager implements System { * the channel buffer, fires the corresponding Signals, then evaluates * each registered binding. */ - public update(): this { + public update(_delta: Time): void { this.updateGamepads(); for (const binding of this.bindings) { @@ -343,8 +344,6 @@ export class InputManager implements System { if (this.flags.value !== InputManagerFlag.None) { this.updateEvents(); } - - return this; } public destroy(): void { diff --git a/src/input/InteractionManager.ts b/src/input/InteractionManager.ts index 29f6a960..64e155c5 100644 --- a/src/input/InteractionManager.ts +++ b/src/input/InteractionManager.ts @@ -2,6 +2,7 @@ import type { Application } from '#core/Application'; import type { Signal } from '#core/Signal'; import type { InteractionHooks, Stage } from '#core/Stage'; import type { System } from '#core/System'; +import type { Time } from '#core/Time'; import type { PointLike } from '#math/PointLike'; import type { QuadtreeItem } from '#math/Quadtree'; import { Quadtree } from '#math/Quadtree'; @@ -252,7 +253,7 @@ export class InteractionManager implements InteractionHooks, System { * activity; every signal handler that enqueues an event sets `_dirty = * true`, and `update()` clears it at the top before draining the queue. */ - public update(): void { + public update(_delta: Time): void { if (!this._dirty) return; this._dirty = false; diff --git a/src/renderer-sdk.ts b/src/renderer-sdk.ts index efa18506..6b4c459a 100644 --- a/src/renderer-sdk.ts +++ b/src/renderer-sdk.ts @@ -3,6 +3,13 @@ // backends, low-level GL/GPU building blocks). Ordinary application code should // import from the root `@codexo/exojs` barrel; these symbols are intentionally // kept out of it (see src/rendering/public.ts). +// +// Custom renderers extend the ABSTRACT renderers below (AbstractWebGl2Renderer / +// AbstractWebGl2BatchedRenderer / AbstractWebGpuRenderer) — the subclass-stable +// contract used by the exojs-particles and exojs-tilemap packages. The engine's +// built-in CONCRETE renderers (WebGl2SpriteRenderer, WebGpuMeshRenderer, …) are +// internal and intentionally NOT exported here: they are coupled to internal +// sprite/mesh data paths and are not a stable subclassing surface pre-1.0. export { Drawable } from '#rendering/Drawable'; export type { PixelSnapMode } from '#rendering/pixelSnap'; diff --git a/src/rendering/index.ts b/src/rendering/index.ts deleted file mode 100644 index 53017109..00000000 --- a/src/rendering/index.ts +++ /dev/null @@ -1,121 +0,0 @@ -/// - -export type { BackendRenderPass } from './BackendRenderPass'; -export type { CallbackRenderPassOptions } from './CallbackRenderPass'; -export { CallbackRenderPass } from './CallbackRenderPass'; -export type { CameraOptions } from './Camera'; -export { Camera } from './Camera'; -export { Container } from './Container'; -export { Drawable } from './Drawable'; -export type { PixelSnapMode } from './pixelSnap'; -export type { RenderBackend } from './RenderBackend'; -export { RenderBackendType } from './RenderBackendType'; -export { RenderBatch } from './RenderBatch'; -export type { DrawableConstructor, Renderer } from './Renderer'; -export { RendererRegistry } from './RendererRegistry'; -export type { DrawBatchOptions, DrawGeometryOptions, RenderOptions, RenderToOptions } from './RenderingContext'; -export { RenderingContext } from './RenderingContext'; -export type { MaskSource } from './RenderNode'; -export { RenderNode } from './RenderNode'; -export type { RenderNodePassOptions } from './RenderNodePass'; -export { RenderNodePass } from './RenderNodePass'; -export type { RenderPassOptions } from './RenderPass'; -export { RenderPass } from './RenderPass'; -export { RenderPipeline } from './RenderPipeline'; -export type { RenderStats } from './RenderStats'; -export { createRenderStats, resetRenderStats } from './RenderStats'; -export { RenderTarget } from './RenderTarget'; -export { BlendModes, BufferTypes, BufferUsage, RenderingPrimitives, ScaleModes, ShaderPrimitives, WrapModes } from './types'; -export type { ViewFollowOptions, ViewFollowTarget, ViewShakeOptions } from './View'; -export { View, ViewFlags } from './View'; -export type { BlurFilterOptions } from '#rendering/filters/BlurFilter'; -export { BlurFilter } from '#rendering/filters/BlurFilter'; -export { ColorFilter } from '#rendering/filters/ColorFilter'; -export { Filter } from '#rendering/filters/Filter'; -export type { LutFilterOptions, LutMode } from '#rendering/filters/LutFilter'; -export { LutFilter } from '#rendering/filters/LutFilter'; -export type { ShaderFilterUniformValue, WebGl2ShaderFilterOptions } from '#rendering/filters/WebGl2ShaderFilter'; -export { WebGl2ShaderFilter } from '#rendering/filters/WebGl2ShaderFilter'; -export type { WebGpuShaderFilterOptions } from '#rendering/filters/WebGpuShaderFilter'; -export { WebGpuShaderFilter } from '#rendering/filters/WebGpuShaderFilter'; -export { Geometry } from '#rendering/geometry/Geometry'; -export type { AttributeType, GeometryAttribute, GeometryOptions, GeometryUsage, Topology } from '#rendering/geometry/GeometryAttribute'; -export type { GradientStop, GradientToTextureOptions, GradientType } from '#rendering/gradient/Gradient'; -export { Gradient } from '#rendering/gradient/Gradient'; -export { LinearGradient } from '#rendering/gradient/LinearGradient'; -export { RadialGradient } from '#rendering/gradient/RadialGradient'; -export type { MaterialOptions, UniformValue } from '#rendering/material/Material'; -export { Material } from '#rendering/material/Material'; -export { MeshMaterial } from '#rendering/material/MeshMaterial'; -export type { ShaderSourceOptions } from '#rendering/material/ShaderSource'; -export { ShaderSource } from '#rendering/material/ShaderSource'; -export { SpriteMaterial } from '#rendering/material/SpriteMaterial'; -export type { MeshOptions } from '#rendering/mesh/Mesh'; -export { Mesh } from '#rendering/mesh/Mesh'; -export { Graphics } from '#rendering/primitives/Graphics'; -export type { ShaderProgram } from '#rendering/shader/Shader'; -export { Shader } from '#rendering/shader/Shader'; -export { ShaderAttribute } from '#rendering/shader/ShaderAttribute'; -export { ShaderUniform } from '#rendering/shader/ShaderUniform'; -export { upgradeFragmentShaderToGl300 } from '#rendering/shader/upgradeFragmentShaderToGl300'; -export type { AnimatedSpriteClipDefinition, AnimatedSpritePlayOptions } from '#rendering/sprite/AnimatedSprite'; -export { AnimatedSprite } from '#rendering/sprite/AnimatedSprite'; -export type { NineSliceInsets, NineSliceModes, NineSliceOptions } from '#rendering/sprite/nineSlice'; -export { NineSliceSprite } from '#rendering/sprite/NineSliceSprite'; -export { RepeatingSprite } from '#rendering/sprite/RepeatingSprite'; -export type { RepeatingSpriteOptions } from '#rendering/sprite/repeatingSpritePlan'; -export { Sprite, SpriteFlags } from '#rendering/sprite/Sprite'; -export type { SpritesheetData, SpritesheetFrame } from '#rendering/sprite/Spritesheet'; -export { Spritesheet } from '#rendering/sprite/Spritesheet'; -export { AbstractText } from '#rendering/text/AbstractText'; -export type { BitmapTextOptions } from '#rendering/text/BitmapText'; -export { BitmapText, BmFontAdapter } from '#rendering/text/BitmapText'; -export type { BmFontChar, BmFontData } from '#rendering/text/BmFont'; -export { BmFont } from '#rendering/text/BmFont'; -export type { AtlasMode } from '#rendering/text/GlyphAtlas'; -export { AtlasPage, GlyphAtlas, SDF_RADIUS } from '#rendering/text/GlyphAtlas'; -export { getDefaultGlyphAtlasPool, GlyphAtlasPool, resetDefaultGlyphAtlasPool } from '#rendering/text/GlyphAtlasPool'; -export type { FontFormat, HTMLTextOptions } from '#rendering/text/HTMLText'; -export { HTMLText } from '#rendering/text/HTMLText'; -export type { LayoutOptions } from '#rendering/text/LayoutOptions'; -export { Text } from '#rendering/text/Text'; -export { buildTextPageQuads, layoutText, measureText } from '#rendering/text/TextLayout'; -export type { FontFamily, FontRegistry, FontWeight, GradientAxis, StyleChangeHint, TextStyleOptions } from '#rendering/text/TextStyle'; -export { TextStyle } from '#rendering/text/TextStyle'; -export type { GlyphInfo, GlyphKey, GlyphPlacement, GlyphProvider, TextAlignment, TextLayoutStyle, TextPageQuads, TextSize } from '#rendering/text/types'; -export type { DataTextureBuffer, DataTextureDirtyRegion, DataTextureFormat, DataTextureOptions } from '#rendering/texture/DataTexture'; -export { DataTexture } from '#rendering/texture/DataTexture'; -export { RenderTexture } from '#rendering/texture/RenderTexture'; -export type { RepeatFit, RepeatMode, RepeatPlan, RepeatSegment } from '#rendering/texture/repeat'; -export { planRepeat } from '#rendering/texture/repeat'; -export type { SamplerOptions } from '#rendering/texture/Sampler'; -export { Sampler } from '#rendering/texture/Sampler'; -export { Texture } from '#rendering/texture/Texture'; -export type { TextureRegionInsets, TextureRegionOptions } from '#rendering/texture/TextureRegion'; -export { TextureRegion } from '#rendering/texture/TextureRegion'; -export { Video } from '#rendering/video/Video'; -export { AbstractWebGl2BatchedRenderer } from '#rendering/webgl2/AbstractWebGl2BatchedRenderer'; -export { AbstractWebGl2Renderer } from '#rendering/webgl2/AbstractWebGl2Renderer'; -export { WebGl2Backend } from '#rendering/webgl2/WebGl2Backend'; -export { WebGl2MeshRenderer } from '#rendering/webgl2/WebGl2MeshRenderer'; -export { WebGl2NineSliceSpriteRenderer } from '#rendering/webgl2/WebGl2NineSliceSpriteRenderer'; -export type { WebGl2RenderBufferRuntime } from '#rendering/webgl2/WebGl2RenderBuffer'; -export { WebGl2RenderBuffer } from '#rendering/webgl2/WebGl2RenderBuffer'; -export { WebGl2RepeatingSpriteRenderer } from '#rendering/webgl2/WebGl2RepeatingSpriteRenderer'; -export { WebGl2ShaderBlock } from '#rendering/webgl2/WebGl2ShaderBlock'; -export { webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames } from '#rendering/webgl2/WebGl2ShaderMappings'; -export { createWebGl2ShaderProgram } from '#rendering/webgl2/WebGl2ShaderProgram'; -export { WebGl2SpriteRenderer } from '#rendering/webgl2/WebGl2SpriteRenderer'; -export { WebGl2TextRenderer } from '#rendering/webgl2/WebGl2TextRenderer'; -export type { WebGl2VertexArrayObjectRuntime } from '#rendering/webgl2/WebGl2VertexArrayObject'; -export { WebGl2VertexArrayObject } from '#rendering/webgl2/WebGl2VertexArrayObject'; -export { AbstractWebGpuRenderer } from '#rendering/webgpu/AbstractWebGpuRenderer'; -export type { ComputeBinding } from '#rendering/webgpu/compute/index'; -export { WebGpuComputePipeline, WebGpuStorageBuffer } from '#rendering/webgpu/compute/index'; -export { WebGpuBackend } from '#rendering/webgpu/WebGpuBackend'; -export { getWebGpuBlendState } from '#rendering/webgpu/WebGpuBlendState'; -export { WebGpuMeshRenderer } from '#rendering/webgpu/WebGpuMeshRenderer'; -export { WebGpuNineSliceSpriteRenderer } from '#rendering/webgpu/WebGpuNineSliceSpriteRenderer'; -export { WebGpuRepeatingSpriteRenderer } from '#rendering/webgpu/WebGpuRepeatingSpriteRenderer'; -export { WebGpuSpriteRenderer } from '#rendering/webgpu/WebGpuSpriteRenderer'; -export { WebGpuTextRenderer } from '#rendering/webgpu/WebGpuTextRenderer'; diff --git a/src/rendering/public.ts b/src/rendering/public.ts index 03d767c9..17807b79 100644 --- a/src/rendering/public.ts +++ b/src/rendering/public.ts @@ -3,8 +3,9 @@ // @codexo/exojs application-facing rendering surface. Re-exported by the root // barrel (`src/index.ts`). Backend/renderer-author internals (abstract renderers, // concrete backend renderers, VAOs, shader programs, glyph/text layout helpers) -// are intentionally NOT here — they live in the internal `#rendering/index` -// barrel and, for the curated public author surface, in `@codexo/exojs/renderer-sdk`. +// are intentionally NOT here — they live in their own modules under +// `#rendering/**` and, for the curated public author surface, in +// `@codexo/exojs/renderer-sdk`. export type { BackendRenderPass } from './BackendRenderPass'; export type { CallbackRenderPassOptions } from './CallbackRenderPass'; diff --git a/test/core/scene-systems.test.ts b/test/core/scene-systems.test.ts index eb9da56a..0fbd6443 100644 --- a/test/core/scene-systems.test.ts +++ b/test/core/scene-systems.test.ts @@ -1,6 +1,9 @@ +import { AudioManager } from '#audio/AudioManager'; import { Scene } from '#core/Scene'; import type { System } from '#core/System'; import { Time } from '#core/Time'; +import { InputManager } from '#input/InputManager'; +import { InteractionManager } from '#input/InteractionManager'; // Scene-bound system registry: systems tick after Scene.update in ascending // `order`, structural mutations during a tick are deferred, and systems are @@ -124,3 +127,11 @@ describe('Scene.systems', () => { scene.destroy(); }); }); + +describe('System.update contract', () => { + test('InputManager, InteractionManager, AudioManager all satisfy (delta: Time) => void', () => { + expectTypeOf(InputManager.prototype.update).toEqualTypeOf<(delta: Time) => void>(); + expectTypeOf(InteractionManager.prototype.update).toEqualTypeOf<(delta: Time) => void>(); + expectTypeOf(AudioManager.prototype.update).toEqualTypeOf<(delta: Time) => void>(); + }); +}); diff --git a/test/core/serialization.test.ts b/test/core/serialization.test.ts index 16af4782..d6f8cb14 100644 --- a/test/core/serialization.test.ts +++ b/test/core/serialization.test.ts @@ -5,8 +5,8 @@ import { Scene } from '#core/Scene'; import { SceneNode } from '#core/SceneNode'; import { Prefab } from '#core/serialization/Prefab'; import { SaveManager } from '#core/serialization/SaveManager'; -import { registerSerializer, SerializationRegistry } from '#core/serialization/SerializationRegistry'; -import { deserializeTree, serializeTree } from '#core/serialization/serialize'; +import { defaultSerializationRegistry, registerSerializer, SerializationRegistry } from '#core/serialization/SerializationRegistry'; +import { _resetDefaultSerializers, deserializeTree, serializeTree } from '#core/serialization/serialize'; import { SERIALIZATION_VERSION, type SerializedNode } from '#core/serialization/types'; import { Rectangle } from '#math/Rectangle'; import { Container } from '#rendering/Container'; @@ -76,6 +76,11 @@ const mockPool = { getAtlas: vi.fn(() => mockAtlas) }; beforeEach(() => resetDefaultGlyphAtlasPool(mockPool as unknown as GlyphAtlasPool)); afterEach(() => resetDefaultGlyphAtlasPool()); +// Tests here register serializers on the process-wide default registry and lazy-init +// the core serializers; reset both module states after each test so registrations do +// not leak into sibling suites (e.g. the `Marker` custom-serializer test). +afterEach(_resetDefaultSerializers); + /** * Minimal {@link Loader} stand-in implementing only the two methods the * serialization context calls — keeps the round-trip tests free of real asset @@ -290,6 +295,42 @@ describe('serialization — custom serializer', () => { expect(restored.kind).toBe('checkpoint'); expect(restored.x).toBe(5); }); + + it('registers app-scoped without leaking into the global registry', () => { + class AppOnly extends SceneNode { + public tag = 'app'; + } + + // `app.serializers` is a SerializationRegistry chained to the global one; + // construct that directly to keep the test free of a full Application. + const appSerializers = new SerializationRegistry(defaultSerializationRegistry); + const serializer = { + write: (node: AppOnly) => ({ tag: node.tag }), + read: () => new AppOnly(), + }; + + registerSerializer('AppOnly', AppOnly, serializer, appSerializers); + + // The app-scoped registry resolves it, but the global registry does not. + expect(appSerializers.hasType('AppOnly')).toBe(true); + expect(appSerializers.resolveByName('AppOnly')?.serializer).toBe(serializer); + expect(defaultSerializationRegistry.hasType('AppOnly')).toBe(false); + }); +}); + +describe('serialization — default-registry reset', () => { + it('clears the lazily-registered core serializers until the next use', () => { + // serializeTree lazy-registers the core serializers on the global registry. + serializeTree(new Container()); + expect(defaultSerializationRegistry.hasType('Container')).toBe(true); + + _resetDefaultSerializers(); + expect(defaultSerializationRegistry.hasType('Container')).toBe(false); + + // The next serialize re-arms the latch and re-registers the core serializers. + serializeTree(new Container()); + expect(defaultSerializationRegistry.hasType('Container')).toBe(true); + }); }); describe('serialization — errors & version', () => { diff --git a/test/rendering/pass/web-gl2-pass-coordinator.test.ts b/test/rendering/pass/web-gl2-pass-coordinator.test.ts index 85e4ec33..5be4c7c2 100644 --- a/test/rendering/pass/web-gl2-pass-coordinator.test.ts +++ b/test/rendering/pass/web-gl2-pass-coordinator.test.ts @@ -234,8 +234,8 @@ describe('WebGl2PassCoordinator', () => { }); describe('render pass internals are not publicly exported', () => { - test('the rendering barrel exposes neither the coordinator nor the stencil enum', async () => { - const rendering = (await import('#rendering/index')) as unknown as Record; + test('the app-facing rendering surface exposes neither the coordinator nor the stencil enum', async () => { + const rendering = (await import('#rendering/public')) as unknown as Record; expect(rendering['WebGl2PassCoordinator']).toBeUndefined(); expect(rendering['StencilAttachmentMode']).toBeUndefined();