From ab5e80ba72d6752c64d11ef4bbc0b0ac1a0200a3 Mon Sep 17 00:00:00 2001 From: Dave Hulbert Date: Sun, 26 Oct 2025 08:51:43 +0000 Subject: [PATCH 1/4] Add WebGPU fractal explorer tool --- tools/webgpu-fractal-explorer/README.md | 4 + tools/webgpu-fractal-explorer/index.html | 389 +++++++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 tools/webgpu-fractal-explorer/README.md create mode 100644 tools/webgpu-fractal-explorer/index.html diff --git a/tools/webgpu-fractal-explorer/README.md b/tools/webgpu-fractal-explorer/README.md new file mode 100644 index 0000000..408c3e4 --- /dev/null +++ b/tools/webgpu-fractal-explorer/README.md @@ -0,0 +1,4 @@ +--- +title: WebGPU Fractal Explorer +description: Explore Mandelbrot fractals with real-time WebGPU rendering +--- diff --git a/tools/webgpu-fractal-explorer/index.html b/tools/webgpu-fractal-explorer/index.html new file mode 100644 index 0000000..2cb3768 --- /dev/null +++ b/tools/webgpu-fractal-explorer/index.html @@ -0,0 +1,389 @@ + + + + + + WebGPU Fractal Explorer + + + +
+
+

WebGPU Fractal Explorer

+

+ Render and explore the Mandelbrot set directly in your browser using WebGPU – + no native app required. Drag to pan, scroll to zoom, and tweak the parameters + to discover new patterns. +

+
+ +
+
+
+ + +
+
+ Tip: For the best detail, enable "Use hardware acceleration" in your browser settings. +
+
+ + +
+
+ + + + + + From 0cb5c86e6e1483dceb547070662a52293bd46983 Mon Sep 17 00:00:00 2001 From: Dave Hulbert Date: Sun, 26 Oct 2025 10:22:24 +0000 Subject: [PATCH 2/4] Improve WebGPU fractal error handling --- tools/webgpu-fractal-explorer/index.html | 195 +++++++++++++++++++---- 1 file changed, 160 insertions(+), 35 deletions(-) diff --git a/tools/webgpu-fractal-explorer/index.html b/tools/webgpu-fractal-explorer/index.html index 2cb3768..45f630d 100644 --- a/tools/webgpu-fractal-explorer/index.html +++ b/tools/webgpu-fractal-explorer/index.html @@ -111,6 +111,37 @@

Controls

const colorValue = document.getElementById("colorValue"); const resetButton = document.getElementById("resetView"); + function showSupportMessage({ title, body, details, isError = false }) { + supportMessage.replaceChildren(); + + const wrapper = document.createElement("div"); + wrapper.className = "space-y-2"; + + const titleEl = document.createElement("p"); + titleEl.className = `text-lg font-semibold ${ + isError ? "text-red-700" : "text-brand-800" + }`; + titleEl.textContent = title; + wrapper.appendChild(titleEl); + + const bodyEl = document.createElement("p"); + bodyEl.className = "text-sm text-brand-700"; + bodyEl.textContent = body; + wrapper.appendChild(bodyEl); + + if (details) { + const detailsEl = document.createElement("pre"); + detailsEl.className = + "overflow-x-auto rounded-md bg-brand-100/70 p-3 text-left text-xs text-brand-800"; + detailsEl.textContent = details; + wrapper.appendChild(detailsEl); + } + + supportMessage.appendChild(wrapper); + supportMessage.classList.remove("hidden"); + supportMessage.classList.add("flex"); + } + const params = { centerX: -0.75, centerY: 0.0, @@ -142,25 +173,78 @@

Controls

}); async function initWebGPU() { + supportMessage.classList.add("hidden"); + supportMessage.classList.remove("flex"); + supportMessage.textContent = ""; + if (!navigator.gpu) { - supportMessage.textContent = "Your browser does not support WebGPU yet. Try updating to the latest version of Chrome, Edge, or Firefox (with the WebGPU flag enabled)."; - supportMessage.classList.remove("hidden"); - supportMessage.classList.add("flex"); + showSupportMessage({ + title: "WebGPU is unavailable", + body: + "Your browser does not support WebGPU yet. Try updating to the latest version of Chrome, Edge, or Firefox (with the WebGPU flag enabled).", + }); + return; + } + + let adapter; + try { + adapter = await navigator.gpu.requestAdapter(); + } catch (error) { + console.error("Failed to request WebGPU adapter", error); + showSupportMessage({ + title: "Unable to access a GPU adapter", + body: "Ensure hardware acceleration is enabled and that no other app is blocking GPU access.", + details: error instanceof Error ? error.message : String(error), + isError: true, + }); return; } - const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { - supportMessage.textContent = "Unable to access a GPU adapter. Ensure hardware acceleration is enabled."; - supportMessage.classList.remove("hidden"); - supportMessage.classList.add("flex"); + showSupportMessage({ + title: "No compatible GPU adapter found", + body: "Your device reported that no WebGPU-compatible adapter is available. Try enabling hardware acceleration or switching to a newer browser.", + isError: true, + }); + return; + } + + let device; + try { + device = await adapter.requestDevice(); + } catch (error) { + console.error("Failed to request WebGPU device", error); + showSupportMessage({ + title: "Unable to start WebGPU", + body: "The GPU rejected the WebGPU device request. This can happen if required features are disabled or the driver is outdated.", + details: error instanceof Error ? error.message : String(error), + isError: true, + }); return; } - const device = await adapter.requestDevice(); const context = canvas.getContext("webgpu"); + if (!context) { + showSupportMessage({ + title: "Unable to initialise canvas", + body: "The browser could not create a WebGPU context for the canvas element.", + isError: true, + }); + return; + } const format = navigator.gpu.getPreferredCanvasFormat(); - context.configure({ device, format, alphaMode: "premultiplied" }); + try { + context.configure({ device, format, alphaMode: "premultiplied" }); + } catch (error) { + console.error("Failed to configure WebGPU context", error); + showSupportMessage({ + title: "WebGPU configuration failed", + body: "Configuring the canvas for WebGPU rendering failed. This may happen on low-memory devices or when WebGPU is partially supported.", + details: error instanceof Error ? error.message : String(error), + isError: true, + }); + return; + } const shaderModule = device.createShaderModule({ label: "fractal-shader", @@ -242,7 +326,9 @@

Controls

`, }); - const pipeline = device.createRenderPipeline({ + let pipeline; + try { + pipeline = device.createRenderPipeline({ label: "fractal-pipeline", layout: "auto", vertex: { @@ -258,7 +344,17 @@

Controls

topology: "triangle-strip", stripIndexFormat: "uint32", }, - }); + }); + } catch (error) { + console.error("Failed to create WebGPU pipeline", error); + showSupportMessage({ + title: "Rendering pipeline creation failed", + body: "The GPU could not compile the fractal shader. Try updating to the latest browser or GPU drivers.", + details: error instanceof Error ? error.message : String(error), + isError: true, + }); + return; + } const uniformBuffer = device.createBuffer({ label: "uniform-buffer", @@ -301,29 +397,39 @@

Controls

} function render() { - resizeCanvas(); - updateUniforms(); - - const encoder = device.createCommandEncoder(); - const textureView = context.getCurrentTexture().createView(); - const pass = encoder.beginRenderPass({ - colorAttachments: [ - { - view: textureView, - clearValue: { r: 0, g: 0, b: 0, a: 1 }, - loadOp: "clear", - storeOp: "store", - }, - ], - }); - - pass.setPipeline(pipeline); - pass.setBindGroup(0, bindGroup); - pass.draw(4); - pass.end(); - - device.queue.submit([encoder.finish()]); - requestAnimationFrame(render); + try { + resizeCanvas(); + updateUniforms(); + + const encoder = device.createCommandEncoder(); + const textureView = context.getCurrentTexture().createView(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: textureView, + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: "clear", + storeOp: "store", + }, + ], + }); + + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(4); + pass.end(); + + device.queue.submit([encoder.finish()]); + requestAnimationFrame(render); + } catch (error) { + console.error("WebGPU render loop failed", error); + showSupportMessage({ + title: "WebGPU render loop stopped", + body: "Rendering stopped due to an unexpected error.", + details: error instanceof Error ? error.message : String(error), + isError: true, + }); + } } let isDragging = false; @@ -380,10 +486,29 @@

Controls

} }); + device.lost.then((info) => { + console.warn("WebGPU device lost", info); + showSupportMessage({ + title: "WebGPU device lost", + body: `The GPU device was lost (${info.reason}). Trying to recover…`, + details: info.message, + isError: true, + }); + initWebGPU(); + }); + render(); } - initWebGPU(); + initWebGPU().catch((error) => { + console.error("Failed to initialise WebGPU fractal explorer", error); + showSupportMessage({ + title: "Initialisation error", + body: "Something went wrong while starting the fractal explorer.", + details: error instanceof Error ? error.message : String(error), + isError: true, + }); + }); From 352d887ea121941a9e0d94f77d52cd1c5e716331 Mon Sep 17 00:00:00 2001 From: Dave Hulbert Date: Sun, 26 Oct 2025 21:33:20 +0000 Subject: [PATCH 3/4] Fix WebGPU uniform buffer sizing and surface JS errors --- tools/webgpu-fractal-explorer/index.html | 46 +++++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/tools/webgpu-fractal-explorer/index.html b/tools/webgpu-fractal-explorer/index.html index 45f630d..80af6b3 100644 --- a/tools/webgpu-fractal-explorer/index.html +++ b/tools/webgpu-fractal-explorer/index.html @@ -356,9 +356,15 @@

Controls

return; } + const uniformArray = new Float32Array(8); + const uniformBufferSize = Math.max( + uniformArray.byteLength, + device.limits?.minUniformBufferOffsetAlignment || 256 + ); + const uniformBuffer = device.createBuffer({ label: "uniform-buffer", - size: 4 * 8, + size: uniformBufferSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); @@ -367,13 +373,14 @@

Controls

entries: [ { binding: 0, - resource: { buffer: uniformBuffer }, + resource: { + buffer: uniformBuffer, + size: uniformArray.byteLength, + }, }, ], }); - const uniformArray = new Float32Array(8); - function resizeCanvas() { const dpr = window.devicePixelRatio || 1; const displayWidth = Math.floor(canvas.clientWidth * dpr); @@ -393,7 +400,13 @@

Controls

uniformArray[2] = params.scale; uniformArray[4] = params.iterations; uniformArray[5] = params.colorShift; - device.queue.writeBuffer(uniformBuffer, 0, uniformArray.buffer); + device.queue.writeBuffer( + uniformBuffer, + 0, + uniformArray, + 0, + uniformArray.byteLength + ); } function render() { @@ -509,6 +522,29 @@

Controls

isError: true, }); }); + + window.addEventListener("error", (event) => { + console.error("Uncaught error in fractal explorer", event.error || event.message); + showSupportMessage({ + title: "A script error occurred", + body: "The fractal explorer hit a JavaScript error and had to stop.", + details: event?.error?.stack || event?.message || String(event), + isError: true, + }); + }); + + window.addEventListener("unhandledrejection", (event) => { + console.error("Unhandled promise rejection in fractal explorer", event.reason); + showSupportMessage({ + title: "An unexpected promise rejection occurred", + body: "A background task failed unexpectedly.", + details: + event?.reason instanceof Error + ? event.reason.stack || event.reason.message + : String(event?.reason ?? "Unknown reason"), + isError: true, + }); + }); From 92ffbad3dc30bae66b510707839b0c478aff3f64 Mon Sep 17 00:00:00 2001 From: Dave Hulbert Date: Sun, 26 Oct 2025 21:41:01 +0000 Subject: [PATCH 4/4] Fix WebGPU uniform uploads on strict implementations --- tools/webgpu-fractal-explorer/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/webgpu-fractal-explorer/index.html b/tools/webgpu-fractal-explorer/index.html index 80af6b3..d5064d8 100644 --- a/tools/webgpu-fractal-explorer/index.html +++ b/tools/webgpu-fractal-explorer/index.html @@ -403,8 +403,8 @@

Controls

device.queue.writeBuffer( uniformBuffer, 0, - uniformArray, - 0, + uniformArray.buffer, + uniformArray.byteOffset, uniformArray.byteLength ); }