Skip to content

feat(sprite): add renderer-native Sprite2D Y-sort - #595

Open
VicenteCartas wants to merge 4 commits into
BabylonJS:masterfrom
VicenteCartas:feat/sprite2d-y-sort
Open

feat(sprite): add renderer-native Sprite2D Y-sort#595
VicenteCartas wants to merge 4 commits into
BabylonJS:masterfrom
VicenteCartas:feat/sprite2d-y-sort

Conversation

@VicenteCartas

@VicenteCartas VicenteCartas commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🤖 This PR was created by the create-pr skill.

Purpose

Top-down, isometric, and 2.5D sprite games need characters and scenery to overlap according to their world Y position. Doing this outside Lite would require rearranging private packed instance buffers, which would break public numeric indices and stable sprite handles.

This PR adds an optional renderer-native Y-sort for pure Sprite2DLayers. It preserves canonical CPU storage and identity while uploading a separately packed GPU draw order.

Behavior

  • Sorts ascending by positionPx.y + bias, so larger effective Y draws later/on top
  • Uses persistent insertion serials for deterministic equal-Y ties
  • Supports per-index and handle-safe baseline bias
  • Preserves logical indices, swap-remove behavior, and Sprite2DHandle identity
  • Makes pickSprite2D resolve the same topmost sprite the GPU displays
  • Reuses permutation, inverse, and staging buffers with no steady-state allocations
  • Re-sorts only when Y, bias, membership, or capacity changes
  • Supports pure depth: "none" layers and rejects depth-hosted layers explicitly
  • Can be disabled to release optional state and restore canonical draw order

Pay-for-use

Y-sort is installed through an explicit optional module and a null-by-default hook. Ordinary Sprite2D scenes retain zero Y-sort bytes. Scene 50 remains byte-identical at 16,168 bytes and excludes all Y-sort modules.

Validation

  • 129 focused Sprite2D, renderer, picker, handle, and API tests pass
  • Scene 303 verifies live Y movement, stable equal-Y ties, and bias through rendered overlap pixels and picking
  • Deliberately disabling Y-sort makes Scene 303 fail as expected
  • Scene 303 measures 20,249 bytes raw / 8.5 KB gzip against a 25 KB ceiling
  • Package, lab, and test typechecks pass
  • Public API/declaration tests pass
  • ESLint, targeted Prettier, and diff hygiene pass

Scene 303 is intentionally skipParity: Babylon.js has no equivalent pure-2D GPU-order staging API to use as a pixel oracle.

@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index 319e8155..4b340401 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -2117,6 +2117,9 @@ export interface DirectionalLight extends LightBase {
 // @public
 export function disableOrthographicCamera(camera: Camera): void;
 
+// @public
+export function disableSprite2DYSort(layer: Sprite2DLayer): boolean;
+
 // @public
 export interface DiscOptions {
     arc?: number;
@@ -2466,6 +2469,9 @@ export function enableSkeletonShadows(generator: ShadowGenerator): void;
 // @public
 export function enableSpatial(host: AudioGraphHost, options?: SpatialSoundOptions): void;
 
+// @public
+export function enableSprite2DYSort(layer: Sprite2DLayer, options?: Sprite2DYSortOptions): Sprite2DYSortState;
+
 // @public
 export function enableStandardSkeleton(): void;
 
@@ -5861,6 +5867,12 @@ export function setSprite2DShaderParams(layer: Sprite2DLayer, params: readonly [
 // @public
 export function setSprite2DUvOffset(layer: Sprite2DLayer, index: number, uvOffset: readonly [number, number]): void;
 
+// @public
+export function setSprite2DYSortBias(layer: Sprite2DLayer, index: number, bias: number): void;
+
+// @public
+export function setSprite2DYSortHandleBias(handle: Sprite2DHandle, bias: number): void;
+
 // @public
 export function setSpriteRendererTarget(sr: SpriteRenderer, target: Texture2D | null): void;
 
@@ -6383,6 +6395,18 @@ export interface Sprite2DView {
     zoom: number;
 }
 
+// @public
+export interface Sprite2DYSortOptions {
+    defaultBias?: number;
+}
+
+// @public
+export interface Sprite2DYSortState {
+    readonly defaultBias: number;
+    readonly enabled: boolean;
+    readonly layer: Sprite2DLayer;
+}
+
 // @public
 export interface SpriteAnimationBinding {
     // (undocumented)

@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

Lite Playground - Static Site

Open deployed site

Build 20260821.7 - merge @ f5cce63

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@VicenteCartas
VicenteCartas marked this pull request as ready for review August 21, 2026 22:08
Copilot AI lite review requested due to automatic review settings August 21, 2026 22:08
@VicenteCartas

Copy link
Copy Markdown
Contributor Author

Self code review completed by the code-review skill. Review fixes: 9cdfe11d

  • Removed unrelated numeric-format churn from existing scene ceilings.
  • Made repeated enableSprite2DYSort calls validate non-finite defaultBias values without mutating installed state.
  • Documented and tested that disable/re-enable creates fresh bias and insertion-serial state while valid repeated enable remains idempotent.

The full Y-sort implementation and correction round were independently approved with no remaining substantive findings.

@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index 319e8155..4b340401 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -2117,6 +2117,9 @@ export interface DirectionalLight extends LightBase {
 // @public
 export function disableOrthographicCamera(camera: Camera): void;
 
+// @public
+export function disableSprite2DYSort(layer: Sprite2DLayer): boolean;
+
 // @public
 export interface DiscOptions {
     arc?: number;
@@ -2466,6 +2469,9 @@ export function enableSkeletonShadows(generator: ShadowGenerator): void;
 // @public
 export function enableSpatial(host: AudioGraphHost, options?: SpatialSoundOptions): void;
 
+// @public
+export function enableSprite2DYSort(layer: Sprite2DLayer, options?: Sprite2DYSortOptions): Sprite2DYSortState;
+
 // @public
 export function enableStandardSkeleton(): void;
 
@@ -5861,6 +5867,12 @@ export function setSprite2DShaderParams(layer: Sprite2DLayer, params: readonly [
 // @public
 export function setSprite2DUvOffset(layer: Sprite2DLayer, index: number, uvOffset: readonly [number, number]): void;
 
+// @public
+export function setSprite2DYSortBias(layer: Sprite2DLayer, index: number, bias: number): void;
+
+// @public
+export function setSprite2DYSortHandleBias(handle: Sprite2DHandle, bias: number): void;
+
 // @public
 export function setSpriteRendererTarget(sr: SpriteRenderer, target: Texture2D | null): void;
 
@@ -6383,6 +6395,18 @@ export interface Sprite2DView {
     zoom: number;
 }
 
+// @public
+export interface Sprite2DYSortOptions {
+    defaultBias?: number;
+}
+
+// @public
+export interface Sprite2DYSortState {
+    readonly defaultBias: number;
+    readonly enabled: boolean;
+    readonly layer: Sprite2DLayer;
+}
+
 // @public
 export interface SpriteAnimationBinding {
     // (undocumented)

@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

Lite Playground - Static Site

Open deployed site

Build 20260821.9 - merge @ 67e2c8f

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in, renderer-native stable Y-sort path for pure Sprite2DLayers so top-down/isometric sprite overlap (and pickSprite2D) matches GPU draw order without mutating canonical CPU instance storage or breaking stable handle/index identity.

Changes:

  • Introduces optional Y-sort state, sorting/permutation logic, and packed GPU-order upload path behind a null-by-default hook.
  • Integrates the hook into Sprite2D mutation tracking, instance uploads, and CPU picking to keep visual and pick order consistent.
  • Adds focused unit + Playwright parity coverage plus bundle-size isolation assertions and a new Scene 303 demo/config.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/lite/unit/sprite-2d-y-sort.test.ts New unit tests covering ordering, stability, bias, dirty behavior, and enable/disable semantics.
tests/lite/parity/scenes/scene303-sprite2d-y-sort.spec.ts New Playwright spec validating visible overlap pixels and picking for Scene 303.
tests/lite/parity/bundle-size.spec.ts Adds bundle-size / runtime-module assertions to ensure Y-sort remains opt-in and Scene 303 stays on the intended sprite path.
scene-config.json Registers Scene 303 with ceiling/tags and skipParity rationale.
packages/babylon-lite/src/sprite/sprite-pipeline.ts Routes instance uploads through the optional Y-sort hook when present.
packages/babylon-lite/src/sprite/sprite-2d.ts Adds _ySortState field and forwards add/remove/clear/dirty events to the Y-sort hook.
packages/babylon-lite/src/sprite/sprite-2d-y-sort.ts New optional Y-sort implementation: stable sort, permutation/inverse, packed staging, bias API, enable/disable.
packages/babylon-lite/src/sprite/sprite-2d-y-sort-hook.ts New null-by-default hook contract + global registration getter/setter.
packages/babylon-lite/src/sprite/sprite-2d-handle-y-sort.ts New optional handle-based bias setter that resolves current index.
packages/babylon-lite/src/sprite/picking/pick-sprite-2d.ts Updates picking to respect Y-sort draw permutation when enabled.
packages/babylon-lite/src/index.ts Re-exports the new Y-sort APIs/types from the root entry point.
lab/public/bundle/manifest/scene303.json Adds committed bundle manifest for Scene 303 size tracking.
lab/lite/src/lite/scene303.ts New demo scene exercising live Y changes, equal-Y ties, and bias behavior (plus dataset for tests).
lab/lite/scene303.html New dev HTML entry for Scene 303.
lab/lite/bundle-scene303.html New bundled HTML entry for Scene 303.
docs/lite/architecture/32-sprites.md Documents the Y-sort feature, constraints, semantics, and its interaction with picking/uploads/bundle ceilings.
Suppressed comments (1)

packages/babylon-lite/src/sprite/sprite-2d-y-sort.ts:418

  • Same key !== state._keys[index] issue as in observeDirty: if positionPx.y ever becomes NaN, this comparison will always evaluate true and keep forcing _sortDirty/_fullUpload, leading to repeated full uploads even when the effective key didn’t change. A NaN-stable equality check (e.g. Object.is) would avoid this churn.
    const key = keyAt(layer, state, index);
    if (key !== state._keys[index]) {
        state._keys[index] = key;
        state._sortDirty = true;
        state._fullUpload = true;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/babylon-lite/src/sprite/sprite-2d-y-sort.ts
Comment thread packages/babylon-lite/src/sprite/sprite-2d-y-sort.ts Outdated
@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index 319e8155..4b340401 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -2117,6 +2117,9 @@ export interface DirectionalLight extends LightBase {
 // @public
 export function disableOrthographicCamera(camera: Camera): void;
 
+// @public
+export function disableSprite2DYSort(layer: Sprite2DLayer): boolean;
+
 // @public
 export interface DiscOptions {
     arc?: number;
@@ -2466,6 +2469,9 @@ export function enableSkeletonShadows(generator: ShadowGenerator): void;
 // @public
 export function enableSpatial(host: AudioGraphHost, options?: SpatialSoundOptions): void;
 
+// @public
+export function enableSprite2DYSort(layer: Sprite2DLayer, options?: Sprite2DYSortOptions): Sprite2DYSortState;
+
 // @public
 export function enableStandardSkeleton(): void;
 
@@ -5861,6 +5867,12 @@ export function setSprite2DShaderParams(layer: Sprite2DLayer, params: readonly [
 // @public
 export function setSprite2DUvOffset(layer: Sprite2DLayer, index: number, uvOffset: readonly [number, number]): void;
 
+// @public
+export function setSprite2DYSortBias(layer: Sprite2DLayer, index: number, bias: number): void;
+
+// @public
+export function setSprite2DYSortHandleBias(handle: Sprite2DHandle, bias: number): void;
+
 // @public
 export function setSpriteRendererTarget(sr: SpriteRenderer, target: Texture2D | null): void;
 
@@ -6383,6 +6395,18 @@ export interface Sprite2DView {
     zoom: number;
 }
 
+// @public
+export interface Sprite2DYSortOptions {
+    defaultBias?: number;
+}
+
+// @public
+export interface Sprite2DYSortState {
+    readonly defaultBias: number;
+    readonly enabled: boolean;
+    readonly layer: Sprite2DLayer;
+}
+
 // @public
 export interface SpriteAnimationBinding {
     // (undocumented)

@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

Lite Playground - Static Site

Open deployed site

Build 20260821.11 - merge @ 19e2ccb

@VicenteCartas

Copy link
Copy Markdown
Contributor Author

Bundle-size CI diagnosis and fix:

  • The 25 KB ceiling did not fail. Scene 303 rebuilt at 20,249 bytes (19.8 KB raw / 8.5 KB gzip).
  • The Object.is review fix changed emitted output by 18 bytes, exceeding the manifest validator's 10-byte drift tolerance.
  • Commit 42a94f26 regenerates the Scene 303 manifest and updates the pinned architecture value.
  • pnpm validate:bundle-manifest now passes locally: 242 scenes checked.
  • The focused Scene 303 bundle ceiling/isolation test also passes.

@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

API Changes

API Extractor detected public API changes for @babylonjs/lite.

No removed public API lines were detected; this appears to be additive.

API Extractor diff
diff --git a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
index 319e8155..4b340401 100644
--- a/home/vsts/work/1/s/test-results/api-report/target/temp/babylon-lite.api.md
+++ b/home/vsts/work/1/s/test-results/api-report/current/temp/babylon-lite.api.md
@@ -2117,6 +2117,9 @@ export interface DirectionalLight extends LightBase {
 // @public
 export function disableOrthographicCamera(camera: Camera): void;
 
+// @public
+export function disableSprite2DYSort(layer: Sprite2DLayer): boolean;
+
 // @public
 export interface DiscOptions {
     arc?: number;
@@ -2466,6 +2469,9 @@ export function enableSkeletonShadows(generator: ShadowGenerator): void;
 // @public
 export function enableSpatial(host: AudioGraphHost, options?: SpatialSoundOptions): void;
 
+// @public
+export function enableSprite2DYSort(layer: Sprite2DLayer, options?: Sprite2DYSortOptions): Sprite2DYSortState;
+
 // @public
 export function enableStandardSkeleton(): void;
 
@@ -5861,6 +5867,12 @@ export function setSprite2DShaderParams(layer: Sprite2DLayer, params: readonly [
 // @public
 export function setSprite2DUvOffset(layer: Sprite2DLayer, index: number, uvOffset: readonly [number, number]): void;
 
+// @public
+export function setSprite2DYSortBias(layer: Sprite2DLayer, index: number, bias: number): void;
+
+// @public
+export function setSprite2DYSortHandleBias(handle: Sprite2DHandle, bias: number): void;
+
 // @public
 export function setSpriteRendererTarget(sr: SpriteRenderer, target: Texture2D | null): void;
 
@@ -6383,6 +6395,18 @@ export interface Sprite2DView {
     zoom: number;
 }
 
+// @public
+export interface Sprite2DYSortOptions {
+    defaultBias?: number;
+}
+
+// @public
+export interface Sprite2DYSortState {
+    readonly defaultBias: number;
+    readonly enabled: boolean;
+    readonly layer: Sprite2DLayer;
+}
+
 // @public
 export interface SpriteAnimationBinding {
     // (undocumented)

@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

Lite Playground - Static Site

Open deployed site

Build 20260821.13 - merge @ fe2c5fb

@bjsplat

bjsplat commented Aug 21, 2026

Copy link
Copy Markdown

Lab - Static Site

Open deployed site

Build 20260821.13 - merge @ fe2c5fb

@VicenteCartas

Copy link
Copy Markdown
Contributor Author

Replacement CI build 58038 is fully green on final head 42a94f26.

The Bundle Size job now passes with Scene 303 at 20,249 bytes raw (19.8 KB / 8.5 KB gzip), below its 25 KB ceiling. API, compat, lint/typecheck, snapshot, cloud parity, performance, release markers, unit tests, and GitGuardian also pass. Both Copilot review threads remain resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants