Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -97,6 +98,7 @@ function App() {
name="StorageBufferVertices"
component={StorageBufferVertices}
/>
<Stack.Screen name="HDR" component={HDR} />
</Stack.Navigator>
</NavigationContainer>
</GestureHandlerRootView>
Expand Down
193 changes: 193 additions & 0 deletions apps/example/src/HDR/HDR.tsx
Original file line number Diff line number Diff line change
@@ -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<CanvasRef>(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 <Canvas ref={ref} style={StyleSheet.absoluteFill} />;
}

export function HDR() {
const [extended, setExtended] = useState(true);
const mode: ToneMapping = extended ? "extended" : "standard";
return (
<View style={styles.container}>
<View style={styles.toolbar}>
<Text style={styles.title}>HDR — {mode}</Text>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>Extended</Text>
<Switch value={extended} onValueChange={setExtended} />
</View>
</View>

<Text style={styles.hint}>
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.
</Text>
<Text style={styles.hint}>
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.
</Text>

<View style={styles.canvasContainer}>
{/* 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. */}
<HDRCanvas key={mode} toneMapping={mode} peak={PEAK_MULTIPLIER} />
</View>

{Platform.OS !== "ios" && Platform.OS !== "macos" ? (
<Text style={styles.warning}>
Note: HDR display is currently only wired for Apple platforms.
</Text>
) : null}
</View>
);
}

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,
},
});
1 change: 1 addition & 0 deletions apps/example/src/HDR/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HDR } from "./HDR";
55 changes: 55 additions & 0 deletions apps/example/src/HDR/shaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export const fullscreenTriangleVertWGSL = /* wgsl */ `
struct VSOut {
@builtin(position) position : vec4<f32>,
@location(0) uv : vec2<f32>,
};

@vertex
fn main(@builtin(vertex_index) idx : u32) -> VSOut {
var pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
let p = pos[idx];
var out : VSOut;
out.position = vec4<f32>(p, 0.0, 1.0);
out.uv = (p + vec2<f32>(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<uniform> params : Params;

@fragment
fn main(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> {
let x = uv.x;
if (x < 0.4) {
// SDR reference white.
return vec4<f32>(1.0, 1.0, 1.0, 1.0);
} else if (x < 0.6) {
// Black gap so the two whites are not adjacent.
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
} else {
// Bright (potentially HDR) white.
return vec4<f32>(params.peak, params.peak, params.peak, 1.0);
}
}
`;
4 changes: 4 additions & 0 deletions apps/example/src/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ export const examples = [
screen: "StorageBufferVertices",
title: "💾 Storage Buffer Vertices",
},
{
screen: "HDR",
title: "🌞 HDR Canvas",
},
];

const styles = StyleSheet.create({
Expand Down
1 change: 1 addition & 0 deletions apps/example/src/Route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ export type Routes = {
AsyncStarvation: undefined;
DeviceLostHang: undefined;
StorageBufferVertices: undefined;
HDR: undefined;
};
17 changes: 17 additions & 0 deletions packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <memory>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
#include <utility>
Expand Down Expand Up @@ -40,6 +41,7 @@ class SurfaceInfo {
config.width = width;
config.height = height;
config.presentMode = wgpu::PresentMode::Fifo;
_rebindColorChain();
_configure();
}

Expand Down Expand Up @@ -151,7 +153,21 @@ class SurfaceInfo {
return config.device;
}

void setColorManagement(std::optional<wgpu::SurfaceColorManagement> mgmt) {
std::unique_lock<std::shared_mutex> 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);
Expand All @@ -175,6 +191,7 @@ class SurfaceInfo {
wgpu::SurfaceConfiguration config;
int width;
int height;
std::optional<wgpu::SurfaceColorManagement> _colorManagement;
};

class SurfaceRegistry {
Expand Down
8 changes: 8 additions & 0 deletions packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <memory>
#include <string>
#include <vector>

#include "webgpu/webgpu_cpp.h"
Expand All @@ -13,13 +14,19 @@ namespace jsi = facebook::jsi;

namespace rnwgpu {

enum class GPUCanvasToneMappingMode {
Standard,
Extended,
};

struct GPUCanvasConfiguration {
std::shared_ptr<GPUDevice> device; // GPUDevice
wgpu::TextureFormat format; // GPUTextureFormat
std::optional<double> usage; // GPUTextureUsageFlags
std::optional<std::vector<wgpu::TextureFormat>>
viewFormats; // Iterable<GPUTextureFormat>
wgpu::CompositeAlphaMode alphaMode = wgpu::CompositeAlphaMode::Opaque;
GPUCanvasToneMappingMode toneMappingMode = GPUCanvasToneMappingMode::Standard;
};

} // namespace rnwgpu
Expand Down Expand Up @@ -63,6 +70,20 @@ struct JSIConverter<std::shared_ptr<rnwgpu::GPUCanvasConfiguration>> {
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;
Expand Down
Loading