Skip to content

Commit bfbe325

Browse files
fix(🐛): handle flipY: false correctly in copyExternalImageToTexture (#337)
--------- Co-authored-by: William Candillon <wcandillon@gmail.com>
1 parent f1e3b27 commit bfbe325

3 files changed

Lines changed: 126 additions & 2 deletions

File tree

packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ void GPUQueue::copyExternalImageToTexture(
124124
throw std::runtime_error("Invalid input for GPUQueue::writeTexture()");
125125
}
126126

127-
if (source->flipY) {
127+
if (source->flipY.value_or(false)) {
128128
// Calculate the row size and total size
129129
uint32_t rowSize = bytesPerPixel * source->source->getWidth();
130130
uint32_t totalSize = source->source->getSize();

packages/webgpu/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-wgpu",
3-
"version": "0.5.8",
3+
"version": "0.5.9",
44
"description": "React Native WebGPU",
55
"main": "lib/commonjs/index",
66
"module": "lib/module/index",

packages/webgpu/src/__tests__/ExternalTexture.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,128 @@ describe("External Textures", () => {
281281
const image = encodeImage(result);
282282
checkImage(image, "snapshots/f2.png");
283283
});
284+
it("flipY: false should not flip (same as omitting flipY)", async () => {
285+
const result = await client.eval(
286+
({ gpu, device, ctx, canvas, urls: { fTexture } }) => {
287+
const module = device.createShaderModule({
288+
label: "our hardcoded textured quad shaders",
289+
code: /* wgsl */ `
290+
struct OurVertexShaderOutput {
291+
@builtin(position) position: vec4f,
292+
@location(0) texcoord: vec2f,
293+
};
294+
295+
@vertex fn vs(
296+
@builtin(vertex_index) vertexIndex : u32
297+
) -> OurVertexShaderOutput {
298+
let pos = array(
299+
// 1st triangle
300+
vec2f( 0.0, 0.0), // center
301+
vec2f( 1.0, 0.0), // right, center
302+
vec2f( 0.0, 1.0), // center, top
303+
304+
// 2st triangle
305+
vec2f( 0.0, 1.0), // center, top
306+
vec2f( 1.0, 0.0), // right, center
307+
vec2f( 1.0, 1.0), // right, top
308+
);
309+
310+
var vsOutput: OurVertexShaderOutput;
311+
let xy = pos[vertexIndex];
312+
vsOutput.position = vec4f(xy, 0.0, 1.0);
313+
vsOutput.texcoord = xy;
314+
return vsOutput;
315+
}
316+
317+
@group(0) @binding(0) var ourSampler: sampler;
318+
@group(0) @binding(1) var ourTexture: texture_2d<f32>;
319+
320+
@fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
321+
return textureSample(ourTexture, ourSampler, fsInput.texcoord);
322+
}
323+
`,
324+
});
325+
326+
const presentationFormat = gpu.getPreferredCanvasFormat();
327+
const pipeline = device.createRenderPipeline({
328+
label: "hardcoded textured quad pipeline",
329+
layout: "auto",
330+
vertex: {
331+
module,
332+
},
333+
fragment: {
334+
module,
335+
targets: [{ format: presentationFormat }],
336+
},
337+
});
338+
339+
return fetch(fTexture).then((res) => {
340+
return res.blob().then((blob) => {
341+
return createImageBitmap(blob, {
342+
colorSpaceConversion: "none",
343+
}).then((source) => {
344+
const texture = device.createTexture({
345+
label: fTexture,
346+
format: "rgba8unorm",
347+
size: [source.width, source.height],
348+
usage:
349+
GPUTextureUsage.TEXTURE_BINDING |
350+
GPUTextureUsage.COPY_DST |
351+
GPUTextureUsage.RENDER_ATTACHMENT,
352+
});
353+
// Explicitly pass flipY: false - should behave same as omitting it
354+
device.queue.copyExternalImageToTexture(
355+
{ source, flipY: false },
356+
{ texture },
357+
{ width: source.width, height: source.height },
358+
);
359+
const sampler = device.createSampler({
360+
addressModeU: "repeat",
361+
addressModeV: "repeat",
362+
magFilter: "linear",
363+
});
364+
365+
const bindGroup = device.createBindGroup({
366+
layout: pipeline.getBindGroupLayout(0),
367+
entries: [
368+
{ binding: 0, resource: sampler },
369+
{ binding: 1, resource: texture.createView() },
370+
],
371+
});
372+
373+
const renderPassDescriptor: GPURenderPassDescriptor = {
374+
label: "our basic canvas renderPass",
375+
colorAttachments: [
376+
{
377+
view: ctx.getCurrentTexture().createView(),
378+
clearValue: [0.3, 0.3, 0.3, 1],
379+
loadOp: "clear",
380+
storeOp: "store",
381+
},
382+
],
383+
};
384+
385+
const encoder = device.createCommandEncoder({
386+
label: "render quad encoder",
387+
});
388+
const pass = encoder.beginRenderPass(renderPassDescriptor);
389+
pass.setPipeline(pipeline);
390+
pass.setBindGroup(0, bindGroup);
391+
pass.draw(6);
392+
pass.end();
393+
394+
const commandBuffer = encoder.finish();
395+
device.queue.submit([commandBuffer]);
396+
return canvas.getImageData();
397+
});
398+
});
399+
});
400+
},
401+
{},
402+
);
403+
const image = encodeImage(result);
404+
// flipY: false should produce the same result as omitting flipY (f2.png)
405+
// This test catches the bug where std::optional<bool> was checked incorrectly
406+
checkImage(image, "snapshots/f2.png");
407+
});
284408
});

0 commit comments

Comments
 (0)