diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index 667f87164..909f8f7de 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -36,6 +36,7 @@ import { Reanimated } from "./Reanimated";
import { AsyncStarvation } from "./Diagnostics/AsyncStarvation";
import { DeviceLostHang } from "./Diagnostics/DeviceLostHang";
import { StorageBufferVertices } from "./StorageBufferVertices";
+import { HDR } from "./HDR";
// The two lines below are needed by three.js
import "fast-text-encoding";
@@ -97,6 +98,7 @@ function App() {
name="StorageBufferVertices"
component={StorageBufferVertices}
/>
+
diff --git a/apps/example/src/HDR/HDR.tsx b/apps/example/src/HDR/HDR.tsx
new file mode 100644
index 000000000..b37e11e05
--- /dev/null
+++ b/apps/example/src/HDR/HDR.tsx
@@ -0,0 +1,193 @@
+import React, { useEffect, useRef, useState } from "react";
+import {
+ Platform,
+ PixelRatio,
+ StyleSheet,
+ Switch,
+ Text,
+ View,
+} from "react-native";
+import type { CanvasRef } from "react-native-wgpu";
+import { Canvas } from "react-native-wgpu";
+
+import { fullscreenTriangleVertWGSL, hdrBandFragWGSL } from "./shaders";
+
+type ToneMapping = "standard" | "extended";
+
+const HDR_FORMAT: GPUTextureFormat = "rgba16float";
+const PEAK_MULTIPLIER = 8.0; // 8x SDR reference white.
+
+function HDRCanvas({
+ toneMapping,
+ peak,
+}: {
+ toneMapping: ToneMapping;
+ peak: number;
+}) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ const adapter = await navigator.gpu.requestAdapter();
+ if (!adapter) {
+ throw new Error("No adapter");
+ }
+ const device = await adapter.requestDevice();
+ if (cancelled) {
+ return;
+ }
+
+ const context = ref.current!.getContext("webgpu")!;
+ const canvas = context.canvas as HTMLCanvasElement;
+ canvas.width = canvas.clientWidth * PixelRatio.get();
+ canvas.height = canvas.clientHeight * PixelRatio.get();
+
+ context.configure({
+ device,
+ format: HDR_FORMAT,
+ alphaMode: "opaque",
+ toneMapping: { mode: toneMapping },
+ });
+
+ const pipeline = device.createRenderPipeline({
+ layout: "auto",
+ vertex: {
+ module: device.createShaderModule({
+ code: fullscreenTriangleVertWGSL,
+ }),
+ entryPoint: "main",
+ },
+ fragment: {
+ module: device.createShaderModule({ code: hdrBandFragWGSL }),
+ entryPoint: "main",
+ targets: [{ format: HDR_FORMAT }],
+ },
+ primitive: { topology: "triangle-list" },
+ });
+
+ const paramsBuffer = device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+ device.queue.writeBuffer(
+ paramsBuffer,
+ 0,
+ new Float32Array([peak, 0, 0, 0]),
+ );
+
+ const bindGroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: paramsBuffer } }],
+ });
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: context.getCurrentTexture().createView(),
+ clearValue: [0, 0, 0, 1],
+ loadOp: "clear",
+ storeOp: "store",
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.draw(3);
+ pass.end();
+ device.queue.submit([encoder.finish()]);
+ context.present();
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [toneMapping, peak]);
+
+ return ;
+}
+
+export function HDR() {
+ const [extended, setExtended] = useState(true);
+ const mode: ToneMapping = extended ? "extended" : "standard";
+ return (
+
+
+ HDR — {mode}
+
+ Extended
+
+
+
+
+
+ Left band: SDR white (1.0). Right band: {PEAK_MULTIPLIER}x. Toggle the
+ switch. With "extended" on an EDR display (iPhone Pro / iPad Pro XDR /
+ MBP XDR), the right band glows visibly brighter than the left. In
+ "standard" both bands match.
+
+
+ Tip: dim the display brightness; the OS allocates more EDR headroom at
+ lower SDR brightness, so the boost is more obvious. iOS Settings,
+ Display & Brightness, Auto-Brightness can also affect headroom.
+
+
+
+ {/* Force a fresh CAMetalLayer per mode: iOS won't downgrade a layer
+ out of EDR composition once it's been promoted, so we remount the
+ Canvas (and therefore the underlying MetalView) when the toggle
+ flips. */}
+
+
+
+ {Platform.OS !== "ios" && Platform.OS !== "macos" ? (
+
+ Note: HDR display is currently only wired for Apple platforms.
+
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: "black",
+ },
+ toolbar: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ padding: 12,
+ },
+ title: {
+ color: "white",
+ fontSize: 14,
+ fontWeight: "600",
+ flex: 1,
+ },
+ switchRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ switchLabel: {
+ color: "white",
+ marginRight: 8,
+ },
+ hint: {
+ color: "#bbb",
+ fontSize: 12,
+ paddingHorizontal: 12,
+ paddingBottom: 8,
+ },
+ canvasContainer: {
+ flex: 1,
+ margin: 12,
+ backgroundColor: "black",
+ },
+ warning: {
+ color: "#ffb347",
+ fontSize: 12,
+ padding: 12,
+ },
+});
diff --git a/apps/example/src/HDR/index.ts b/apps/example/src/HDR/index.ts
new file mode 100644
index 000000000..7086cc0ba
--- /dev/null
+++ b/apps/example/src/HDR/index.ts
@@ -0,0 +1 @@
+export { HDR } from "./HDR";
diff --git a/apps/example/src/HDR/shaders.ts b/apps/example/src/HDR/shaders.ts
new file mode 100644
index 000000000..fc5e6cf2d
--- /dev/null
+++ b/apps/example/src/HDR/shaders.ts
@@ -0,0 +1,55 @@
+export const fullscreenTriangleVertWGSL = /* wgsl */ `
+struct VSOut {
+ @builtin(position) position : vec4,
+ @location(0) uv : vec2,
+};
+
+@vertex
+fn main(@builtin(vertex_index) idx : u32) -> VSOut {
+ var pos = array, 3>(
+ vec2(-1.0, -1.0),
+ vec2( 3.0, -1.0),
+ vec2(-1.0, 3.0),
+ );
+ let p = pos[idx];
+ var out : VSOut;
+ out.position = vec4(p, 0.0, 1.0);
+ out.uv = (p + vec2(1.0, 1.0)) * 0.5;
+ out.uv.y = 1.0 - out.uv.y;
+ return out;
+}
+`;
+
+// Renders three vertical bands:
+// left third: solid value 1.0 (SDR reference white)
+// middle third: black gap
+// right third: solid value = peak (HDR if peak > 1)
+//
+// With "extended" tone mapping on an EDR-capable display the right band
+// glows visibly brighter than the left. With "standard" tone mapping
+// the right band is clamped to 1.0 and matches the left.
+export const hdrBandFragWGSL = /* wgsl */ `
+struct Params {
+ peak : f32,
+ _p0 : f32,
+ _p1 : f32,
+ _p2 : f32,
+};
+
+@group(0) @binding(0) var params : Params;
+
+@fragment
+fn main(@location(0) uv : vec2) -> @location(0) vec4 {
+ let x = uv.x;
+ if (x < 0.4) {
+ // SDR reference white.
+ return vec4(1.0, 1.0, 1.0, 1.0);
+ } else if (x < 0.6) {
+ // Black gap so the two whites are not adjacent.
+ return vec4(0.0, 0.0, 0.0, 1.0);
+ } else {
+ // Bright (potentially HDR) white.
+ return vec4(params.peak, params.peak, params.peak, 1.0);
+ }
+}
+`;
diff --git a/apps/example/src/Home.tsx b/apps/example/src/Home.tsx
index 9272dfec9..47ffe40bf 100644
--- a/apps/example/src/Home.tsx
+++ b/apps/example/src/Home.tsx
@@ -127,6 +127,10 @@ export const examples = [
screen: "StorageBufferVertices",
title: "💾 Storage Buffer Vertices",
},
+ {
+ screen: "HDR",
+ title: "🌞 HDR Canvas",
+ },
];
const styles = StyleSheet.create({
diff --git a/apps/example/src/Route.ts b/apps/example/src/Route.ts
index 152923e1e..4d91d0db0 100644
--- a/apps/example/src/Route.ts
+++ b/apps/example/src/Route.ts
@@ -29,4 +29,5 @@ export type Routes = {
AsyncStarvation: undefined;
DeviceLostHang: undefined;
StorageBufferVertices: undefined;
+ HDR: undefined;
};
diff --git a/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h b/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h
index 110a45d44..1ff5c7c4c 100644
--- a/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h
+++ b/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h
@@ -1,6 +1,7 @@
#pragma once
#include
+#include
#include
#include
#include
@@ -40,6 +41,7 @@ class SurfaceInfo {
config.width = width;
config.height = height;
config.presentMode = wgpu::PresentMode::Fifo;
+ _rebindColorChain();
_configure();
}
@@ -151,7 +153,21 @@ class SurfaceInfo {
return config.device;
}
+ void setColorManagement(std::optional mgmt) {
+ std::unique_lock lock(_mutex);
+ _colorManagement = std::move(mgmt);
+ _rebindColorChain();
+ if (surface) {
+ surface.Configure(&config);
+ }
+ }
+
private:
+ void _rebindColorChain() {
+ config.nextInChain =
+ _colorManagement ? &_colorManagement.value() : nullptr;
+ }
+
void _configure() {
if (surface) {
surface.Configure(&config);
@@ -175,6 +191,7 @@ class SurfaceInfo {
wgpu::SurfaceConfiguration config;
int width;
int height;
+ std::optional _colorManagement;
};
class SurfaceRegistry {
diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp
index d75eb7b0f..abd57d7ca 100644
--- a/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp
+++ b/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp
@@ -35,6 +35,14 @@ void GPUCanvasContext::configure(
#endif
surfaceConfiguration.presentMode = wgpu::PresentMode::Fifo;
_surfaceInfo->configure(surfaceConfiguration);
+
+ wgpu::SurfaceColorManagement colorManagement;
+ colorManagement.colorSpace = wgpu::PredefinedColorSpace::SRGB;
+ colorManagement.toneMappingMode =
+ configuration->toneMappingMode == GPUCanvasToneMappingMode::Extended
+ ? wgpu::ToneMappingMode::Extended
+ : wgpu::ToneMappingMode::Standard;
+ _surfaceInfo->setColorManagement(colorManagement);
}
void GPUCanvasContext::unconfigure() {}
diff --git a/packages/webgpu/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h b/packages/webgpu/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h
index c27b4a7fb..6136407e7 100644
--- a/packages/webgpu/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h
+++ b/packages/webgpu/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h
@@ -1,6 +1,7 @@
#pragma once
#include
+#include
#include
#include "webgpu/webgpu_cpp.h"
@@ -13,6 +14,11 @@ namespace jsi = facebook::jsi;
namespace rnwgpu {
+enum class GPUCanvasToneMappingMode {
+ Standard,
+ Extended,
+};
+
struct GPUCanvasConfiguration {
std::shared_ptr device; // GPUDevice
wgpu::TextureFormat format; // GPUTextureFormat
@@ -20,6 +26,7 @@ struct GPUCanvasConfiguration {
std::optional>
viewFormats; // Iterable
wgpu::CompositeAlphaMode alphaMode = wgpu::CompositeAlphaMode::Opaque;
+ GPUCanvasToneMappingMode toneMappingMode = GPUCanvasToneMappingMode::Standard;
};
} // namespace rnwgpu
@@ -63,6 +70,20 @@ struct JSIConverter> {
result->alphaMode = wgpu::CompositeAlphaMode::Premultiplied;
}
}
+ if (value.hasProperty(runtime, "toneMapping")) {
+ auto toneMapping = value.getProperty(runtime, "toneMapping");
+ if (toneMapping.isObject()) {
+ auto tmObj = toneMapping.getObject(runtime);
+ if (tmObj.hasProperty(runtime, "mode")) {
+ auto mode = tmObj.getProperty(runtime, "mode")
+ .asString(runtime)
+ .utf8(runtime);
+ if (mode == "extended") {
+ result->toneMappingMode = GPUCanvasToneMappingMode::Extended;
+ }
+ }
+ }
+ }
}
return result;