Skip to content

Commit 1be642c

Browse files
feat: console.log on the GPU (#1657)
1 parent 7f9b025 commit 1be642c

File tree

26 files changed

+1821
-54
lines changed

26 files changed

+1821
-54
lines changed

apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Every `root.create*` function creates a typed resource.
4343

4444
| Function | Description |
4545
| --- | --- |
46-
| <div className="w-max">`root.createBuffer`</div> | Creates a typed buffer with a given data-type and, optionally, an initial value. More information in [the next chapter](/TypeGPU/fundamentals/buffers). |
46+
| <div className="w-max">`root.createBuffer`</div> | Creates a typed buffer with a given data-type and, optionally, an initial value. More information in [the Buffers chapter](/TypeGPU/fundamentals/buffers). |
4747

4848
## Unwrapping resources
4949

apps/typegpu-docs/src/content/docs/fundamentals/tgsl.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ Keep in mind that you cannot execute entry-point functions in JavaScript.
115115

116116
* **TGSL limitations** --
117117
For a function to be valid TGSL, it must consist only of supported JS syntax (again, see [tinyest-for-wgsl repository](https://github.com/software-mansion/TypeGPU/blob/release/packages/tinyest-for-wgsl/src/parsers.ts)), possibly including references to bound buffers, constant variables defined outside the function, other TGSL functions etc.
118-
This means that, for example, `console.log()` calls will not work on the GPU.
118+
This means that, for example, `Math.sqrt(n)` calls will not work on the GPU.
119+
One exception to this is `console.log()`, about which you can read more [here](/TypeGPU/fundamentals/utils/#consolelog).
119120

120121
* **Differences between JS on the CPU and GPU** --
121122
TGSL is developed to work on the GPU the same as on the CPU as much as possible,

apps/typegpu-docs/src/content/docs/fundamentals/utils.mdx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,85 @@ The default workgroup sizes are:
109109

110110
The callback is not called if the global invocation id of a thread would exceed the size in any dimension.
111111
:::
112+
113+
## *console.log*
114+
115+
Yes, you read that correctly, TypeGPU implements logging to the console on the GPU!
116+
Just call `console.log` like you would in plain JavaScript, and open the console to see the results.
117+
118+
```ts twoslash
119+
import tgpu, { prepareDispatch } from 'typegpu';
120+
import * as d from 'typegpu/data';
121+
122+
const root = await tgpu.init();
123+
// ---cut---
124+
const callCountMutable = root.createMutable(d.u32, 0);
125+
const dispatch = prepareDispatch(root, () => {
126+
'kernel';
127+
callCountMutable.$ += 1;
128+
console.log('Call number', callCountMutable.$);
129+
});
130+
131+
dispatch();
132+
dispatch();
133+
134+
// Eventually...
135+
// "[GPU] Call number 1"
136+
// "[GPU] Call number 2"
137+
```
138+
139+
Under the hood, TypeGPU translates `console.log` to a series of serializing functions that write the logged arguments to a buffer that is read and deserialized after every draw/dispatch call.
140+
141+
The buffer is of fixed size, which may limit the total amount of information that can be logged; if the buffer overflows, additional logs are dropped.
142+
If that's an issue, you may specify the size manually when creating the `root` object.
143+
144+
```ts twoslash
145+
import tgpu, { prepareDispatch } from 'typegpu';
146+
import * as d from 'typegpu/data';
147+
148+
const presentationFormat = undefined as any;
149+
const canvas = undefined as any;
150+
const context = canvas.getContext('webgpu') as any;
151+
// ---cut---
152+
const root = await tgpu.init({
153+
unstable_logOptions: {
154+
logCountLimit: 32,
155+
logSizeLimit: 8, // in bytes, enough to fit 2*u32
156+
},
157+
});
158+
159+
/* vertex shader */
160+
161+
const mainFragment = tgpu['~unstable'].fragmentFn({
162+
in: { pos: d.builtin.position },
163+
out: d.vec4f,
164+
})(({ pos }) => {
165+
// this log fits in 8 bytes
166+
// static strings do not count towards the serialized log size
167+
console.log('X:', d.u32(pos.x), 'Y:', d.u32(pos.y));
168+
return d.vec4f(0, 1, 1, 1);
169+
});
170+
171+
/* pipeline creation and draw call */
172+
```
173+
174+
:::note
175+
The logs are written to console only after the dispatch finishes and the buffer is read.
176+
This may happen with a noticeable delay.
177+
:::
178+
179+
:::caution
180+
When using `console.log`, atomic operations are injected into the WGSL code to safely synchronize logging from multiple threads.
181+
This synchronization can introduce overhead and significantly impact shader performance.
182+
:::
183+
184+
There are some limitations (some of which we intend to alleviate in the future):
185+
186+
- `console.log` only works when used in TGSL, when calling or resolving a TypeGPU pipeline.
187+
Otherwise, for example when using `tgpu.resolve` on a WGSL template, logs are ignored.
188+
- `console.log` only works in fragment and compute shaders.
189+
This is due to [WebGPU limitation](https://www.w3.org/TR/WGSL/#address-space) that does not allow modifying buffers during the vertex shader stage.
190+
- TypeGPU needs to handle every logged data type individually.
191+
Currently, the only supported types are `bool`, `u32`, `vec2u`, `vec3u` and `vec4u`.
192+
- `console.log` currently does not support template literals and string substitutions.
193+
- Other `console` methods like `clear` or `warn` are not yet supported.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Press buttons to run the tests! Check logs for results.
2+
3+
<canvas width="16" height="16"></canvas>
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import tgpu, { prepareDispatch } from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
4+
const root = await tgpu.init({
5+
unstable_logOptions: {
6+
logCountLimit: 32,
7+
logSizeLimit: 32,
8+
},
9+
});
10+
11+
// #region Example controls and cleanup
12+
13+
export const controls = {
14+
'One argument': {
15+
onButtonClick: () =>
16+
prepareDispatch(root, () => {
17+
'kernel';
18+
console.log(d.u32(321));
19+
})(),
20+
},
21+
'Multiple arguments': {
22+
onButtonClick: () =>
23+
prepareDispatch(root, () => {
24+
'kernel';
25+
console.log(d.u32(1), d.vec3u(2, 3, 4), d.u32(5), d.u32(6));
26+
})(),
27+
},
28+
'String literals': {
29+
onButtonClick: () =>
30+
prepareDispatch(root, () => {
31+
'kernel';
32+
console.log(d.u32(2), 'plus', d.u32(3), 'equals', d.u32(5));
33+
})(),
34+
},
35+
'Different types': {
36+
onButtonClick: () =>
37+
prepareDispatch(root, () => {
38+
'kernel';
39+
console.log(d.bool(true));
40+
console.log(d.u32(3_000_000_000));
41+
console.log(d.vec2u(1, 2));
42+
console.log(d.vec3u(1, 2, 3));
43+
console.log(d.vec4u(1, 2, 3, 4));
44+
})(),
45+
},
46+
'Two logs': {
47+
onButtonClick: () =>
48+
prepareDispatch(root, () => {
49+
'kernel';
50+
console.log('First log.');
51+
console.log('Second log.');
52+
})(),
53+
},
54+
'Two threads': {
55+
onButtonClick: () =>
56+
prepareDispatch(root, (x) => {
57+
'kernel';
58+
console.log('Log from thread', x);
59+
})(2),
60+
},
61+
'100 dispatches': {
62+
onButtonClick: async () => {
63+
const indexUniform = root.createUniform(d.u32);
64+
const dispatch = prepareDispatch(root, () => {
65+
'kernel';
66+
console.log('Log from dispatch', indexUniform.$);
67+
});
68+
for (let i = 0; i < 100; i++) {
69+
indexUniform.write(i);
70+
dispatch();
71+
console.log(`dispatched ${i}`);
72+
}
73+
},
74+
},
75+
'Varying size logs': {
76+
onButtonClick: async () => {
77+
const logCountUniform = root.createUniform(d.u32);
78+
const dispatch = prepareDispatch(root, () => {
79+
'kernel';
80+
for (let i = d.u32(); i < logCountUniform.$; i++) {
81+
console.log('Log index', d.u32(i) + 1, 'out of', logCountUniform.$);
82+
}
83+
});
84+
logCountUniform.write(3);
85+
dispatch();
86+
logCountUniform.write(1);
87+
dispatch();
88+
},
89+
},
90+
'Render pipeline': {
91+
onButtonClick: () => {
92+
const mainVertex = tgpu['~unstable'].vertexFn({
93+
in: { vertexIndex: d.builtin.vertexIndex },
94+
out: { pos: d.builtin.position },
95+
})((input) => {
96+
const positions = [
97+
d.vec2f(0, 0.5),
98+
d.vec2f(-0.5, -0.5),
99+
d.vec2f(0.5, -0.5),
100+
];
101+
102+
return { pos: d.vec4f(positions[input.vertexIndex], 0, 1) };
103+
});
104+
105+
const mainFragment = tgpu['~unstable'].fragmentFn({
106+
in: { pos: d.builtin.position },
107+
out: d.vec4f,
108+
})(({ pos }) => {
109+
console.log('X:', d.u32(pos.x), 'Y:', d.u32(pos.y));
110+
return d.vec4f(0.769, 0.392, 1.0, 1);
111+
});
112+
113+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
114+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
115+
const context = canvas.getContext('webgpu') as GPUCanvasContext;
116+
117+
context.configure({
118+
device: root.device,
119+
format: presentationFormat,
120+
alphaMode: 'premultiplied',
121+
});
122+
123+
const pipeline = root['~unstable']
124+
.withVertex(mainVertex, {})
125+
.withFragment(mainFragment, { format: presentationFormat })
126+
.createPipeline();
127+
128+
pipeline
129+
.withColorAttachment({
130+
view: context.getCurrentTexture().createView(),
131+
clearValue: [0, 0, 0, 0],
132+
loadOp: 'clear',
133+
storeOp: 'store',
134+
})
135+
.draw(3);
136+
},
137+
},
138+
'Too many logs': {
139+
onButtonClick: () =>
140+
prepareDispatch(root, (x) => {
141+
'kernel';
142+
console.log('Log 1 from thread', x);
143+
console.log('Log 2 from thread', x);
144+
console.log('Log 3 from thread', x);
145+
})(16),
146+
},
147+
'Too much data': {
148+
onButtonClick: () => {
149+
const dispatch = prepareDispatch(root, () => {
150+
'kernel';
151+
console.log(d.vec3u(), d.vec3u(), d.vec3u());
152+
});
153+
try {
154+
dispatch();
155+
} catch (err) {
156+
console.log(err);
157+
}
158+
},
159+
},
160+
};
161+
162+
export function onCleanup() {
163+
root.destroy();
164+
}
165+
166+
// #endregion
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"title": "Console Log Test",
3+
"category": "tests",
4+
"tags": ["experimental"],
5+
"dev": true
6+
}

packages/typegpu/src/core/buffer/buffer.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BufferReader, BufferWriter, getSystemEndianness } from 'typed-binary';
22
import { getCompiledWriterForSchema } from '../../data/compiledIO.ts';
33
import { readData, writeData } from '../../data/dataIO.ts';
4+
import type { AnyData } from '../../data/dataTypes.ts';
45
import { getWriteInstructions } from '../../data/partialIO.ts';
56
import { sizeOf } from '../../data/sizeOf.ts';
67
import type { BaseData } from '../../data/wgslTypes.ts';
@@ -33,7 +34,6 @@ import {
3334
type TgpuBufferUniform,
3435
type TgpuFixedBufferUsage,
3536
} from './bufferUsage.ts';
36-
import type { AnyData } from '../../data/dataTypes.ts';
3737

3838
// ----------
3939
// Public API
@@ -127,6 +127,7 @@ export interface TgpuBuffer<TData extends BaseData> extends TgpuNamable {
127127
compileWriter(): void;
128128
write(data: Infer<TData>): void;
129129
writePartial(data: InferPartial<TData>): void;
130+
clear(): void;
130131
copyFrom(srcBuffer: TgpuBuffer<MemIdentity<TData>>): void;
131132
read(): Promise<Infer<TData>>;
132133
destroy(): void;
@@ -362,6 +363,23 @@ class TgpuBufferImpl<TData extends AnyData> implements TgpuBuffer<TData> {
362363
}
363364
}
364365

366+
public clear(): void {
367+
const gpuBuffer = this.buffer;
368+
const device = this._group.device;
369+
370+
if (gpuBuffer.mapState === 'mapped') {
371+
new Uint8Array(gpuBuffer.getMappedRange()).fill(0);
372+
return;
373+
}
374+
375+
// Flushing any commands yet to be encoded.
376+
this._group.flush();
377+
378+
const encoder = device.createCommandEncoder();
379+
encoder.clearBuffer(gpuBuffer);
380+
device.queue.submit([encoder.finish()]);
381+
}
382+
365383
copyFrom(srcBuffer: TgpuBuffer<MemIdentity<TData>>): void {
366384
if (this.buffer.mapState === 'mapped') {
367385
throw new Error('Cannot copy to a mapped buffer.');

0 commit comments

Comments
 (0)