Skip to content

Commit db81b12

Browse files
committed
feat(example): the outpost pays for its own shader before anyone sees it
C2. Scope held: `gym`, `arena`, and C1's cell geometry, radii and criteria are untouched. One existing visible thing in the cell — the beacon — gains the project's only `.esmaterial`. That is what makes the attribution structural rather than argued. Home held zero material-owned programs before this and holds zero now, so a program the material path owns cannot have been built by anything the player has already walked past. A rare stock flag would have had to be argued about; this cannot. `render.mesh.materialPrograms` is the instrument: cumulative, read-only, and emitted from a collect that was going to run anyway. Nothing here asks for readiness in order to observe it — an observer that did would be the one paying the compile, and the frame it asked about would be clean because of the asking. The packaged game cannot reach the prewarm probe at all: that ABI is absent from a release build, so this is a property of the binary and not of discipline. Three criteria, and the first is a guard rather than a claim: it fails the day the home half gains a material, which is the day the other two stop being about the outpost. Then the outpost is prepared with nothing of it in the world and the program is ALREADY built; then it is resident and visible and the count has not moved. A cumulative counter that reads 1 before publication and 1 after is a frame delta of zero without sampling every frame to say so. Four sabotages, all pre-registered, all fire where they should. Removing the material: both claims fail on nothing unique having been exercised. Putting the same material at home: only the guard fires, and the other two stay green — which is precisely why it is there. Disconnecting readiness while still letting publication proceed: preparation buys nothing, and the count goes 0 before the boundary to 1 after it. That last one is also the control this needed, because it shows the counter CAN see a build that happens late. `shader-readiness` is certified now, with `runBy` and a release criterion of its own name. One command answers two questions; a claim nobody names is one nobody notices going missing. 46/47, and only `settings` is left.
1 parent b5b444f commit db81b12

7 files changed

Lines changed: 92 additions & 7 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "1.0",
3+
"type": "material",
4+
"shader": "builtin:model",
5+
"blendMode": 9,
6+
"depthTest": true,
7+
"depthWrite": true,
8+
"cull": 1,
9+
"properties": {
10+
"u_emissive": {
11+
"r": 0.95,
12+
"g": 0.78,
13+
"b": 0.22,
14+
"a": 1
15+
},
16+
"u_metallic": 0.15,
17+
"u_roughness": 0.35
18+
}
19+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"uuid": "91765692-eb29-4bd0-88d8-f87969ab2b0d",
3+
"version": "2.0",
4+
"type": "material",
5+
"importer": {}
6+
}

examples/third-person-3d/assets/scenes/main.esscene

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5375,7 +5375,8 @@
53755375
"g": 0.85,
53765376
"b": 0.4,
53775377
"a": 1
5378-
}
5378+
},
5379+
"material": "assets/materials/beacon.esmaterial"
53795380
}
53805381
},
53815382
{

src/esengine/renderer/plugins/MeshPlugin.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,12 @@ void MeshPlugin::collect(RenderCollectContext& collect_ctx) {
703703
ES_PROFILE_COUNTER("render.mesh.lodGathers", lodGathers);
704704
ES_PROFILE_COUNTER("render.mesh.programAsks", programAsks);
705705
ES_PROFILE_COUNTER("render.mesh.programCompiles", compiled_this_frame_);
706+
// Material-owned programs built so far, CUMULATIVE. Read-only: an observer
707+
// that had to ask for readiness to see it would be the one paying, and the
708+
// frame it asked about would be clean because of the asking.
709+
if (ctx.materials != nullptr) {
710+
ES_PROFILE_COUNTER("render.mesh.materialPrograms", ctx.materials->builtVariantCount());
711+
}
706712
if (compiled_this_frame_ > 0) {
707713
ES_PROFILE_COUNTER("render.mesh.compiledKey", last_compiled_variant_);
708714
}

tools/goldenProjects.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,6 @@ export const EVIDENCE = {
275275
* coverage — the same bargain check-project-settings strikes.
276276
*/
277277
export const KNOWN_GAPS = {
278-
// A work item, not a dispensation: the staleness check below forces the entry
279-
// out the moment a project certifies it.
280-
'shader-readiness': 'no golden project watches the frame something first appears on; needs a runBy a release criterion schedules',
281278
// Present in the engine and shown by non-golden samples, but never carried
282279
// through the chain by a project the release argues from.
283280
settings: 'Celestial Heights persists language, effects and key bindings and reads them back at boot; volume waits on the game having sound',
@@ -380,7 +377,11 @@ export const GOLDEN = [
380377
{
381378
id: 'third-person-3d',
382379
certifies: ['third-person', 'level-of-detail', 'ssao', 'navigation-3d',
383-
'root-motion', 'animation-events', 'world-streaming'],
380+
'root-motion', 'animation-events', 'world-streaming', 'shader-readiness'],
381+
// A packaged frame looks the same whether first sight of the outpost cost a
382+
// compile or not; the run is what reads the counter on both sides of the
383+
// boundary. Scheduled by `a-streamed-place-is-ready-before-it-is-seen`.
384+
runBy: 'node tools/verify-third-person.mjs',
384385
targets: ['web'],
385386
tier: 'pr',
386387
// The character walks on the key it declares. What it DOES on the way is

tools/releaseGate.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ export const CRITERIA = [
6767
answeredBy: 'node tools/verify-third-person.mjs',
6868
needs: ['tools/verify-third-person.mjs', 'examples/third-person-3d/project.esproject'],
6969
},
70+
{
71+
id: 'a-streamed-place-is-ready-before-it-is-seen',
72+
says: 'a streamed place pays its own shader requirements while it is being prepared,'
73+
+ ' not on the frame the player first sees it',
74+
// The same command as above, deliberately under its own name: deleting the
75+
// shader half would leave the character half green, and the registry unable
76+
// to say what this command still owes.
77+
answeredBy: 'node tools/verify-third-person.mjs',
78+
needs: ['tools/verify-third-person.mjs',
79+
'examples/third-person-3d/assets/materials/beacon.esmaterial'],
80+
},
7081
{
7182
id: '3d-runs-where-it-ships',
7283
host: 'android',

tools/verify-third-person.mjs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,18 +496,19 @@ const START = { y: 60, z: 120 };
496496
// on is persistent, and one outpost past the causeway is the only thing cut into
497497
// a cell. The claim is gameplay only a resident cell makes possible.
498498
function excursion({ out, frames, swings = 0, back = 0 }) {
499-
const holds = [{ key: 'KeyS', from: 0, to: out }];
499+
const holds = out > 0 ? [{ key: 'KeyS', from: 0, to: out }] : [];
500500
for (let i = 0; i < swings; i++) holds.push({ key: 'KeyJ', from: 225 + i * 20, to: 228 + i * 20 });
501501
if (back > 0) holds.push({ key: 'KeyW', from: out + 10, to: out + 10 + back });
502502
const r = runElectron([
503503
LAUNCHER, '--dir', dir, '--w', String(W), '--h', String(H),
504504
'--settle', '30', '--timeout', '120000', '--scene', 'main', '--streaming',
505505
'--gameplay', 'Player,Camera', '--combat', 'Player:Sentry,Beacon,Player',
506-
'--ai', 'Sentry', '--input', JSON.stringify({ holds, frames }),
506+
'--ai', 'Sentry', '--render', '--input', JSON.stringify({ holds, frames }),
507507
], { encoding: 'utf8', cwd: ROOT });
508508
return {
509509
at: reading(r.stdout, 'gameplay'),
510510
world: reading(r.stdout, 'streaming'),
511+
counters: reading(r.stdout, 'render') ?? {},
511512
combat: reading(r.stdout, 'combat') ?? { targets: {} },
512513
ai: reading(r.stdout, 'ai') ?? { found: false },
513514
};
@@ -565,6 +566,46 @@ const HOME_AGENTS = 1;
565566
+ ` ${seen.combat?.targets?.Sentry === undefined ? 'gone' : 'still there'}`);
566567
}
567568

569+
// The outpost's beacon carries the project's ONLY material, so a program that
570+
// path owns cannot have been built by anything at home. `materialPrograms` counts
571+
// them cumulatively, and reading it asks for nothing.
572+
const materials = (seen) => seen.counters?.['render.mesh.materialPrograms'] ?? -1;
573+
574+
{
575+
// The guard, not the claim: it fails the day the home half gains a material,
576+
// which is the day everything below stops being about the outpost.
577+
const home = excursion({ out: 0, frames: 40 });
578+
check('nothing at home needs the material the outpost brings',
579+
materials(home) === 0 && (home.world?.residentCells?.length ?? 9) === 0,
580+
`${materials(home)} material program(s) with the outpost unpublished`);
581+
}
582+
583+
{
584+
// Prepared, and nothing of it in the world yet — and the program is built.
585+
// This is where the compile is meant to be paid, and it is the reading that
586+
// says it was.
587+
const near = excursion({ out: 130, frames: 170 });
588+
check('the outpost pays for its own shader while it is still only prepared',
589+
near.world?.preparedCells?.includes('main.cell_0_2') === true
590+
&& (near.world?.residentCells?.length ?? 9) === 0 && materials(near) === 1,
591+
`prepared ${JSON.stringify(near.world?.preparedCells)}, resident`
592+
+ ` ${JSON.stringify(near.world?.residentCells)}, ${materials(near)} material program(s)`);
593+
}
594+
595+
{
596+
// And seeing it adds nothing. The counter is cumulative, so one that is
597+
// already 1 before publication and 1 after is a frame delta of zero without
598+
// sampling every frame to say so.
599+
const there = excursion({ out: 300, frames: 400 });
600+
check('first sight of the outpost pays no shader compile',
601+
there.world?.residentCells?.includes('main.cell_0_2') === true
602+
&& materials(there) === 1
603+
&& (there.counters?.['render.mesh.programCompiles'] ?? -1) === 0,
604+
`resident ${JSON.stringify(there.world?.residentCells)},`
605+
+ ` ${materials(there)} material program(s) — the same one preparation built —`
606+
+ ` and ${there.counters?.['render.mesh.programCompiles']} stock compile(s) on the frame`);
607+
}
608+
568609
const failed = results.filter((r) => !r.ok);
569610
console.log(`\nverify-third-person: ${results.length - failed.length}/${results.length}`
570611
+ ' behaviour(s) hold up in the packaged game.');

0 commit comments

Comments
 (0)