mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
perf(navara): submit stencil stages in one render
Preserve the native stencil draws and geometry MSAA while avoiding two intermediate MRT resolves per volume. Verify original pixels, holes, overlaps, ownership and controlled comparison modes.
This commit is contained in:
1 parent
c76872dfaa
commit
15f1f9f5b6
5 files changed
+305
-3
No files matched your search
@@ -1272,5 +1272,8 @@
|
||||
"src/ui/imageryBoxNavara.test.mjs",
|
||||
"scripts/qa-w2-imagery.mjs",
|
||||
"src/globe/navara/postSamplingPass.test.mjs",
|
||||
"scripts/qa-post-sampling.mjs"
|
||||
"scripts/qa-post-sampling.mjs",
|
||||
"scripts/qa-installation-stencil.mjs",
|
||||
"scripts/qa-installation-stencil-entry.mjs",
|
||||
"src/layers/installations/render/navaraStencilBatch.test.mjs"
|
||||
]
|
||||
@@ -2705,6 +2705,7 @@
|
||||
"src/layers/bikeshare/render/navara.js",
|
||||
"src/layers/installations/render/navaraGeometry.js",
|
||||
"src/layers/installations/render/navaraOutlineBatch.js",
|
||||
"src/layers/installations/render/navaraStencilBatch.js",
|
||||
"src/layers/firms/rectangle.js",
|
||||
"src/layers/firms/render/cesium.js",
|
||||
"src/layers/radio/render/cesium.js",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Shape,
|
||||
} from 'three';
|
||||
import * as geo from '../../../geo/index.js';
|
||||
import { batchStencilDraws } from './navaraStencilBatch.js';
|
||||
|
||||
export const INSTALLATION_DRAPE_DEPTH_M = 40000;
|
||||
|
||||
@@ -135,7 +136,7 @@ export function createInstallationDrapedMesh(
|
||||
side: DoubleSide,
|
||||
});
|
||||
setupMaterialForDrape?.(material, globeNormal, globeDepth);
|
||||
const mesh = new DrapedMesh(geometry, material);
|
||||
const mesh = batchStencilDraws(new DrapedMesh(geometry, material));
|
||||
mesh.matrix.fromArray(frame);
|
||||
mesh.matrixAutoUpdate = false;
|
||||
mesh.matrixWorldNeedsUpdate = true;
|
||||
@@ -190,7 +191,7 @@ export function createInstallationOutlineDrapedMesh(
|
||||
side: DoubleSide,
|
||||
});
|
||||
setupMaterialForDrape?.(material, globeNormal, globeDepth);
|
||||
const mesh = new DrapedMesh(geometry, material);
|
||||
const mesh = batchStencilDraws(new DrapedMesh(geometry, material));
|
||||
mesh.matrix.fromArray(frame);
|
||||
mesh.matrixAutoUpdate = false;
|
||||
mesh.matrixWorldNeedsUpdate = true;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Material, Mesh } from 'three';
|
||||
|
||||
const stateKeys = [
|
||||
'side',
|
||||
'colorWrite',
|
||||
'depthWrite',
|
||||
'depthTest',
|
||||
'stencilWrite',
|
||||
'stencilFunc',
|
||||
'stencilRef',
|
||||
'stencilFuncMask',
|
||||
'stencilWriteMask',
|
||||
'stencilFail',
|
||||
'stencilZFail',
|
||||
'stencilZPass',
|
||||
];
|
||||
|
||||
// Keep Navara's three stencil draws, but submit them together. Three resolves
|
||||
// the entire multisampled MRT after each render(), even for colorWrite=false.
|
||||
// Use the public DrapedMesh.process callback to obtain the SDK's exact states.
|
||||
export function batchStencilDraws(mesh) {
|
||||
const process = mesh.process;
|
||||
if (!process || Array.isArray(mesh.material)) return mesh;
|
||||
const source = mesh.material;
|
||||
let stages = null,
|
||||
hidden = null,
|
||||
version = -1,
|
||||
disposed = false;
|
||||
const dispose = () => {
|
||||
disposed = true;
|
||||
source.removeEventListener('dispose', dispose);
|
||||
for (const stage of stages ?? []) stage.material.dispose();
|
||||
hidden?.dispose();
|
||||
stages = null;
|
||||
};
|
||||
source.addEventListener('dispose', dispose);
|
||||
mesh.process = function (render) {
|
||||
if (disposed) return;
|
||||
if (this.material !== source || this.children.length)
|
||||
return process.call(this, render);
|
||||
let index = 0;
|
||||
process.call(this, () => {
|
||||
if (!stages) {
|
||||
hidden = new Material();
|
||||
hidden.visible = false;
|
||||
stages = Array.from({ length: 3 }, (_, order) => {
|
||||
const stage = new Mesh(this.geometry, source.clone());
|
||||
stage.renderOrder = order;
|
||||
return stage;
|
||||
});
|
||||
}
|
||||
const stage = stages[index++];
|
||||
if (!stage) return;
|
||||
const material = stage.material;
|
||||
if (version !== source.version) {
|
||||
material.copy(source);
|
||||
material.defines = { ...source.defines };
|
||||
material.onBeforeCompile = source.onBeforeCompile;
|
||||
material.customProgramCacheKey = source.customProgramCacheKey;
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
for (const key of stateKeys) material[key] = source[key];
|
||||
material.color = source.color;
|
||||
material.emissive = source.emissive;
|
||||
material.opacity = source.opacity;
|
||||
material.visible = source.visible;
|
||||
material.transparent = source.transparent;
|
||||
stage.geometry = this.geometry;
|
||||
stage.frustumCulled = this.frustumCulled;
|
||||
stage.layers.mask = this.layers.mask;
|
||||
});
|
||||
if (index === 0) return;
|
||||
if (index !== 3) return process.call(this, render);
|
||||
version = source.version;
|
||||
this.material = hidden;
|
||||
this.add(...stages);
|
||||
try {
|
||||
render();
|
||||
} finally {
|
||||
this.remove(...stages);
|
||||
this.material = source;
|
||||
}
|
||||
};
|
||||
return mesh;
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
AlwaysStencilFunc,
|
||||
BackSide,
|
||||
FrontSide,
|
||||
KeepStencilOp,
|
||||
IncrementWrapStencilOp,
|
||||
DecrementWrapStencilOp,
|
||||
NotEqualStencilFunc,
|
||||
ZeroStencilOp,
|
||||
BufferGeometry,
|
||||
Mesh,
|
||||
MeshLambertMaterial,
|
||||
Group,
|
||||
} from 'three';
|
||||
import { batchStencilDraws } from './navaraStencilBatch.js';
|
||||
|
||||
const keys = [
|
||||
'side',
|
||||
'colorWrite',
|
||||
'depthWrite',
|
||||
'depthTest',
|
||||
'stencilWrite',
|
||||
'stencilFunc',
|
||||
'stencilRef',
|
||||
'stencilFuncMask',
|
||||
'stencilWriteMask',
|
||||
'stencilFail',
|
||||
'stencilZFail',
|
||||
'stencilZPass',
|
||||
];
|
||||
const snapshot = (m) => Object.fromEntries(keys.map((k) => [k, m[k]]));
|
||||
class Drape extends Mesh {
|
||||
enabled() {
|
||||
return this.visible;
|
||||
}
|
||||
process(render) {
|
||||
if (!this.enabled()) return;
|
||||
const m = this.material,
|
||||
saved = snapshot(m);
|
||||
try {
|
||||
Object.assign(m, {
|
||||
side: BackSide,
|
||||
colorWrite: false,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
stencilWrite: true,
|
||||
stencilFunc: AlwaysStencilFunc,
|
||||
stencilFail: KeepStencilOp,
|
||||
stencilZFail: IncrementWrapStencilOp,
|
||||
stencilZPass: KeepStencilOp,
|
||||
});
|
||||
render();
|
||||
m.side = FrontSide;
|
||||
m.stencilZFail = DecrementWrapStencilOp;
|
||||
render();
|
||||
Object.assign(m, {
|
||||
side: BackSide,
|
||||
colorWrite: true,
|
||||
depthTest: false,
|
||||
stencilFunc: NotEqualStencilFunc,
|
||||
stencilFail: ZeroStencilOp,
|
||||
stencilZFail: ZeroStencilOp,
|
||||
stencilZPass: ZeroStencilOp,
|
||||
});
|
||||
render();
|
||||
} finally {
|
||||
Object.assign(m, saved);
|
||||
}
|
||||
}
|
||||
}
|
||||
function fixture() {
|
||||
const material = new MeshLambertMaterial({
|
||||
color: 0x74aaff,
|
||||
opacity: 0.85,
|
||||
transparent: true,
|
||||
});
|
||||
material.defines = { NVR_TEST: 1 };
|
||||
material.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.shared = { value: 123 };
|
||||
};
|
||||
material.customProgramCacheKey = () => 'native-drape';
|
||||
return new Drape(new BufferGeometry(), material);
|
||||
}
|
||||
|
||||
test('three native stencil states render in one submission with the same geometry, transforms and shader hooks', () => {
|
||||
const mesh = fixture(),
|
||||
material = mesh.material,
|
||||
expected = [];
|
||||
mesh.process(() => expected.push(snapshot(material)));
|
||||
mesh.position.set(100, 200, 300);
|
||||
mesh.layers.set(2);
|
||||
batchStencilDraws(mesh);
|
||||
let calls = 0;
|
||||
mesh.process(() => {
|
||||
calls++;
|
||||
assert.equal(mesh.material.visible, false);
|
||||
mesh.updateMatrixWorld(true);
|
||||
assert.deepEqual(
|
||||
mesh.children.map((m) => snapshot(m.material)),
|
||||
expected,
|
||||
);
|
||||
for (const [i, stage] of mesh.children.entries()) {
|
||||
assert.equal(stage.renderOrder, i);
|
||||
assert.equal(stage.geometry, mesh.geometry);
|
||||
assert.deepEqual(stage.matrixWorld.elements, mesh.matrixWorld.elements);
|
||||
assert.equal(stage.layers.mask, mesh.layers.mask);
|
||||
assert.equal(stage.material.onBeforeCompile, material.onBeforeCompile);
|
||||
assert.equal(
|
||||
stage.material.customProgramCacheKey(),
|
||||
material.customProgramCacheKey(),
|
||||
);
|
||||
assert.deepEqual(stage.material.defines, material.defines);
|
||||
}
|
||||
});
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(mesh.material, material);
|
||||
assert.equal(mesh.children.length, 0);
|
||||
mesh.geometry.dispose();
|
||||
material.dispose();
|
||||
});
|
||||
|
||||
test('stages track replaced batch geometry, shader versions and live colour/opacity without recompiling steady frames', () => {
|
||||
const mesh = batchStencilDraws(fixture()),
|
||||
source = mesh.material;
|
||||
let previous, versions;
|
||||
mesh.process(() => {
|
||||
previous = mesh.children.slice();
|
||||
versions = previous.map((s) => s.material.version);
|
||||
});
|
||||
const old = mesh.geometry;
|
||||
mesh.geometry = new BufferGeometry();
|
||||
old.dispose();
|
||||
source.color.setHex(0xffcc00);
|
||||
source.opacity = 0.22;
|
||||
source.stencilRef = 2;
|
||||
source.stencilFuncMask = 0x0f;
|
||||
source.stencilWriteMask = 0x0f;
|
||||
mesh.process(() => {
|
||||
assert.deepEqual(mesh.children, previous);
|
||||
assert.deepEqual(
|
||||
mesh.children.map((s) => s.material.version),
|
||||
versions,
|
||||
);
|
||||
for (const s of mesh.children) {
|
||||
assert.equal(s.geometry, mesh.geometry);
|
||||
assert.equal(s.material.color, source.color);
|
||||
assert.equal(s.material.opacity, 0.22);
|
||||
assert.equal(s.material.stencilRef, 2);
|
||||
assert.equal(s.material.stencilFuncMask, 0x0f);
|
||||
assert.equal(s.material.stencilWriteMask, 0x0f);
|
||||
}
|
||||
});
|
||||
source.defines.NVR_NORMAL = 1;
|
||||
source.needsUpdate = true;
|
||||
mesh.process(() => {
|
||||
for (const [i, s] of mesh.children.entries()) {
|
||||
assert.equal(s.material.defines.NVR_NORMAL, 1);
|
||||
assert.ok(s.material.version > versions[i]);
|
||||
}
|
||||
});
|
||||
mesh.geometry.dispose();
|
||||
source.dispose();
|
||||
});
|
||||
|
||||
test('render exceptions restore original ownership and stencil state; material disposal releases only owned resources', () => {
|
||||
const mesh = batchStencilDraws(fixture()),
|
||||
source = mesh.material,
|
||||
saved = snapshot(source);
|
||||
let disposed = 0,
|
||||
geometryDisposed = 0;
|
||||
mesh.geometry.addEventListener('dispose', () => geometryDisposed++);
|
||||
assert.throws(
|
||||
() =>
|
||||
mesh.process(() => {
|
||||
mesh.material.addEventListener('dispose', () => disposed++);
|
||||
for (const s of mesh.children)
|
||||
s.material.addEventListener('dispose', () => disposed++);
|
||||
throw new Error('draw failure');
|
||||
}),
|
||||
/draw failure/,
|
||||
);
|
||||
assert.equal(mesh.material, source);
|
||||
assert.equal(mesh.children.length, 0);
|
||||
assert.deepEqual(snapshot(source), saved);
|
||||
source.dispose();
|
||||
assert.equal(disposed, 4);
|
||||
assert.equal(geometryDisposed, 0);
|
||||
mesh.process(() => assert.fail('disposed owner must not allocate or draw'));
|
||||
source.dispose();
|
||||
assert.equal(disposed, 4);
|
||||
mesh.geometry.dispose();
|
||||
});
|
||||
|
||||
test('hidden meshes allocate nothing; meshes with children keep native behavior', () => {
|
||||
const mesh = batchStencilDraws(fixture());
|
||||
mesh.visible = false;
|
||||
mesh.process(() => assert.fail('hidden draw'));
|
||||
assert.equal(mesh.children.length, 0);
|
||||
mesh.visible = true;
|
||||
const child = new Group();
|
||||
mesh.add(child);
|
||||
let calls = 0;
|
||||
mesh.process(() => {
|
||||
calls++;
|
||||
assert.deepEqual(mesh.children, [child]);
|
||||
});
|
||||
assert.equal(calls, 3);
|
||||
mesh.material.dispose();
|
||||
mesh.geometry.dispose();
|
||||
});
|
||||
Reference in new issue
Block a user