Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/typegpu-docs/src/components/translator/lib/rolldown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { InputOptions, OutputOptions } from '@rolldown/browser';
import { join } from 'pathe';

export interface BundleResult {
output: Record<string, string | Uint8Array>;
output: Record<string, string | Uint8Array<ArrayBuffer>>;
warnings?: string[] | undefined;
}

Expand Down Expand Up @@ -88,7 +88,7 @@ export async function bundle(
result.output.map((chunk) =>
chunk.type === 'chunk'
? [chunk.fileName, chunk.code]
: [chunk.fileName, chunk.source]
: [chunk.fileName, chunk.source.slice()]
),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ function manhattanDistance(a: d.v3f, b: d.v3f) {
const dy = std.abs(a.y - b.y);
const dz = std.abs(a.z - b.z);

return std.max(dx, std.max(dy, dz));
return std.max(dx, dy, dz);
}
```

Expand Down
70 changes: 70 additions & 0 deletions apps/typegpu-docs/src/content/docs/fundamentals/utils.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,76 @@ tgpu.resolve([innerPipeline]);
```
:::

## *tgpu.comptime*

`tgpu['~unstable'].comptime(func)` creates a version of `func` that instead of being transpiled to WGSL, will be called during the WGSL code generation.
This can be used to precompute and inject a value into the final shader code.

```ts twoslash
import tgpu from 'typegpu';
import * as d from 'typegpu/data';

const hsvToRgb = tgpu.fn([d.f32, d.f32, d.f32], d.vec3f)``;

// ---cut---
const injectRand01 = tgpu['~unstable']
.comptime(() => Math.random());

const getColor = tgpu.fn([d.vec3f])((diffuse) => {
'use gpu';
const albedo = hsvToRgb(injectRand01(), 1, 0.5);
return albedo.mul(diffuse);
});
```

Note how the function passed into `comptime` doesn't have to be marked with
`'use gpu'` and can use `Math`. That's because the function doesn't execute on the GPU, it gets
executed before the shader code gets sent to the GPU.

## *tgpu.rawCodeSnippet*

When working on top of some existing shader code, sometimes you may know for certain that some variable will be already defined and should be accessible in the code.
In such scenario you can use `tgpu['~unstable'].rawCodeSnippet` -- an advanced API that creates a typed shader expression which can be injected into the final shader bundle upon use.

```ts twoslash
import tgpu from 'typegpu';
import * as d from 'typegpu/data';

// ---cut---
// `EXISTING_GLOBAL` is an identifier that we know will be in the
// final shader bundle, but we cannot
// refer to it in any other way.
const existingGlobal = tgpu['~unstable']
.rawCodeSnippet('EXISTING_GLOBAL', d.f32, 'constant');

const foo = tgpu.fn([], d.f32)(() => {
'use gpu';
return existingGlobal.$ * 22;
});

const wgsl = tgpu.resolve([foo]);
// fn foo() -> f32 {
// return (EXISTING_GLOBAL * 22f);
// }
```

:::note
There currently is no way to create a `rawCodeSnippet` that refers to an existing function.
:::

### Which origin to choose?

The optional third parameter `origin` lets TypeGPU transpiler know how to optimize the code snippet, as well as allows for some transpilation-time validity checks.

Usually `'runtime'` (the default) is a safe bet, but if you're sure that the expression or
computation is constant (either a reference to a constant, a numeric literal,
or an operation on constants), then pass `'constant'` as it might lead to better
optimizations.

If what the expression is a direct reference to an existing value (e.g. a uniform, a
storage binding, ...), then choose from `'uniform'`, `'mutable'`, `'readonly'`, `'workgroup'`,
`'private'` or `'handle'` depending on the address space of the referred value.

## *console.log*

Yes, you read that correctly, TypeGPU implements logging to the console on the GPU!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
<div class="example">
<canvas></canvas>
<div class="predictions-container">
<div class="predictions-label">Predictions (-.--ms)</div>

<div class="bars-container">
<div class="bar">O</div>
<div class="bar">1</div>
Expand Down Expand Up @@ -51,11 +49,6 @@
gap: 1rem;
}

.predictions-label {
margin-bottom: 0.5rem;
font-size: 1.25rem;
}

.bars-container {
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ export const controls = {
initial: Object.keys(testCases)[0],
options: Object.keys(testCases),
onSelectChange: async (selected: keyof typeof testCases) => {
// biome-ignore lint/performance/noDynamicNamespaceImportAccess: no other way
testCase = testCases[selected];
pipelines = createPipelines();
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ async function updateLUT(file: string) {

device.queue.writeTexture(
{ texture: root.unwrap(currentLUTTexture) },
parsed.data,
parsed.data.buffer,
{
bytesPerRow: parsed.size * 4 * 2,
rowsPerImage: parsed.size,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ const fragmentFunction = tgpu['~unstable'].fragmentFn({

let density = d.f32(0);
let invColor = d.vec3f(0, 0, 0);
let tMin = d.f32(0);
let intersectionFound = false;

for (let i = 0; i < X; i++) {
Expand Down Expand Up @@ -240,7 +239,6 @@ const fragmentFunction = tgpu['~unstable'].fragmentFn({
div(d.vec3f(1), boxMatrix.$[i][j][k].albedo),
),
);
tMin = intersection.tMin;
intersectionFound = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export async function loadCubemap(
chosenCubemap: CubemapNames,
) {
const images = await Promise.all(
getCubemapUrls(chosenCubemap).map(async (url, i) => {
getCubemapUrls(chosenCubemap).map(async (url) => {
const response = await fetch(url);
const blob = await response.blob();
return await createImageBitmap(blob);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,21 +352,14 @@ resizeObserver.observe(canvas);
let isDragging = false;
let prevX = 0;
let prevY = 0;
let lastPinchDist = 0;
let orbitRadius = std.length(cameraInitialPos);

// Yaw and pitch angles facing the origin.
let orbitYaw = Math.atan2(cameraInitialPos.x, cameraInitialPos.z);
let orbitPitch = Math.asin(cameraInitialPos.y / orbitRadius);

function updateCameraOrbit(dx: number, dy: number) {
const orbitSensitivity = 0.005;
orbitYaw += -dx * orbitSensitivity;
orbitPitch += dy * orbitSensitivity;
// Clamp pitch to avoid flipping
const maxPitch = Math.PI / 2 - 0.01;
if (orbitPitch > maxPitch) orbitPitch = maxPitch;
if (orbitPitch < -maxPitch) orbitPitch = -maxPitch;
// Convert spherical coordinates to cartesian coordinates
function updateCameraPosition() {
const newCamX = orbitRadius * Math.sin(orbitYaw) * Math.cos(orbitPitch);
const newCamY = orbitRadius * Math.sin(orbitPitch);
const newCamZ = orbitRadius * Math.cos(orbitYaw) * Math.cos(orbitPitch);
Expand All @@ -381,21 +374,24 @@ function updateCameraOrbit(dx: number, dy: number) {
cameraBuffer.writePartial({ view: newView, position: newCameraPos });
}

canvas.addEventListener('wheel', (event: WheelEvent) => {
event.preventDefault();
const zoomSensitivity = 0.05;
orbitRadius = std.clamp(orbitRadius + event.deltaY * zoomSensitivity, 3, 100);
const newCamX = orbitRadius * Math.sin(orbitYaw) * Math.cos(orbitPitch);
const newCamY = orbitRadius * Math.sin(orbitPitch);
const newCamZ = orbitRadius * Math.cos(orbitYaw) * Math.cos(orbitPitch);
const newCameraPos = d.vec4f(newCamX, newCamY, newCamZ, 1);
const newView = m.mat4.lookAt(
newCameraPos,
d.vec3f(0, 0, 0),
d.vec3f(0, 1, 0),
d.mat4x4f(),
function updateCameraOrbit(dx: number, dy: number) {
orbitYaw += -dx * 0.005;
orbitPitch = std.clamp(
orbitPitch + dy * 0.005,
-Math.PI / 2 + 0.01,
Math.PI / 2 - 0.01,
);
cameraBuffer.writePartial({ view: newView, position: newCameraPos });
updateCameraPosition();
}

function zoomCamera(delta: number) {
orbitRadius = std.clamp(orbitRadius + delta, 3, 100);
updateCameraPosition();
}

canvas.addEventListener('wheel', (e: WheelEvent) => {
e.preventDefault();
zoomCamera(e.deltaY * 0.05);
}, { passive: false });

canvas.addEventListener('mousedown', (event) => {
Expand All @@ -404,12 +400,17 @@ canvas.addEventListener('mousedown', (event) => {
prevY = event.clientY;
});

canvas.addEventListener('touchstart', (event) => {
event.preventDefault();
if (event.touches.length === 1) {
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (e.touches.length === 1) {
isDragging = true;
prevX = event.touches[0].clientX;
prevY = event.touches[0].clientY;
prevX = e.touches[0].clientX;
prevY = e.touches[0].clientY;
} else if (e.touches.length === 2) {
isDragging = false;
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
lastPinchDist = Math.sqrt(dx * dx + dy * dy);
}
}, { passive: false });

Expand All @@ -418,8 +419,14 @@ const mouseUpEventListener = () => {
};
window.addEventListener('mouseup', mouseUpEventListener);

const touchEndEventListener = () => {
isDragging = false;
const touchEndEventListener = (e: TouchEvent) => {
if (e.touches.length === 1) {
isDragging = true;
prevX = e.touches[0].clientX;
prevY = e.touches[0].clientY;
} else {
isDragging = false;
}
};
window.addEventListener('touchend', touchEndEventListener);

Expand All @@ -435,21 +442,31 @@ const mouseMoveEventListener = (event: MouseEvent) => {
};
window.addEventListener('mousemove', mouseMoveEventListener);

const touchMoveEventListener = (event: TouchEvent) => {
if (isDragging && event.touches.length === 1) {
event.preventDefault();
const dx = event.touches[0].clientX - prevX;
const dy = event.touches[0].clientY - prevY;
prevX = event.touches[0].clientX;
prevY = event.touches[0].clientY;

const touchMoveEventListener = (e: TouchEvent) => {
if (e.touches.length === 1 && isDragging) {
e.preventDefault();
const dx = e.touches[0].clientX - prevX;
const dy = e.touches[0].clientY - prevY;
prevX = e.touches[0].clientX;
prevY = e.touches[0].clientY;
updateCameraOrbit(dx, dy);
}
};
window.addEventListener('touchmove', touchMoveEventListener, {
passive: false,
});

canvas.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const pinchDist = Math.sqrt(dx * dx + dy * dy);
zoomCamera((lastPinchDist - pinchDist) * 0.05);
lastPinchDist = pinchDist;
}
}, { passive: false });

function hideHelp() {
const helpElem = document.getElementById('help');
if (helpElem) {
Expand Down
16 changes: 13 additions & 3 deletions apps/typegpu-docs/src/examples/rendering/disco/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import tgpu from 'typegpu';
import * as d from 'typegpu/data';
import { mainVertex } from './shaders/vertex.ts';
import { resolutionAccess, timeAccess } from './consts.ts';
import {
mainFragment,
mainFragment1,
mainFragment2,
mainFragment3,
mainFragment4,
mainFragment5,
mainFragment6,
mainFragment7,
} from './shaders/fragment.ts';
import { mainVertex } from './shaders/vertex.ts';

const canvas = document.querySelector('canvas') as HTMLCanvasElement;
const context = canvas.getContext('webgpu') as GPUCanvasContext;
Expand All @@ -32,7 +32,7 @@ const resolutionUniform = root.createUniform(
);

const fragmentShaders = [
mainFragment,
mainFragment1,
mainFragment2,
mainFragment3,
mainFragment4,
Expand Down Expand Up @@ -107,4 +107,14 @@ export const controls = {
}
},
},
'Test Resolution': import.meta.env.DEV && {
onButtonClick() {
const namespace = tgpu['~unstable'].namespace();
Array.from({ length: 6 }).map((_, i) =>
root.device.createShaderModule({
code: tgpu.resolve([pipelines[i + 1]], { names: namespace }),
})
);
},
},
};
Loading