Skip to content
Open
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
90 changes: 1 addition & 89 deletions apps/typegpu-docs/src/components/ExampleView.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import cs from 'classnames';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import {
type RefObject,
Suspense,
useEffect,
useLayoutEffect,
useRef,
useState,
lazy,
} from 'react';
import { Suspense, useEffect, useLayoutEffect, useRef, useState, lazy } from 'react';
import { currentSnackbarAtom } from '../utils/examples/currentSnackbarAtom.ts';
import { tsoverUsedAtom, exampleFullscreenAtom } from '../utils/examples/exampleViewStateAtoms.ts';
import { ExecutionCancelledError } from '../utils/examples/errors.ts';
Expand Down Expand Up @@ -131,7 +123,6 @@ export function ExampleView({ example, common }: Props) {
}, [fullscreen, setFullscreen]);

const isLoading = useExample(tsImport, setSnackbarText);
useResizableCanvas(exampleHtmlRef);

return (
<>
Expand Down Expand Up @@ -348,85 +339,6 @@ function GPUUnsupportedPanel() {
);
}

function useResizableCanvas(exampleHtmlRef: RefObject<HTMLDivElement | null>) {
useEffect(() => {
const canvases = exampleHtmlRef.current?.querySelectorAll('canvas') as
| HTMLCanvasElement[]
| undefined;
const observers: ResizeObserver[] = [];

for (const canvas of canvases ?? []) {
if ('width' in canvas.attributes || 'height' in canvas.attributes) {
continue;
}

const newCanvas = document.createElement('canvas');
const container = document.createElement('div');
const frame = document.createElement('div');

frame.appendChild(newCanvas);
container.appendChild(frame);

container.className = 'flex flex-1 justify-center items-center w-full h-full md:w-auto';
container.style.containerType = 'size';

frame.className = 'relative';

if (canvas.dataset.fitToContainer !== undefined) {
frame.style.width = '100%';
frame.style.height = '100%';
} else {
const aspectRatio = canvas.dataset.aspectRatio ?? '1';
frame.style.aspectRatio = aspectRatio;
frame.style.height = `min(100cqh, calc(100cqw/(${aspectRatio})))`;
}

for (const prop of canvas.style) {
// @ts-expect-error
newCanvas.style[prop] = canvas.style[prop];
}
for (const attribute of canvas.attributes) {
// @ts-expect-error
newCanvas[attribute.name] = attribute.value;
}
newCanvas.className = 'absolute w-full h-full';

canvas.parentElement?.replaceChild(container, canvas);

const onResize: ResizeObserverCallback = ([entry]) => {
if (!entry) {
return;
}

// Despite what the types say this property does not exist in Safari (hence the optional chaining).
const dpcb = entry.devicePixelContentBoxSize?.[0] as ResizeObserverSize | undefined;

const dpr = dpcb ? 1 : window.devicePixelRatio || 1;
const box =
dpcb ??
(Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize);

if (!box) {
return;
}

newCanvas.width = Math.round(box.inlineSize * dpr);
newCanvas.height = Math.round(box.blockSize * dpr);
};

const observer = new ResizeObserver(onResize);
observer.observe(newCanvas);
observers.push(observer);
}

return () => {
for (const observer of observers) {
observer.disconnect();
}
};
}, [exampleHtmlRef]);
}

/**
* NOTE: this function only filters common files used in src files.
* Common files used in other common files will not be included.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { TgpuRoot } from 'typegpu';
import { useConfigureContext, useRoot } from '@typegpu/react';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';

interface ExampleState {
onResize(): void;
onCleanup(): void;
}

Expand All @@ -12,8 +13,12 @@ interface HoverExampleLiveProps {

export default function HoverExampleLive({ setup }: HoverExampleLiveProps) {
const root = useRoot();
const exampleRef = useRef<ExampleState | null>(null);
const { ctxRef, ref: canvasRef } = useConfigureContext({
alphaMode: 'premultiplied',
onResize() {
exampleRef.current?.onResize();
},
});
const [isActive, setIsActive] = useState(false);
const [error, setError] = useState<unknown>(null);
Expand All @@ -40,13 +45,19 @@ export default function HoverExampleLive({ setup }: HoverExampleLiveProps) {

try {
const example = await setup(root, ctxRef.current);
onCleanup = () => example.onCleanup();

if (cancelled) {
onCleanup();
example.onCleanup();
return;
}

exampleRef.current = example;
onCleanup = () => {
exampleRef.current = null;
example.onCleanup();
};
// The initial resize notification can arrive before async setup finishes.
example.onResize();

setIsActive(true);
} catch (err) {
// Error boundaries can't catch throws from async effect callbacks, so
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ async function setup(root: TgpuRoot, context: GPUCanvasContext) {
const scene = await setupScene(root, context);
scene.qualityScale = await scene.computeOptimalQuality();

return { onCleanup: () => scene.onCleanup() };
return {
onResize: () => scene.onResize(),
onCleanup: () => scene.onCleanup(),
};
}

export default function JellySlider() {
Expand Down
46 changes: 0 additions & 46 deletions apps/typegpu-docs/src/components/stackblitz/stackBlitzIndex.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stackblitz examples are now 300x150

Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because common.attachAutoResizer isn't published yet, and Stackblitz uses the pinned published version of typegpu

Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,6 @@ body.style.margin = '0';
body.style.boxSizing = 'border-box';
body.style.padding = '1rem';

// Resize canvases
for (const canvas of document.querySelectorAll('canvas')) {
if ('width' in canvas.attributes || 'height' in canvas.attributes) {
continue; // custom canvas, not replacing with resizable
}

const container = document.createElement('div');
const frame = document.createElement('div');

canvas.parentElement?.replaceChild(container, canvas);

frame.appendChild(canvas);
container.appendChild(frame);

container.style.display = 'flex';
container.style.flex = '1';
container.style.justifyContent = 'center';
container.style.alignItems = 'top';
container.style.width = '100%';

container.style.containerType = 'size';

frame.style.position = 'relative';

if (canvas.dataset.fitToContainer !== undefined) {
frame.style.width = '100%';
frame.style.height = '100%';
} else {
const aspectRatio = canvas.dataset.aspectRatio ?? '1';
frame.style.aspectRatio = aspectRatio;
frame.style.height = `min(calc(min(100cqw, 100cqh)/(${aspectRatio})), min(100cqw, 100cqh))`;
}

canvas.style.position = 'absolute';
canvas.style.width = '100%';
canvas.style.height = '100%';

const onResize = () => {
canvas.width = frame.clientWidth * window.devicePixelRatio;
canvas.height = frame.clientHeight * window.devicePixelRatio;
};

onResize();
new ResizeObserver(onResize).observe(container);
}

const controlsPanel = document.createElement('div');
controlsPanel.style.display = 'grid';
controlsPanel.style.gridTemplateColumns = '1fr 1fr';
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<canvas data-fit-to-container></canvas>
<canvas class="w-full h-full"></canvas>
29 changes: 19 additions & 10 deletions apps/typegpu-docs/src/examples/advanced/appjs-venue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,6 @@ function createRenderTargets() {
.$usage('render');

return {
width: canvas.width,
height: canvas.height,
normal,
depth,
colorAttachments: [
Expand Down Expand Up @@ -452,14 +450,6 @@ function frame(timestamp: number) {
const dt = Math.min((timestamp - (lastTimestamp ?? timestamp)) * 0.001, 1 / 30);
lastTimestamp = timestamp;

if (canvas.width === 0 || canvas.height === 0) {
return;
}
if (canvas.width !== targets.width || canvas.height !== targets.height) {
targets.destroy();
targets = createRenderTargets();
}

controls.update(dt);
outsideOpacity.target = controls.focus.z > 0.5 ? 1 : 0;
outsideOpacity.step(dt);
Expand All @@ -469,6 +459,14 @@ function frame(timestamp: number) {
outsideOpacityUniform.write(std.saturate(outsideOpacity.value));
fadeInUniform.write(std.saturate(controls.in3dTransition * 2 - 1));

render();
}

function render() {
if (canvas.width === 0 || canvas.height === 0) {
return;
}

const encoder = root['~unstable'].createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: targets.colorAttachments,
Expand Down Expand Up @@ -506,8 +504,19 @@ function frame(timestamp: number) {

frameId = requestAnimationFrame(frame);

const autoResizer = common.attachAutoResizer({
root,
canvas,
onResize() {
targets.destroy();
targets = createRenderTargets();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new onResize recreates the targets on every observed size, but createRenderTargets() requests mipLevelCount: 2 for normal and depth. attachAutoResizer clamps the canvas to a minimum of 1x1, and a 1x1 texture supports only a single mip level, so collapsing the example (narrow panel, hidden preview) creates invalid textures and the example stops rendering. The previous per-frame path never hit this because it returned early when the canvas was 0-sized.

Technical details
# `mipLevelCount: 2` exceeds the mip limit for a clamped 1x1 canvas

## Affected sites
- `apps/typegpu-docs/src/examples/advanced/appjs-venue/index.ts:512` — newly calls `createRenderTargets()` from `onResize`.
- `apps/typegpu-docs/src/examples/advanced/appjs-venue/index.ts:392` — `normal` created with `mipLevelCount: 2`.
- `apps/typegpu-docs/src/examples/advanced/appjs-venue/index.ts:393` — `depth` created with `mipLevelCount: 2`.
- `packages/typegpu/src/common/attachAutoResizer.ts:84` — clamps the canvas to `Math.max(1, ...)`.

## Required outcome
- Recreating render targets when the canvas collapses to 1x1 must not request more mip levels than the texture size supports (`floor(log2(max(width, height))) + 1`).

## Suggested approach (optional)
- Derive the mip count from the target size, e.g. `Math.min(2, Math.floor(Math.log2(Math.max(width, height))) + 1)`, or skip target recreation while the reported size is below 2x2.

render();
},
});

export function onCleanup() {
abortController.abort();
cancelAnimationFrame(frameId);
autoResizer.detach();
root.destroy();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<canvas data-fit-to-container></canvas>
<canvas class="w-full h-full"></canvas>
<div class="stats"></div>
<style>
.stats {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,11 @@ applyGridSize(...GRID_SIZES[gridSizeKey]);
applyTrack(generateGridTrack(trackSeed, ...GRID_SIZES[gridSizeKey]));
startSimulation();

const autoResizer = common.attachAutoResizer({
root,
canvas,
});

// #region Example controls & Cleanup

export const controls = defineControls({
Expand Down Expand Up @@ -623,6 +628,7 @@ export const controls = defineControls({

export function onCleanup() {
cancelAnimationFrame(rafHandle);
autoResizer.detach();
root.destroy();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<canvas data-fit-to-container></canvas>
<canvas class="w-full h-full"></canvas>
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ let resolutionScale = 0.5;
let [width, height] = [0, 0];

function updateDimensions() {
[width, height] = [canvas.width, canvas.height].map((v) => Math.floor(v * resolutionScale));
[width, height] = [canvas.width, canvas.height].map((v) =>
Math.max(1, Math.floor(v * resolutionScale)),
);
}
updateDimensions();

Expand Down Expand Up @@ -342,8 +344,6 @@ function recreateResources() {
sourceIdx = 0;
}

clearCanvas();

function getTouchPosition(rect: DOMRect, touches: TouchList) {
if (touches.length === 2) {
return {
Expand All @@ -357,17 +357,17 @@ function getTouchPosition(rect: DOMRect, touches: TouchList) {
};
}

// #region Example controls & Cleanup

let resizeTimeout: ReturnType<typeof setTimeout>;
const resizeObserver = new ResizeObserver(() => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
const autoResizer = common.attachAutoResizer({
root,
canvas,
onResize() {
Comment thread
iwoplaza marked this conversation as resolved.
recreateResources();
clearCanvas();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
clearCanvas();
clearCanvas();
updateBrushSize();

}, 100);
updateBrushSize();
},
});
resizeObserver.observe(canvas);

// #region Example controls & Cleanup

const onMouseDown = (e: MouseEvent) => {
if (e.button !== 0 && e.button !== 2) {
Expand Down Expand Up @@ -475,8 +475,7 @@ export const controls = defineControls({
});

export function onCleanup() {
clearTimeout(resizeTimeout);
resizeObserver.disconnect();
autoResizer.detach();
root.destroy();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<canvas data-fit-to-container></canvas>
<canvas class="w-full h-full"></canvas>
Loading
Loading