From 9812aa1b2caa04821da3d46055293178661cd8aa Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Wed, 12 Aug 2026 15:12:58 +0200 Subject: [PATCH 1/2] work --- .../src/content/docs/apis/textures.mdx | 28 ++++++---- .../algorithms/genetic-racing/index.ts | 2 +- .../examples/simulation/gravity/helpers.ts | 4 +- .../src/examples/tests/texture-test/index.ts | 4 +- packages/typegpu/src/core/texture/texture.ts | 38 ++++++++++--- packages/typegpu/src/indexNamedExports.ts | 7 ++- packages/typegpu/tests/texture.test.ts | 55 ++++++++++++++++--- 7 files changed, 106 insertions(+), 32 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/apis/textures.mdx b/apps/typegpu-docs/src/content/docs/apis/textures.mdx index e7116bf0fc..f3a7e5e3a5 100644 --- a/apps/typegpu-docs/src/content/docs/apis/textures.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/textures.mdx @@ -24,14 +24,14 @@ const root = await tgpu.init(); const texture = root.createTexture({ size: [256, 256], format: 'rgba8unorm' as const, -}).$usage('sampled'); +}).$usage('sampled', 'render'); const response = await fetch('path/to/image.png'); const blob = await response.blob(); const image = await createImageBitmap(blob); -// Uploading image data to the texture (will be resampled if sizes differ) -texture.write(image); +// Uploading image data to the texture (explicitly resampling if sizes differ) +texture.write(image, { fit: 'stretch' }); // Creating a view to use in shader const sampledView = texture.createView(); @@ -119,12 +119,14 @@ The `.write()` method provides multiple overloads for different data sources: ```ts // Image sources (single or array) -write(source: ExternalImageSource | ExternalImageSource[]): void +write(source: ExternalImageSource | ExternalImageSource[], options?: TextureWriteOptions): void // Raw binary data with optional mip level write(source: ArrayBuffer | TypedArray | DataView, mipLevel?: number): void ``` +Writing image sources requires the `'render'` usage flag. Raw binary writes do not. + ### Writing image data You can write various image sources to textures. `ExternalImageSource` includes: @@ -143,24 +145,26 @@ const root = await tgpu.init(); const texture = root.createTexture({ size: [256, 256], format: 'rgba8unorm', -}).$usage('sampled'); +}).$usage('sampled', 'render'); // From an ImageBitmap const response = await fetch('path/to/image.png'); const blob = await response.blob(); const imageBitmap = await createImageBitmap(blob); -texture.write(imageBitmap); +texture.write(imageBitmap, { fit: 'stretch' }); // From an HTMLCanvasElement const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // ... draw on canvas -texture.write(canvas); +texture.write(canvas, { fit: 'stretch' }); ``` -:::tip -If image dimensions don't match the texture size, the image will be automatically resampled to fit (requires 'render' usage). -::: +If image dimensions don't match the texture size, a plain `.write()` call throws. Pass `{ fit: 'stretch' }` to explicitly resample the image to fit (requires `'render'` usage): + +```ts +texture.write(imageBitmap, { fit: 'stretch' }); +``` ### Writing arrays of images @@ -177,9 +181,9 @@ const texture3d = root.createTexture({ size: [256, 256, 3], format: 'rgba8unorm', dimension: '3d', -}).$usage('sampled'); +}).$usage('sampled', 'render'); -// Write array of images for each layer +// Write an image to each layer (each image must match the layer size) texture3d.write([imageBitmap1, imageBitmap2, imageBitmap3]); ``` diff --git a/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.ts b/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.ts index 737848fce0..c4c1857180 100644 --- a/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.ts @@ -68,7 +68,7 @@ const carSpriteTexture = root format: 'rgba8unorm', }) .$usage('sampled', 'render'); -carSpriteTexture.write(carBitmap); +carSpriteTexture.write(carBitmap, { fit: 'stretch' }); const carSpriteView = carSpriteTexture.createView(); const linearSampler = root.createSampler({ diff --git a/apps/typegpu-docs/src/examples/simulation/gravity/helpers.ts b/apps/typegpu-docs/src/examples/simulation/gravity/helpers.ts index ae2a20fd12..30ec69ba78 100644 --- a/apps/typegpu-docs/src/examples/simulation/gravity/helpers.ts +++ b/apps/typegpu-docs/src/examples/simulation/gravity/helpers.ts @@ -85,7 +85,7 @@ export async function loadSkyBox(root: TgpuRoot) { }), ); - texture.write(bitmaps); + texture.write(bitmaps, { fit: 'stretch' }); return texture; } @@ -144,7 +144,7 @@ export async function loadSphereTextures(root: TgpuRoot) { return await createImageBitmap(blob); }), ); - texture.write(planets); + texture.write(planets, { fit: 'stretch' }); return texture; } diff --git a/apps/typegpu-docs/src/examples/tests/texture-test/index.ts b/apps/typegpu-docs/src/examples/tests/texture-test/index.ts index 9de885a762..b0aee7e728 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-test/index.ts +++ b/apps/typegpu-docs/src/examples/tests/texture-test/index.ts @@ -114,7 +114,7 @@ function createPipelineForFormat(format: TestFormat) { } let texture = createTestTexture(currentFormat, currentSize); -texture.write(imageBitmap); +texture.write(imageBitmap, { fit: 'stretch' }); texture.generateMipmaps(); let { layout, pipeline } = createPipelineForFormat(currentFormat); @@ -122,7 +122,7 @@ let bindGroup = root.createBindGroup(layout, { myTexture: texture }); function recreateTexture() { texture = createTestTexture(currentFormat, currentSize); - texture.write(imageBitmap); + texture.write(imageBitmap, { fit: 'stretch' }); texture.generateMipmaps(); ({ layout, pipeline } = createPipelineForFormat(currentFormat)); bindGroup = root.createBindGroup(layout, { myTexture: texture }); diff --git a/packages/typegpu/src/core/texture/texture.ts b/packages/typegpu/src/core/texture/texture.ts index e3124e2fa8..5931a986a5 100644 --- a/packages/typegpu/src/core/texture/texture.ts +++ b/packages/typegpu/src/core/texture/texture.ts @@ -94,6 +94,18 @@ export type ExternalImageSource = | OffscreenCanvas | VideoFrame; +export type TextureWriteFit = 'stretch'; + +export type TextureWriteOptions = { + /** + * How to handle a source whose dimensions do not match the texture. + * + * By default, mismatched writes throw. Use `'stretch'` to resample the + * source to the texture's dimensions. + */ + fit?: TextureWriteFit; +}; + type TgpuTextureViewDescriptor = { /** * Which {@link GPUTextureAspect | aspect(s)} of the texture are accessible to the texture view. @@ -209,7 +221,7 @@ export interface TgpuTexture extends TgpuNama clear(mipLevel?: number | 'all'): void; generateMipmaps(baseMipLevel?: number, mipLevels?: number): void; - write(source: ExternalImageSource | ExternalImageSource[]): void; + write(source: ExternalImageSource | ExternalImageSource[], options?: TextureWriteOptions): void; write(source: ArrayBuffer | TypedArray | DataView, mipLevel?: number): void; // TODO: support copies from GPUBuffers and TgpuBuffers copyFrom>(source: T): void; @@ -477,22 +489,29 @@ class TgpuTextureImpl implements TgpuTexture implements TgpuTexture implements TgpuTexture { - const texture = root.createTexture({ - size: [32, 32], - format: 'rgba8unorm', - }); + const texture = root + .createTexture({ + size: [32, 32], + format: 'rgba8unorm', + }) + .$usage('render'); const mockImage = { width: 32, @@ -635,9 +637,9 @@ Overload 3 of 4, '(schema: "(Error) Texture not usable as storage, call $usage(' ); }); - it('handles resizing when image dimensions do not match texture', ({ root, device }) => { + it('throws when image source writes are missing render usage', ({ root }) => { const texture = root.createTexture({ - size: [64, 64], + size: [32, 32], format: 'rgba8unorm', }); @@ -646,7 +648,46 @@ Overload 3 of 4, '(schema: "(Error) Texture not usable as storage, call $usage(' height: 32, } as HTMLImageElement; - texture.write(mockImage); + expect(() => texture.write(mockImage)).toThrowErrorMatchingInlineSnapshot( + `[Error: texture.write(...) with image sources requires 'render' usage. Add it via the $usage('render') method.]`, + ); + }); + + it('throws when image dimensions do not match texture without a fit mode', ({ root }) => { + const texture = root + .createTexture({ + size: [64, 64], + format: 'rgba8unorm', + }) + .$usage('render'); + + const mockImage = { + width: 32, + height: 32, + } as HTMLImageElement; + + expect(() => texture.write(mockImage)).toThrowErrorMatchingInlineSnapshot( + `[Error: Texture write source size 32x32 does not match target size 64x64. Pass fit: 'stretch' to resize explicitly.]`, + ); + }); + + it('handles resizing when image dimensions do not match texture with fit: stretch', ({ + root, + device, + }) => { + const texture = root + .createTexture({ + size: [64, 64], + format: 'rgba8unorm', + }) + .$usage('render'); + + const mockImage = { + width: 32, + height: 32, + } as HTMLImageElement; + + texture.write(mockImage, { fit: 'stretch' }); // Should create textures for resampling since image size doesn't match texture size expect(device.mock.createTexture).toHaveBeenCalled(); From 7f16e99d658129862f63771504fd576d528f42a1 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Wed, 12 Aug 2026 15:30:32 +0200 Subject: [PATCH 2/2] fixes --- .../src/content/docs/apis/textures.mdx | 2 +- .../jelly-slider.test.ts | 28 ++----------------- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/apis/textures.mdx b/apps/typegpu-docs/src/content/docs/apis/textures.mdx index f3a7e5e3a5..052365ff61 100644 --- a/apps/typegpu-docs/src/content/docs/apis/textures.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/textures.mdx @@ -248,7 +248,7 @@ const texture = root.createTexture({ mipLevelCount: 9, // log2(256) + 1 }).$usage('sampled', 'render'); -texture.write(imageBitmap); +texture.write(imageBitmap, { fit: 'stretch' }); texture.generateMipmaps(); // Generate all mip levels automatically ``` diff --git a/apps/typegpu-docs/tests/individual-example-tests/jelly-slider.test.ts b/apps/typegpu-docs/tests/individual-example-tests/jelly-slider.test.ts index c4d2b71138..0bdb44026a 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/jelly-slider.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/jelly-slider.test.ts @@ -24,37 +24,15 @@ describe('jelly-slider example', () => { mockFonts(); mockImageLoading(); mockResizeObserver(); - mockCreateImageBitmap(); + mockCreateImageBitmap({ width: 512, height: 256 }); }, - expectedCalls: 6, + expectedCalls: 4, }, device, ); expect(shaderCodes).toMatchInlineSnapshot(` - " - struct VertexOutput { - @builtin(position) pos: vec4f, - @location(0) uv: vec2f, - } - - @vertex - fn vs_main(@builtin(vertex_index) i: u32) -> VertexOutput { - const pos = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3)); - const uv = array(vec2f(0, 1), vec2f(2, 1), vec2f(0, -1)); - return VertexOutput(vec4f(pos[i], 0, 1), uv[i]); - } - - - @group(0) @binding(0) var src: texture_2d; - @group(0) @binding(1) var samp: sampler; - - @fragment - fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f { - return textureSample(src, samp, uv); - } - - struct fullScreenTriangle_Output { + "struct fullScreenTriangle_Output { @builtin(position) pos: vec4f, @location(0) uv: vec2f, }