From 717e73d32d352629d18c7b24cf975a39d95cb1d8 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Fri, 7 Aug 2026 23:22:01 -0500 Subject: [PATCH] framebuffer tga download --- fontSliders.js | 2 +- index.html | 9 ++++ index.js | 130 ++++++++++++++++++++++++++++++++++++------------- 3 files changed, 106 insertions(+), 35 deletions(-) diff --git a/fontSliders.js b/fontSliders.js index 3155c74..02626b5 100644 --- a/fontSliders.js +++ b/fontSliders.js @@ -9,7 +9,7 @@ function makeSliders() const sliders = { subpixelOptions: new RowHeader("global options"), mode: new Select("mode", ["supersampling", "subpixel"]), - scale: new Slider("scale", 0.0, 0.01), + scale: new Slider("scale", 0.0, 0.05), translateX: new Slider("translate x", -1, 1), translateY: new Slider("translate y", -1, 1), multisampleCount: new Slider("multisampleCount", 1, 4, 1), diff --git a/index.html b/index.html index e39f8f9..b2212b7 100644 --- a/index.html +++ b/index.html @@ -65,6 +65,11 @@ font: 0.8rem sans-serif; } + input[type="button"] { + width: calc(100% - 1.1em); + grid-column: 1 / span 3; + } + input[type="range"], input[type="text"] { height: 1rem; } @@ -100,6 +105,10 @@ + + + + diff --git a/index.js b/index.js index 61068ce..6c9dcfd 100644 --- a/index.js +++ b/index.js @@ -56,6 +56,7 @@ module.memory = memory; var depthTexture = undefined; var multisampleTexture = undefined; +var copyTexture = undefined; const eyeValueX = document.getElementById("eye-value-x"); const eyeValueY = document.getElementById("eye-value-y"); @@ -64,6 +65,9 @@ const eyeValueZ = document.getElementById("eye-value-z"); const yawValue = document.getElementById("yaw"); const pitchValue = document.getElementById("pitch"); +var downloadFramebufferPending = false; +var tempBuffer = undefined; + function recreateDepth(canvasTexture, sampleCount) { const depthResized = depthTexture === undefined || canvasTexture.width != depthTexture.width || canvasTexture.height != depthTexture.height; @@ -81,6 +85,33 @@ function recreateDepth(canvasTexture, sampleCount) }); } + const tempBufferSize = canvasTexture.width * canvasTexture.height * 4; + const tempResized = tempBuffer === undefined || tempBufferSize != tempBuffer.size; + if (tempResized) { + if (tempBuffer !== undefined) { + tempBuffer.destroy(); + } + tempBuffer = device.createBuffer({ + label: "temp buffer", + size: tempBufferSize, + usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, + }); + } + + const copyResized = copyTexture === undefined || canvasTexture.width !== copyTexture.width || canvasTexture.height !== copyTexture.height; + if (copyResized) { + if (copyTexture !== undefined) { + copyTexture.destroy(); + } + + copyTexture = device.createTexture({ + format: canvasTexture.format, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + size: [canvasTexture.width, canvasTexture.height], + sampleCount: 1, + }); + } + if (sampleCount > 1) { const multisampleResized = multisampleTexture === undefined || canvasTexture.width !== multisampleTexture.width || canvasTexture.height !== multisampleTexture.height; if (multisampleResized || multisampleTexture.sampleCount !== sampleCount) { @@ -177,9 +208,18 @@ function handleKeyup(e) } } +function handleDownloadButton(e) +{ + downloadFramebufferPending = true; + console.log("download framebuffer"); +} + window.addEventListener('keydown', handleKeydown, false); window.addEventListener('keyup', handleKeyup, false); +const downloadFramebufferButton = document.getElementById("download-framebuffer"); +downloadFramebufferButton.addEventListener("click", handleDownloadButton, false); + function updateView() { const lu = keyState[KEY.W] === true; @@ -260,7 +300,30 @@ canvas.addEventListener("mousemove", onMouseMove); var canRender = false; var frameNumber = 0; -var tempBuffer = undefined; +function tgaBlob(texture, mappedRange) +{ + const tgaHeaderSize = 18; + const tgaBuffer = new ArrayBuffer(mappedRange.byteLength + tgaHeaderSize); + const tgaView = new DataView(tgaBuffer); + tgaView.setUint8(0, 0); // idLength + tgaView.setUint8(1, 0); // colorMapType + tgaView.setUint8(2, 2); // imageTypeCode: 2 uncompressed RGB + tgaView.setUint16(3, 0, true); // colorMap origin + tgaView.setUint16(5, 0, true); // colorMap length + tgaView.setUint8(7, 0); // colorMap depth + tgaView.setUint16(8, 0, true); // xOrigin + tgaView.setUint16(10, 0, true); // yOrigin + tgaView.setUint16(12, texture.width, true); // width + tgaView.setUint16(14, texture.height, true); // height + tgaView.setUint8(16, 32); // bits per pixel + const descriptor = ((8 << 0) | // number of attribute bits + (1 << 5) | // origin in upper left + (0 << 6)); // non-interleaved + tgaView.setUint8(17, descriptor); // descriptor + const tgaU8 = new Uint8Array(tgaBuffer); + tgaU8.set(new Uint8Array(mappedRange), 18); + return new Blob([tgaBuffer]); +} function render2() { @@ -281,16 +344,19 @@ function render2() updateView(); - const clearValue = { r: 0.2, g: 0.2, b: 0.4, a: 1.0 }; + //const clearValue = ; + const clearValue = (downloadFramebufferPending) ? { r: 0.0, g: 0.0, b: 0.0, a: 0.0 } : { r: 0.2, g: 0.2, b: 0.4, a: 1.0 }; + + const viewTexture = (downloadFramebufferPending) ? copyTexture : canvasTexture; const attachment0 = (sampleCount == 1) ? { - view: canvasTexture.createView(), + view: viewTexture.createView(), loadOp: "clear", clearValue: clearValue, storeOp: "store", } : { view: multisampleTexture.createView(), - resolveTarget: canvasTexture.createView(), + resolveTarget: viewTexture.createView(), loadOp: "clear", clearValue: clearValue, storeOp: "store", @@ -318,44 +384,40 @@ function render2() renderPass.end(); - /* - encoder.copyTextureToBuffer({ - texture: multisampleTexture, - }, { - buffer: tempBuffer, - bytesPerRow: canvasTexture.width * 4 * 4, - }, { - width: canvasTexture.width, - height: canvasTexture.height, - depthOrArrayLayers: 1, + if (downloadFramebufferPending) { + encoder.copyTextureToBuffer({ + texture: copyTexture, + }, { + buffer: tempBuffer, + bytesPerRow: canvasTexture.width * 4, + }, { + width: copyTexture.width, + height: copyTexture.height, + depthOrArrayLayers: 1, }); - */ + } const commandBuffer = encoder.finish(); device.queue.submit([commandBuffer]); frameNumber = (frameNumber + 1) % 2; - /* - tempBuffer.mapAsync(GPUMapMode.READ).then(() => { - const readBuffer = tempBuffer.getMappedRange(); // arraybuffer - //console.log(mousePositionClick); - const x = mousePositionClick[0]; - const y = mousePositionClick[1]; - const f32ReadBuffer = new Float32Array(readBuffer); - for (let i = 0; i < 2; i++) { - const a = f32ReadBuffer[((y + i) * canvasTexture.width + x) * 4 + 0]; - const b = f32ReadBuffer[((y + i) * canvasTexture.width + x) * 4 + 1]; - const c = f32ReadBuffer[((y + i) * canvasTexture.width + x) * 4 + 2]; - const d = f32ReadBuffer[((y + i) * canvasTexture.width + x) * 4 + 3]; - //console.log(a, b, c, d); - } - - tempBuffer.unmap(); - requestAnimationFrame(render2); + if (downloadFramebufferPending) { + tempBuffer.mapAsync(GPUMapMode.READ).then(() => { + const mappedRange = tempBuffer.getMappedRange(); // arraybuffer + const blob = tgaBlob(copyTexture, mappedRange); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.setAttribute("href", url); + a.setAttribute("download", "framebuffer.tga"); + a.click(); + tempBuffer.unmap(); + requestAnimationFrame(render2); }); - */ - requestAnimationFrame(render2); + downloadFramebufferPending = false; + } else { + requestAnimationFrame(render2); + } } else { requestAnimationFrame(render2); }