mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
perf(navara): skip redundant empty-scene resolves
Preserve pending clears, backgrounds, callbacks, shadows and real geometry. Verify exact pixels and the underground depth-clear path; add controlled original-render comparison modes.
This commit is contained in:
1 parent
15f1f9f5b6
commit
dc7a7c4d53
5 files changed
+203
-11
No files matched your search
@@ -1272,6 +1272,7 @@
|
||||
"src/ui/imageryBoxNavara.test.mjs",
|
||||
"scripts/qa-w2-imagery.mjs",
|
||||
"src/globe/navara/postSamplingPass.test.mjs",
|
||||
"src/globe/navara/emptySceneRender.test.mjs",
|
||||
"scripts/qa-post-sampling.mjs",
|
||||
"scripts/qa-installation-stencil.mjs",
|
||||
"scripts/qa-installation-stencil-entry.mjs",
|
||||
|
||||
@@ -2671,6 +2671,7 @@
|
||||
"src/globe/navara/overlayPass.js",
|
||||
"src/globe/navara/postSampling.js",
|
||||
"src/globe/navara/postSamplingPass.js",
|
||||
"src/globe/navara/emptySceneRender.js",
|
||||
"src/globe/navara/movers/modelMover.js",
|
||||
"src/globe/navara/movers/modelTint.js",
|
||||
"src/globe/navara/movers/index.js",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Scene } from 'three';
|
||||
|
||||
export function hasGeometry(scene) {
|
||||
let found = false;
|
||||
scene.traverseVisible((object) => {
|
||||
if (object.isMesh || object.isLine || object.isPoints || object.isSprite)
|
||||
found = true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
// Navara renders its MRT and forward scenes even when they contain only lights.
|
||||
// Three resolves the current MSAA target on every render(), including no draws.
|
||||
// Scope the override to this pass; retain clears, backgrounds and callbacks.
|
||||
export function skipEmptySceneRenders(render, scenes) {
|
||||
return function (renderer, ...args) {
|
||||
const original = renderer.render;
|
||||
const clear = renderer.clear;
|
||||
const pendingClears = new Set();
|
||||
if (clear)
|
||||
renderer.clear = function (...args) {
|
||||
pendingClears.add(this.getRenderTarget());
|
||||
return clear.apply(this, args);
|
||||
};
|
||||
renderer.render = function (scene, ...args) {
|
||||
const shadow = this.shadowMap;
|
||||
const target = this.getRenderTarget?.();
|
||||
if (
|
||||
(scene === scenes.mrt || scene === scenes.opaque) &&
|
||||
!this.autoClear &&
|
||||
!pendingClears.has(target) &&
|
||||
!(shadow?.enabled && (shadow.autoUpdate || shadow.needsUpdate)) &&
|
||||
scene.background === null &&
|
||||
scene.onBeforeRender === Scene.prototype.onBeforeRender &&
|
||||
scene.onAfterRender === Scene.prototype.onAfterRender &&
|
||||
!hasGeometry(scene)
|
||||
)
|
||||
return;
|
||||
const result = original.call(this, scene, ...args);
|
||||
pendingClears.delete(target);
|
||||
return result;
|
||||
};
|
||||
try {
|
||||
return render.call(this, renderer, ...args);
|
||||
} finally {
|
||||
renderer.render = original;
|
||||
if (clear) renderer.clear = clear;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Color, DirectionalLight, Group, Mesh, Scene } from 'three';
|
||||
import { skipEmptySceneRenders } from './emptySceneRender.js';
|
||||
|
||||
function fixture() {
|
||||
const scenes = { mrt: new Scene(), opaque: new Scene() };
|
||||
scenes.mrt.add(new DirectionalLight());
|
||||
const calls = [];
|
||||
const renderer = {
|
||||
autoClear: false,
|
||||
shadowMap: { enabled: true, autoUpdate: false, needsUpdate: false },
|
||||
render(scene, camera) {
|
||||
calls.push({ scene, camera, receiver: this });
|
||||
},
|
||||
};
|
||||
const original = renderer.render;
|
||||
return { scenes, renderer, calls, original };
|
||||
}
|
||||
|
||||
test('empty MRT/forward scenes skip resolves; real geometry and other scenes retain their ordered draws', () => {
|
||||
const f = fixture(),
|
||||
camera = {},
|
||||
other = new Scene(),
|
||||
receiver = {};
|
||||
const mesh = new Mesh();
|
||||
f.scenes.opaque.add(mesh);
|
||||
const run = skipEmptySceneRenders(function (renderer, arg) {
|
||||
assert.equal(this, receiver);
|
||||
assert.equal(arg, 42);
|
||||
renderer.render(f.scenes.mrt, camera);
|
||||
renderer.render(f.scenes.opaque, camera);
|
||||
renderer.render(other, camera);
|
||||
return 'result';
|
||||
}, f.scenes);
|
||||
assert.equal(run.call(receiver, f.renderer, 42), 'result');
|
||||
assert.deepEqual(
|
||||
f.calls.map((c) => c.scene),
|
||||
[f.scenes.opaque, other],
|
||||
);
|
||||
assert.ok(
|
||||
f.calls.every((c) => c.camera === camera && c.receiver === f.renderer),
|
||||
);
|
||||
assert.equal(f.renderer.render, f.original);
|
||||
});
|
||||
|
||||
test('visibility changes are evaluated on each render without stale geometry caching', () => {
|
||||
const f = fixture(),
|
||||
group = new Group();
|
||||
group.add(new Mesh());
|
||||
group.visible = false;
|
||||
f.scenes.mrt.add(group);
|
||||
const run = skipEmptySceneRenders(
|
||||
(renderer) => renderer.render(f.scenes.mrt),
|
||||
f.scenes,
|
||||
);
|
||||
run(f.renderer);
|
||||
assert.equal(f.calls.length, 0);
|
||||
group.visible = true;
|
||||
run(f.renderer);
|
||||
assert.equal(f.calls.length, 1);
|
||||
group.removeFromParent();
|
||||
run(f.renderer);
|
||||
assert.equal(f.calls.length, 1);
|
||||
});
|
||||
|
||||
test('clears, backgrounds, callbacks and pending shadow work keep the original render', () => {
|
||||
for (const configure of [
|
||||
(f) => {
|
||||
f.renderer.autoClear = true;
|
||||
},
|
||||
(f) => {
|
||||
f.scenes.mrt.background = new Color('red');
|
||||
},
|
||||
(f) => {
|
||||
f.scenes.mrt.onBeforeRender = () => {};
|
||||
},
|
||||
(f) => {
|
||||
f.scenes.mrt.onAfterRender = () => {};
|
||||
},
|
||||
(f) => {
|
||||
f.renderer.shadowMap.autoUpdate = true;
|
||||
},
|
||||
(f) => {
|
||||
f.renderer.shadowMap.needsUpdate = true;
|
||||
},
|
||||
]) {
|
||||
const f = fixture();
|
||||
configure(f);
|
||||
skipEmptySceneRenders(
|
||||
(renderer) => renderer.render(f.scenes.mrt),
|
||||
f.scenes,
|
||||
)(f.renderer);
|
||||
assert.equal(f.calls.length, 1);
|
||||
assert.equal(f.renderer.render, f.original);
|
||||
}
|
||||
});
|
||||
|
||||
test('nested passes and exceptions restore the exact borrowed renderer method', () => {
|
||||
const f = fixture();
|
||||
const inner = skipEmptySceneRenders(() => {
|
||||
throw new Error('render failure');
|
||||
}, f.scenes);
|
||||
const outer = skipEmptySceneRenders((renderer) => {
|
||||
const wrapped = renderer.render;
|
||||
assert.throws(() => inner(renderer), /render failure/);
|
||||
assert.equal(renderer.render, wrapped);
|
||||
throw new Error('outer failure');
|
||||
}, f.scenes);
|
||||
assert.throws(() => outer(f.renderer), /outer failure/);
|
||||
assert.equal(f.renderer.render, f.original);
|
||||
f.renderer.render(f.scenes.mrt);
|
||||
assert.equal(f.calls.length, 1);
|
||||
});
|
||||
|
||||
test('manual clears must resolve on their own target before empty renders may be skipped', () => {
|
||||
const f = fixture(),
|
||||
first = {},
|
||||
second = {},
|
||||
targets = [];
|
||||
let target = first;
|
||||
const clear = () => targets.push(target);
|
||||
f.renderer.clear = clear;
|
||||
f.renderer.getRenderTarget = () => target;
|
||||
const run = skipEmptySceneRenders((renderer) => {
|
||||
renderer.clear(false, true, false);
|
||||
target = second;
|
||||
renderer.render(f.scenes.mrt);
|
||||
assert.equal(f.calls.length, 0);
|
||||
target = first;
|
||||
renderer.render(f.scenes.mrt);
|
||||
assert.equal(f.calls.length, 1, 'depth clear still needs its resolve');
|
||||
renderer.render(f.scenes.mrt);
|
||||
assert.equal(f.calls.length, 1, 'resolved empty target needs no repeat');
|
||||
renderer.clear(false, false, true);
|
||||
renderer.render(f.scenes.opaque);
|
||||
assert.equal(f.calls.length, 2, 'stencil clear also needs its resolve');
|
||||
throw new Error('after clear');
|
||||
}, f.scenes);
|
||||
assert.throws(() => run(f.renderer), /after clear/);
|
||||
assert.deepEqual(targets, [first, first]);
|
||||
assert.equal(f.renderer.clear, clear);
|
||||
assert.equal(f.renderer.render, f.original);
|
||||
});
|
||||
@@ -1,14 +1,6 @@
|
||||
import { Pass } from 'postprocessing';
|
||||
import { AlwaysDepth, Mesh, PlaneGeometry, ShaderMaterial } from 'three';
|
||||
|
||||
function hasGeometry(scene) {
|
||||
let found = false;
|
||||
scene.traverseVisible((object) => {
|
||||
if (object.isMesh || object.isLine || object.isPoints || object.isSprite)
|
||||
found = true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
import { hasGeometry, skipEmptySceneRenders } from './emptySceneRender.js';
|
||||
|
||||
// Navara 0.2.1 multisamples every composer target, including full-screen
|
||||
// effects. Resolve geometry once; post effects need one sample per pixel.
|
||||
@@ -50,13 +42,17 @@ export class GeometrySamplingPass extends Pass {
|
||||
const pass = this.ctx.getPass(key);
|
||||
if (!pass || this.wrapped.has(pass)) continue;
|
||||
const original = pass.render;
|
||||
const draw =
|
||||
key === 'mrt'
|
||||
? skipEmptySceneRenders(original, this.ctx.scenes)
|
||||
: original;
|
||||
const owner = this;
|
||||
pass.render = function (renderer, input, output, ...rest) {
|
||||
if (key === 'gevOverlay' && !this.overlay.hasDrawables()) return;
|
||||
if (key === 'transparent' && !hasGeometry(owner.ctx.scenes.transparent))
|
||||
return;
|
||||
if (key === 'mrt' && !hasGeometry(owner.ctx.scenes.opaque))
|
||||
return original.call(this, renderer, input, output, ...rest);
|
||||
return draw.call(this, renderer, input, output, ...rest);
|
||||
const target = (owner.target ??= input.clone());
|
||||
owner.setSamples(target, owner.samples);
|
||||
target.setSize(input.width, input.height);
|
||||
@@ -70,7 +66,7 @@ export class GeometrySamplingPass extends Pass {
|
||||
// resolve. The opaque background sorts before transparent glyphs.
|
||||
this.scene.add(owner.background);
|
||||
}
|
||||
original.call(this, renderer, target, output, ...rest);
|
||||
draw.call(this, renderer, target, output, ...rest);
|
||||
renderer.initRenderTarget(input);
|
||||
renderer.copyTextureToTexture(target.texture, input.texture);
|
||||
if (key === 'mrt')
|
||||
|
||||
Reference in new issue
Block a user