@forgeax/engine-render-graph 0.0.0-dev.8d955ade1c79
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.
- package/LICENSE +202 -0
- package/README.md +184 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/observation.unit.test.d.ts +2 -0
- package/dist/__tests__/observation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph-alias-source.unit.test.d.ts +2 -0
- package/dist/__tests__/render-graph-alias-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph-builder.unit.test.d.ts +2 -0
- package/dist/__tests__/render-graph-builder.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph-errors.test-d.d.ts +2 -0
- package/dist/__tests__/render-graph-errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-graph-regressions.unit.test.d.ts +2 -0
- package/dist/__tests__/render-graph-regressions.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph-rhi-null.integration.test.d.ts +2 -0
- package/dist/__tests__/render-graph-rhi-null.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph.unit.test.d.ts +2 -0
- package/dist/__tests__/render-graph.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resource-declaration-collision.test.d.ts +2 -0
- package/dist/__tests__/resource-declaration-collision.test.d.ts.map +1 -0
- package/dist/builder.d.ts +42 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/compiled-graph.d.ts +23 -0
- package/dist/compiled-graph.d.ts.map +1 -0
- package/dist/errors.d.ts +148 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graph.d.ts +403 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +2256 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kernel-internal.d.ts +84 -0
- package/dist/kernel-internal.d.ts.map +1 -0
- package/dist/observation.d.ts +38 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/pass-registry.d.ts +12 -0
- package/dist/pass-registry.d.ts.map +1 -0
- package/dist/pipeline/__tests__/color-value-domain.test.d.ts +2 -0
- package/dist/pipeline/__tests__/color-value-domain.test.d.ts.map +1 -0
- package/dist/pipeline/color-value-domain.d.ts +35 -0
- package/dist/pipeline/color-value-domain.d.ts.map +1 -0
- package/dist/resource-registry.d.ts +62 -0
- package/dist/resource-registry.d.ts.map +1 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/__tests__/observation.unit.test.ts +103 -0
- package/src/__tests__/render-graph-alias-source.unit.test.ts +118 -0
- package/src/__tests__/render-graph-builder.unit.test.ts +673 -0
- package/src/__tests__/render-graph-errors.test-d.ts +207 -0
- package/src/__tests__/render-graph-regressions.unit.test.ts +126 -0
- package/src/__tests__/render-graph-rhi-null.integration.test.ts +86 -0
- package/src/__tests__/render-graph.unit.test.ts +2551 -0
- package/src/__tests__/resource-declaration-collision.test.ts +151 -0
- package/src/builder.ts +1009 -0
- package/src/compiled-graph.ts +383 -0
- package/src/errors.ts +205 -0
- package/src/graph.ts +1269 -0
- package/src/index.ts +97 -0
- package/src/kernel-internal.ts +148 -0
- package/src/observation.ts +134 -0
- package/src/pass-registry.ts +43 -0
- package/src/pipeline/__tests__/color-value-domain.test.ts +43 -0
- package/src/pipeline/color-value-domain.ts +139 -0
- package/src/resource-registry.ts +174 -0
- package/src/types.ts +216 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
type AliasSourceDetail,
|
|
4
|
+
type CapabilityDetail,
|
|
5
|
+
type CapMissingDetail,
|
|
6
|
+
type ColorDomainDetail,
|
|
7
|
+
type DanglingReadDetail,
|
|
8
|
+
type DeclarationDetail,
|
|
9
|
+
type DescriptorDetail,
|
|
10
|
+
type DuplicateResourceDetail,
|
|
11
|
+
type InvalidFormatDetail,
|
|
12
|
+
type LifecycleDetail,
|
|
13
|
+
type ObservationDetail,
|
|
14
|
+
type PassFailureDetail,
|
|
15
|
+
RenderGraphError,
|
|
16
|
+
type RenderGraphErrorCode,
|
|
17
|
+
type RenderGraphErrorDetail,
|
|
18
|
+
type ResourceAccessDetail,
|
|
19
|
+
type ResourceAllocFailedDetail,
|
|
20
|
+
} from '../errors.js';
|
|
21
|
+
import type { GraphPassKind, GraphResourceKind } from '../types.js';
|
|
22
|
+
|
|
23
|
+
describe('RenderGraphError code/detail correlation', () => {
|
|
24
|
+
it('accepts every existing code with its matching detail interface', () => {
|
|
25
|
+
new RenderGraphError({
|
|
26
|
+
code: 'dangling-read',
|
|
27
|
+
expected: '',
|
|
28
|
+
hint: '',
|
|
29
|
+
detail: { resourceKey: 'resource', passName: 'pass' } satisfies DanglingReadDetail,
|
|
30
|
+
});
|
|
31
|
+
new RenderGraphError({
|
|
32
|
+
code: 'cap-missing',
|
|
33
|
+
expected: '',
|
|
34
|
+
hint: '',
|
|
35
|
+
detail: { cap: 'compute', passName: 'pass' } satisfies CapMissingDetail,
|
|
36
|
+
});
|
|
37
|
+
new RenderGraphError({
|
|
38
|
+
code: 'duplicate-resource',
|
|
39
|
+
expected: '',
|
|
40
|
+
hint: '',
|
|
41
|
+
detail: { resourceKey: 'resource' } satisfies DuplicateResourceDetail,
|
|
42
|
+
});
|
|
43
|
+
new RenderGraphError({
|
|
44
|
+
code: 'alias-source-missing',
|
|
45
|
+
expected: '',
|
|
46
|
+
hint: '',
|
|
47
|
+
detail: { aliasKey: 'alias', sourceKey: 'source' } satisfies AliasSourceDetail,
|
|
48
|
+
});
|
|
49
|
+
new RenderGraphError({
|
|
50
|
+
code: 'unknown-resource',
|
|
51
|
+
expected: '',
|
|
52
|
+
hint: '',
|
|
53
|
+
detail: { resourceKey: 'resource', passName: 'pass' } satisfies DanglingReadDetail,
|
|
54
|
+
});
|
|
55
|
+
new RenderGraphError({
|
|
56
|
+
code: 'resource-alloc-failed',
|
|
57
|
+
expected: '',
|
|
58
|
+
hint: '',
|
|
59
|
+
detail: {
|
|
60
|
+
resourceKey: 'resource',
|
|
61
|
+
passName: 'pass',
|
|
62
|
+
rhiCode: 'lost',
|
|
63
|
+
} satisfies ResourceAllocFailedDetail,
|
|
64
|
+
});
|
|
65
|
+
new RenderGraphError({
|
|
66
|
+
code: 'invalid-format',
|
|
67
|
+
expected: '',
|
|
68
|
+
hint: '',
|
|
69
|
+
detail: {
|
|
70
|
+
resourceKey: 'resource',
|
|
71
|
+
format: 'rgba16float',
|
|
72
|
+
expected: ['rgba16float'],
|
|
73
|
+
} satisfies InvalidFormatDetail,
|
|
74
|
+
});
|
|
75
|
+
for (const code of [
|
|
76
|
+
'observation-absent',
|
|
77
|
+
'observation-invalid-format',
|
|
78
|
+
'observation-invalid-size',
|
|
79
|
+
'observation-missing-copy-src',
|
|
80
|
+
'observation-stale',
|
|
81
|
+
'observation-retired',
|
|
82
|
+
] as const) {
|
|
83
|
+
new RenderGraphError({
|
|
84
|
+
code,
|
|
85
|
+
expected: '',
|
|
86
|
+
hint: '',
|
|
87
|
+
detail: { frameId: 1, expected: 'frame' } satisfies ObservationDetail,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const code of [
|
|
91
|
+
'invalid-color-domain',
|
|
92
|
+
'missing-color-domain',
|
|
93
|
+
'color-domain-mismatch',
|
|
94
|
+
] as const) {
|
|
95
|
+
new RenderGraphError({
|
|
96
|
+
code,
|
|
97
|
+
expected: '',
|
|
98
|
+
hint: '',
|
|
99
|
+
detail: { value: 'linear' } satisfies ColorDomainDetail,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('keeps detail optional, including explicit undefined', () => {
|
|
105
|
+
new RenderGraphError({ code: 'dangling-read', expected: '', hint: '' });
|
|
106
|
+
new RenderGraphError({ code: 'invalid-format', expected: '', hint: '', detail: undefined });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('rejects a detail belonging to a different code', () => {
|
|
110
|
+
new RenderGraphError({
|
|
111
|
+
code: 'dangling-read',
|
|
112
|
+
expected: '',
|
|
113
|
+
hint: '',
|
|
114
|
+
// @ts-expect-error -- dangling-read accepts DanglingReadDetail, not CapMissingDetail.
|
|
115
|
+
detail: { cap: 'compute', passName: 'pass' },
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('preserves the closed code and detail-shape public unions', () => {
|
|
120
|
+
expectTypeOf<RenderGraphErrorCode>().toEqualTypeOf<
|
|
121
|
+
| 'dangling-read'
|
|
122
|
+
| 'cap-missing'
|
|
123
|
+
| 'duplicate-resource'
|
|
124
|
+
| 'alias-source-missing'
|
|
125
|
+
| 'unknown-resource'
|
|
126
|
+
| 'resource-alloc-failed'
|
|
127
|
+
| 'invalid-format'
|
|
128
|
+
| 'observation-absent'
|
|
129
|
+
| 'observation-invalid-format'
|
|
130
|
+
| 'observation-invalid-size'
|
|
131
|
+
| 'observation-missing-copy-src'
|
|
132
|
+
| 'observation-stale'
|
|
133
|
+
| 'observation-retired'
|
|
134
|
+
| 'invalid-color-domain'
|
|
135
|
+
| 'missing-color-domain'
|
|
136
|
+
| 'color-domain-mismatch'
|
|
137
|
+
| 'duplicate-pass-name'
|
|
138
|
+
| 'duplicate-resource-label'
|
|
139
|
+
| 'builder-sealed'
|
|
140
|
+
| 'foreign-resource-handle'
|
|
141
|
+
| 'resource-not-declared-by-pass'
|
|
142
|
+
| 'uninitialized-read'
|
|
143
|
+
| 'access-conflict'
|
|
144
|
+
| 'capability-missing'
|
|
145
|
+
| 'resource-descriptor-invalid'
|
|
146
|
+
| 'import-usage-mismatch'
|
|
147
|
+
| 'resource-allocation-failed'
|
|
148
|
+
| 'resource-resolution-failed'
|
|
149
|
+
| 'pass-encode-failed'
|
|
150
|
+
| 'compiled-graph-retired'
|
|
151
|
+
| 'resource-retire-failed'
|
|
152
|
+
>();
|
|
153
|
+
expectTypeOf<RenderGraphErrorDetail>().toEqualTypeOf<
|
|
154
|
+
| DanglingReadDetail
|
|
155
|
+
| CapMissingDetail
|
|
156
|
+
| DuplicateResourceDetail
|
|
157
|
+
| AliasSourceDetail
|
|
158
|
+
| ResourceAllocFailedDetail
|
|
159
|
+
| InvalidFormatDetail
|
|
160
|
+
| ObservationDetail
|
|
161
|
+
| ColorDomainDetail
|
|
162
|
+
| DeclarationDetail
|
|
163
|
+
| ResourceAccessDetail
|
|
164
|
+
| CapabilityDetail
|
|
165
|
+
| DescriptorDetail
|
|
166
|
+
| PassFailureDetail
|
|
167
|
+
| LifecycleDetail
|
|
168
|
+
>();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
function assertNever(value: never): never {
|
|
173
|
+
throw new Error(String(value));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function exhaustivePass(kind: GraphPassKind): string {
|
|
177
|
+
switch (kind) {
|
|
178
|
+
case 'raster':
|
|
179
|
+
return kind;
|
|
180
|
+
case 'compute':
|
|
181
|
+
return kind;
|
|
182
|
+
case 'copy':
|
|
183
|
+
return kind;
|
|
184
|
+
}
|
|
185
|
+
return assertNever(kind);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function exhaustiveResource(kind: GraphResourceKind): string {
|
|
189
|
+
switch (kind) {
|
|
190
|
+
case 'texture':
|
|
191
|
+
return kind;
|
|
192
|
+
case 'buffer':
|
|
193
|
+
return kind;
|
|
194
|
+
}
|
|
195
|
+
return assertNever(kind);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
describe('M4 render graph closed unions', () => {
|
|
199
|
+
it('keeps pass and resource variants exhaustively handled without a default branch', () => {
|
|
200
|
+
expectTypeOf(exhaustivePass).returns.toEqualTypeOf<string>();
|
|
201
|
+
expectTypeOf(exhaustiveResource).returns.toEqualTypeOf<string>();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('keeps RenderGraphErrorCode closed', () => {
|
|
205
|
+
expectTypeOf<RenderGraphErrorCode>().not.toEqualTypeOf<string>();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Buffer, RhiCommandEncoder } from '@forgeax/engine-rhi';
|
|
2
|
+
import { rhi } from '@forgeax/engine-rhi-null';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { RenderGraphBuilder } from '../builder.js';
|
|
5
|
+
import { RenderGraph } from '../graph.js';
|
|
6
|
+
|
|
7
|
+
interface Frame {
|
|
8
|
+
readonly encoder: RhiCommandEncoder;
|
|
9
|
+
readonly external: Buffer;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('RenderGraph fixed temporal regressions', () => {
|
|
13
|
+
it('accepts write-read-write without depending on a future writer', async () => {
|
|
14
|
+
const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
|
|
15
|
+
const graph = new RenderGraphBuilder<Frame>();
|
|
16
|
+
const x = graph.createBuffer('x', { size: 16 }).unwrap();
|
|
17
|
+
const y = graph.createBuffer('y', { size: 16 }).unwrap();
|
|
18
|
+
graph.addCopyPass('seed-x', {
|
|
19
|
+
accesses: [{ resource: x, usage: 'copy-dst' }],
|
|
20
|
+
encode: () => undefined,
|
|
21
|
+
});
|
|
22
|
+
graph.addCopyPass('consume-x', {
|
|
23
|
+
accesses: [
|
|
24
|
+
{ resource: x, usage: 'copy-src' },
|
|
25
|
+
{ resource: y, usage: 'copy-dst' },
|
|
26
|
+
],
|
|
27
|
+
encode: () => undefined,
|
|
28
|
+
});
|
|
29
|
+
graph.addCopyPass('rewrite-x', {
|
|
30
|
+
accesses: [
|
|
31
|
+
{ resource: y, usage: 'copy-src' },
|
|
32
|
+
{ resource: x, usage: 'copy-dst' },
|
|
33
|
+
],
|
|
34
|
+
encode: () => undefined,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = graph.compile({ device, surfaceSize: { width: 1, height: 1 } });
|
|
38
|
+
expect(result.ok).toBe(true);
|
|
39
|
+
if (result.ok) {
|
|
40
|
+
expect(result.value.inspect().passes.map((pass) => pass.name)).toEqual([
|
|
41
|
+
'seed-x',
|
|
42
|
+
'consume-x',
|
|
43
|
+
'rewrite-x',
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('accepts first read only when a resource is imported from its owner', async () => {
|
|
49
|
+
const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
|
|
50
|
+
const graph = new RenderGraphBuilder<Frame>();
|
|
51
|
+
const input = graph
|
|
52
|
+
.importBuffer('external-input', { size: 16, usage: 0x0040 }, (frame) => frame.external)
|
|
53
|
+
.unwrap();
|
|
54
|
+
graph.addComputePass('consume-external', {
|
|
55
|
+
accesses: [{ resource: input, usage: 'uniform-read' }],
|
|
56
|
+
encode: () => undefined,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(graph.compile({ device, surfaceSize: { width: 1, height: 1 } }).ok).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects a repeated pass label at declaration time', () => {
|
|
63
|
+
const graph = new RenderGraphBuilder<Frame>();
|
|
64
|
+
expect(graph.addCopyPass('duplicate-label', { accesses: [], encode: () => undefined }).ok).toBe(
|
|
65
|
+
true,
|
|
66
|
+
);
|
|
67
|
+
const duplicate = graph.addCopyPass('duplicate-label', {
|
|
68
|
+
accesses: [],
|
|
69
|
+
encode: () => undefined,
|
|
70
|
+
});
|
|
71
|
+
expect(duplicate.ok).toBe(false);
|
|
72
|
+
if (!duplicate.ok) expect(duplicate.error.code).toBe('duplicate-pass-name');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('owns compute pass begin and end for the current RenderGraph facade', async () => {
|
|
76
|
+
const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
|
|
77
|
+
const graph = new RenderGraph<Frame>();
|
|
78
|
+
graph.addResource('external', { kind: 'buffer', lifetime: 'persistent' });
|
|
79
|
+
graph.addComputePass('compute', {
|
|
80
|
+
reads: ['external'],
|
|
81
|
+
writes: [],
|
|
82
|
+
encode: ({ pass }) => pass.dispatchWorkgroups(1),
|
|
83
|
+
});
|
|
84
|
+
expect(graph.compile({ backendKind: 'null', caps: device.caps, device }).ok).toBe(true);
|
|
85
|
+
const encoder = device.createCommandEncoder({ label: 'facade-compute' }).unwrap();
|
|
86
|
+
graph.execute({ encoder, external: device.createBuffer({ size: 16, usage: 0x40 }).unwrap() });
|
|
87
|
+
expect(encoder.finish().ok).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('runs compute after-work only after ending the graph-owned pass', async () => {
|
|
91
|
+
const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
|
|
92
|
+
const graph = new RenderGraph<Frame>();
|
|
93
|
+
const events: string[] = [];
|
|
94
|
+
graph.addResource('external', { kind: 'buffer', lifetime: 'persistent' });
|
|
95
|
+
graph.addComputePass('timed-compute', {
|
|
96
|
+
reads: ['external'],
|
|
97
|
+
writes: [],
|
|
98
|
+
encode: ({ pass }) => {
|
|
99
|
+
events.push('encode');
|
|
100
|
+
pass.dispatchWorkgroups(1);
|
|
101
|
+
},
|
|
102
|
+
after: () => events.push('after'),
|
|
103
|
+
});
|
|
104
|
+
expect(graph.compile({ backendKind: 'null', caps: device.caps, device }).ok).toBe(true);
|
|
105
|
+
const rawEncoder = device.createCommandEncoder({ label: 'timed-compute' }).unwrap();
|
|
106
|
+
const encoder = {
|
|
107
|
+
beginComputePass: (...args: Parameters<RhiCommandEncoder['beginComputePass']>) => {
|
|
108
|
+
events.push('begin');
|
|
109
|
+
const pass = rawEncoder.beginComputePass(...args);
|
|
110
|
+
const end = pass.end.bind(pass);
|
|
111
|
+
pass.end = () => {
|
|
112
|
+
events.push('end');
|
|
113
|
+
end();
|
|
114
|
+
};
|
|
115
|
+
return pass;
|
|
116
|
+
},
|
|
117
|
+
} as RhiCommandEncoder;
|
|
118
|
+
graph.execute({
|
|
119
|
+
encoder,
|
|
120
|
+
external: device.createBuffer({ size: 16, usage: 0x40 }).unwrap(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(events).toEqual(['begin', 'encode', 'end', 'after']);
|
|
124
|
+
expect(rawEncoder.finish().ok).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { RhiCommandEncoder } from '@forgeax/engine-rhi';
|
|
2
|
+
import { type RhiNullDevice, rhi } from '@forgeax/engine-rhi-null';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { RenderGraphBuilder } from '../builder.js';
|
|
5
|
+
|
|
6
|
+
describe('RenderGraphBuilder RhiNull integration', () => {
|
|
7
|
+
it('records copy and compute in declaration order on one command encoder', async () => {
|
|
8
|
+
const adapter = (await rhi.requestAdapter()).unwrap();
|
|
9
|
+
const device = (await adapter.requestDevice()).unwrap();
|
|
10
|
+
const graph = new RenderGraphBuilder<{ readonly encoder: RhiCommandEncoder }>();
|
|
11
|
+
const data = graph.createBuffer('data', { size: 16 }).unwrap();
|
|
12
|
+
graph
|
|
13
|
+
.addCopyPass('seed', {
|
|
14
|
+
accesses: [{ resource: data, usage: 'copy-dst' }],
|
|
15
|
+
encode: ({ encoder, resources }) => {
|
|
16
|
+
encoder.clearBuffer(resources.buffer(data).unwrap());
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
.unwrap();
|
|
20
|
+
graph
|
|
21
|
+
.addComputePass('simulate', {
|
|
22
|
+
accesses: [{ resource: data, usage: 'storage-read-write' }],
|
|
23
|
+
encode: ({ pass, resources }) => {
|
|
24
|
+
expect(resources.buffer(data).ok).toBe(true);
|
|
25
|
+
pass.dispatchWorkgroups(1);
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
.unwrap();
|
|
29
|
+
|
|
30
|
+
const compiled = graph.compile({ device, surfaceSize: { width: 1, height: 1 } }).unwrap();
|
|
31
|
+
expect(compiled.inspect().passes).toMatchObject([
|
|
32
|
+
{ name: 'seed', kind: 'copy', dependencies: [] },
|
|
33
|
+
{ name: 'simulate', kind: 'compute', dependencies: ['seed'] },
|
|
34
|
+
]);
|
|
35
|
+
const encoder = device.createCommandEncoder({ label: 'graph-frame' }).unwrap();
|
|
36
|
+
expect(compiled.execute({ encoder }).ok).toBe(true);
|
|
37
|
+
expect(encoder.finish().ok).toBe(true);
|
|
38
|
+
expect((device as RhiNullDevice).framePassNames).toEqual(['simulate']);
|
|
39
|
+
expect((await compiled.retire()).ok).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('observes compute to indirect raster structure and command counts', async () => {
|
|
43
|
+
const adapter = (await rhi.requestAdapter()).unwrap();
|
|
44
|
+
const device = (await adapter.requestDevice()).unwrap();
|
|
45
|
+
const graph = new RenderGraphBuilder<{ readonly encoder: RhiCommandEncoder }>();
|
|
46
|
+
const args = graph.createBuffer('draw-args', { size: 16 }).unwrap();
|
|
47
|
+
const output = graph
|
|
48
|
+
.createTexture('output', { format: 'rgba8unorm', size: { width: 1, height: 1 } })
|
|
49
|
+
.unwrap();
|
|
50
|
+
const outputView = graph.view(output, { label: 'output.view' }).unwrap();
|
|
51
|
+
graph
|
|
52
|
+
.addComputePass('build-args', {
|
|
53
|
+
accesses: [{ resource: args, usage: 'storage-write' }],
|
|
54
|
+
encode: ({ pass }) => pass.dispatchWorkgroups(1),
|
|
55
|
+
})
|
|
56
|
+
.unwrap();
|
|
57
|
+
graph
|
|
58
|
+
.addRasterPass('consume-args', {
|
|
59
|
+
accesses: [
|
|
60
|
+
{ resource: args, usage: 'indirect-read' },
|
|
61
|
+
{ resource: outputView, usage: 'color-attachment' },
|
|
62
|
+
],
|
|
63
|
+
colorAttachments: [{ view: outputView, loadOp: 'clear', storeOp: 'store' }],
|
|
64
|
+
encode: ({ pass, resources }) => {
|
|
65
|
+
const layout = device.createBindGroupLayout({ entries: [] }).unwrap();
|
|
66
|
+
pass.setBindGroup(0, device.createBindGroup({ layout, entries: [] }).unwrap());
|
|
67
|
+
pass.drawIndirect(resources.buffer(args).unwrap(), 0);
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
.unwrap();
|
|
71
|
+
|
|
72
|
+
const compiled = graph.compile({ device, surfaceSize: { width: 1, height: 1 } }).unwrap();
|
|
73
|
+
expect(compiled.inspect().passes).toMatchObject([
|
|
74
|
+
{ name: 'build-args', kind: 'compute', dependencies: [] },
|
|
75
|
+
{ name: 'consume-args', kind: 'raster', dependencies: ['build-args'] },
|
|
76
|
+
]);
|
|
77
|
+
const encoder = device.createCommandEncoder({ label: 'mixed-frame' }).unwrap();
|
|
78
|
+
expect(compiled.execute({ encoder }).ok).toBe(true);
|
|
79
|
+
expect(encoder.finish().ok).toBe(true);
|
|
80
|
+
expect((device as RhiNullDevice).framePassNames).toEqual(['build-args', 'consume-args']);
|
|
81
|
+
expect((device as RhiNullDevice).totalDispatchCount).toBe(1);
|
|
82
|
+
expect((device as RhiNullDevice).totalDrawCount).toBe(1);
|
|
83
|
+
expect((device as RhiNullDevice).totalBindGroupCount).toBe(1);
|
|
84
|
+
expect((await compiled.retire()).ok).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|