@forgeax/engine-rhi-debug 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/replay/session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/replay/session.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-rhi-debug",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,19 +26,19 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@forgeax/engine-rhi": "0.1.33",
30
- "@forgeax/engine-types": "0.1.33",
29
+ "@forgeax/engine-rhi": "0.1.35",
30
+ "@forgeax/engine-types": "0.1.35",
31
31
  "@noble/hashes": "^2.2.0",
32
32
  "pako": "^1.0.11"
33
33
  },
34
34
  "devDependencies": {
35
- "@forgeax/engine-rhi-null": "0.1.33",
35
+ "@forgeax/engine-rhi-null": "0.1.35",
36
36
  "@types/node": "^20.14.0",
37
37
  "@webgpu/types": "^0.1.71"
38
38
  },
39
39
  "peerDependencies": {
40
- "@forgeax/engine-rhi-webgpu": "0.1.33",
41
- "@forgeax/engine-rhi-wgpu": "0.1.33"
40
+ "@forgeax/engine-rhi-webgpu": "0.1.35",
41
+ "@forgeax/engine-rhi-wgpu": "0.1.35"
42
42
  },
43
43
  "forgeax": {
44
44
  "metrics": {
@@ -142,6 +142,7 @@ async function captureBrowserTape(): Promise<CapturedBrowserTape> {
142
142
  'write source texture',
143
143
  );
144
144
  const encoder = must(device.createCommandEncoder({}), 'create command encoder');
145
+ encoder.pushDebugGroup('frame');
145
146
  const pass = encoder.beginRenderPass({
146
147
  colorAttachments: [
147
148
  {
@@ -154,8 +155,13 @@ async function captureBrowserTape(): Promise<CapturedBrowserTape> {
154
155
  } as never);
155
156
  pass.setPipeline(pipeline);
156
157
  pass.setBindGroup(0, bindGroup, []);
158
+ pass.pushDebugGroup('outer');
159
+ pass.pushDebugGroup('inner');
157
160
  pass.draw(3, 1, 0, 0);
161
+ pass.popDebugGroup();
162
+ pass.popDebugGroup();
158
163
  pass.end();
164
+ encoder.popDebugGroup();
159
165
  const command = must(encoder.finish(), 'finish command encoder');
160
166
  must(device.queue.submit([command]), 'submit command buffer');
161
167
  await device.queue.onSubmittedWorkDone();
@@ -193,7 +193,7 @@ describe('AC-08 partial: import.meta.hot in rhiDebugFlag guard', () => {
193
193
  });
194
194
 
195
195
  describe('RHI-debug smoke roster gate', () => {
196
- it('resolves every declared hello and learn-render smoke at 300 frames', () => {
196
+ it('resolves every declared hello and learn-render smoke at 60 frames', () => {
197
197
  const rosterPath = path.resolve(ENGINE_ROOT, 'scripts', 'rhi-debug-smoke-roster.mjs');
198
198
  const result = spawnSync(
199
199
  process.execPath,
@@ -204,7 +204,7 @@ describe('RHI-debug smoke roster gate', () => {
204
204
  '--learn-root',
205
205
  'apps/learn-render',
206
206
  '--frames',
207
- '300',
207
+ '60',
208
208
  ],
209
209
  { cwd: ENGINE_ROOT, encoding: 'utf8' },
210
210
  );
@@ -223,12 +223,12 @@ describe('RHI-debug smoke roster gate', () => {
223
223
  };
224
224
  expect(roster.status).toBe('unavailable');
225
225
  expect(roster.execution).toMatchObject({ status: 'not-executed', mode: 'declaration-only' });
226
- expect(roster.frameCount).toBe(300);
226
+ expect(roster.frameCount).toBe(60);
227
227
  expect(roster.entries.length).toBeGreaterThan(0);
228
228
  expect(
229
229
  roster.entries.every(
230
230
  (entry) =>
231
- entry.frames === 300 &&
231
+ entry.frames === 60 &&
232
232
  entry.command === entry.invocation &&
233
233
  entry.tokens.join(' ') === entry.invocation &&
234
234
  entry.tokens[0] === 'pnpm' &&
@@ -285,7 +285,7 @@ describe('RHI-debug smoke roster gate', () => {
285
285
  '--cwd',
286
286
  fixture,
287
287
  '--frames',
288
- '300',
288
+ '60',
289
289
  '--execute',
290
290
  ],
291
291
  { cwd: ENGINE_ROOT, encoding: 'utf8' },
@@ -320,7 +320,7 @@ describe('RHI-debug smoke roster gate', () => {
320
320
  invocation: 'pnpm --filter @fake/smoke smoke:browser',
321
321
  tokens: ['pnpm', '--filter', '@fake/smoke', 'smoke:browser'],
322
322
  backend: 'browser',
323
- frames: 300,
323
+ frames: 60,
324
324
  });
325
325
  expect(passedEntry?.stdout).toContain('fake browser smoke');
326
326
 
@@ -330,7 +330,11 @@ describe('RHI-debug smoke roster gate', () => {
330
330
  name: '@fake/smoke',
331
331
  private: true,
332
332
  scripts: {
333
- 'smoke:browser': 'node -e "console.error(\'fake failure\'); process.exit(7)"',
333
+ // process managers may route a failing script's streams differently
334
+ // on each runner; write the diagnostic synchronously to both pipes
335
+ // so the fail-closed assertion always has portable evidence.
336
+ 'smoke:browser':
337
+ "node -e \"const fs = require('node:fs'); fs.writeSync(1, 'fake failure\\n'); fs.writeSync(2, 'fake failure\\n'); process.exit(7)\"",
334
338
  },
335
339
  forgeax: { smokeInvocation: 'pnpm --filter @fake/smoke smoke:browser' },
336
340
  }),
@@ -353,12 +357,31 @@ describe('RHI-debug smoke roster gate', () => {
353
357
  const failedRoster = JSON.parse(failed.stdout) as {
354
358
  status: string;
355
359
  execution: { status: string; failedCount: number };
356
- entries: readonly { status: string; returnCode: number | null; stderr: string }[];
360
+ entries: readonly {
361
+ status: string;
362
+ returnCode: number | null;
363
+ stdout: string;
364
+ stderr: string;
365
+ }[];
357
366
  };
358
367
  expect(failedRoster.status).toBe('failed');
359
368
  expect(failedRoster.execution).toMatchObject({ status: 'failed', failedCount: 1 });
360
- expect(failedRoster.entries[0]).toMatchObject({ status: 'failed', returnCode: 7 });
361
- expect(failedRoster.entries[0]?.stderr).toContain('fake failure');
369
+ // The roster records the declared pnpm command's status. pnpm 11 maps
370
+ // the inner script's exit 7 to 1; compare against that real boundary.
371
+ const directFailure = spawnSync('pnpm', ['--filter', '@fake/smoke', 'smoke:browser'], {
372
+ cwd: fixture,
373
+ encoding: 'utf8',
374
+ });
375
+ expect(directFailure.error).toBeUndefined();
376
+ expect(directFailure.status).not.toBeNull();
377
+ expect(directFailure.status).not.toBe(0);
378
+ expect(failedRoster.entries[0]).toMatchObject({
379
+ status: 'failed',
380
+ returnCode: directFailure.status,
381
+ });
382
+ expect(`${failedRoster.entries[0]?.stdout}\n${failedRoster.entries[0]?.stderr}`).toContain(
383
+ 'fake failure',
384
+ );
362
385
  } finally {
363
386
  rmSync(fixture, { recursive: true, force: true });
364
387
  }
@@ -103,6 +103,7 @@ async function makeRecording(pack: DawnPack): Promise<{
103
103
  const encoderResult = device.createCommandEncoder({});
104
104
  expect(encoderResult.ok).toBe(true);
105
105
  if (!encoderResult.ok) throw new Error(encoderResult.error.hint);
106
+ encoderResult.value.pushDebugGroup('frame');
106
107
  const pass = encoderResult.value.beginRenderPass({
107
108
  colorAttachments: [
108
109
  {
@@ -114,8 +115,13 @@ async function makeRecording(pack: DawnPack): Promise<{
114
115
  ],
115
116
  } as never);
116
117
  pass.setPipeline(pipeline.value);
118
+ pass.pushDebugGroup('outer');
119
+ pass.pushDebugGroup('inner');
117
120
  pass.draw(3, 1, 0, 0);
121
+ pass.popDebugGroup();
122
+ pass.popDebugGroup();
118
123
  pass.end();
124
+ encoderResult.value.popDebugGroup();
119
125
  const command = encoderResult.value.finish();
120
126
  expect(command.ok).toBe(true);
121
127
  if (!command.ok) throw new Error(command.error.hint);
@@ -127,7 +133,7 @@ async function makeRecording(pack: DawnPack): Promise<{
127
133
  }
128
134
 
129
135
  describe.skipIf(SKIP_DAWN)('RHI debug v7 fresh-device evidence', () => {
130
- it('records, encodes, strictly decodes, and replays pixels on a fresh Dawn device', async () => {
136
+ it('records, encodes, strictly decodes, and replays pixels through nested debug groups on a fresh Dawn device', async () => {
131
137
  const pack = await loadDawn();
132
138
  const recording = await makeRecording(pack);
133
139
  const baseline = await readbackTexturePixels(
@@ -594,6 +594,13 @@ async function finalizeWorkPass(
594
594
  finishEventIndex ?? context.tape.events.length,
595
595
  );
596
596
  if (!closure.ok) return closure;
597
+ const groupsClosed = closeReplayDebugGroups(
598
+ context,
599
+ begin.cmdHandleId,
600
+ work.eventIndex,
601
+ encoder.resource.value as RhiCommandEncoder,
602
+ );
603
+ if (!groupsClosed.ok) return groupsClosed;
597
604
  const finished = (encoder.resource.value as RhiCommandEncoder).finish();
598
605
  const finishEvent = context.tape.events[finishEventIndex ?? work.eventIndex] ?? begin;
599
606
  if (!finished.ok)
@@ -656,6 +663,15 @@ function closeReplayPass(
656
663
  );
657
664
  }
658
665
  }
666
+ if (entry.resource.role === 'render-pass') {
667
+ const groupsClosed = closeReplayDebugGroups(
668
+ context,
669
+ begin.passHandleId,
670
+ selectedEventIndex,
671
+ entry.resource.value as RhiRenderPassEncoder,
672
+ );
673
+ if (!groupsClosed.ok) return groupsClosed;
674
+ }
659
675
  (entry.resource.value as RhiRenderPassEncoder | RhiComputePassEncoder).end();
660
676
  } catch (cause) {
661
677
  return eventFailure(pass.endEventIndex ?? selectedEventIndex, begin, 'encode', cause);
@@ -664,6 +680,42 @@ function closeReplayPass(
664
680
  return ok(undefined);
665
681
  }
666
682
 
683
+ // Inspection stops at the selected work, before the recorded scope exits.
684
+ // Derive only the still-open scopes from that prefix; never execute later work.
685
+ function closeReplayDebugGroups(
686
+ context: ReplayExecutionContext,
687
+ handleId: string,
688
+ throughEventIndex: number,
689
+ encoder: RhiCommandEncoder | RhiRenderPassEncoder,
690
+ ): Result<void, RhiDebugError> {
691
+ const open: { event: RhiCallEvent; eventIndex: number }[] = [];
692
+ for (let eventIndex = 0; eventIndex <= throughEventIndex; eventIndex++) {
693
+ const event = context.tape.events[eventIndex];
694
+ if (event === undefined) continue;
695
+ if (
696
+ (event.kind === 'pushDebugGroup' && event.cmdHandleId === handleId) ||
697
+ (event.kind === 'passPushDebugGroup' && event.passHandleId === handleId)
698
+ ) {
699
+ open.push({ event, eventIndex });
700
+ } else if (
701
+ (event.kind === 'popDebugGroup' && event.cmdHandleId === handleId) ||
702
+ (event.kind === 'passPopDebugGroup' && event.passHandleId === handleId)
703
+ ) {
704
+ open.pop();
705
+ }
706
+ }
707
+ for (let index = open.length - 1; index >= 0; index--) {
708
+ const scope = open[index];
709
+ if (scope === undefined) continue;
710
+ try {
711
+ encoder.popDebugGroup();
712
+ } catch (cause) {
713
+ return eventFailure(scope.eventIndex, scope.event, 'encode', cause);
714
+ }
715
+ }
716
+ return ok(undefined);
717
+ }
718
+
667
719
  async function replayCommandClosure(
668
720
  context: ReplayExecutionContext,
669
721
  cmdHandleId: string,