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;
};
3 changes: 3 additions & 0 deletions packages/webgpu/apple/ApplePlatformContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class ApplePlatformContext : public PlatformContext {
wgpu::Surface makeSurface(wgpu::Instance instance, void *surface, int width,
int height) override;

void configureSurfaceColor(void *nativeSurface,
const SurfaceColorConfig &config) override;

ImageData createImageBitmap(std::string blobId, double offset,
double size) override;

Expand Down
65 changes: 65 additions & 0 deletions packages/webgpu/apple/ApplePlatformContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <TargetConditionals.h>

#import <QuartzCore/CAMetalLayer.h>
#import <React/RCTBlobManager.h>
#import <React/RCTBridge+Private.h>
#import <ReactCommon/RCTTurboModule.h>
Expand Down Expand Up @@ -39,6 +40,70 @@ void checkIfUsingSimulatorWithAPIValidation() {
return instance.CreateSurface(&surfaceDescriptor);
}

void ApplePlatformContext::configureSurfaceColor(
void *nativeSurface, const SurfaceColorConfig &config) {
if (!nativeSurface) {
NSLog(@"[RNWebGPU] configureSurfaceColor: nativeSurface is null");
return;
}
CAMetalLayer *layer = (__bridge CAMetalLayer *)nativeSurface;

// CAMetalLayer property setters are documented as thread-safe, and we need
// these changes to land before the caller renders / presents the next
// frame, so apply synchronously here instead of bouncing to the main queue.
[CATransaction begin];
[CATransaction setDisableActions:YES];

CGColorSpaceRef colorSpace = nullptr;
if (config.extendedDynamicRange) {
#if !TARGET_OS_OSX
if (@available(iOS 16.0, *)) {
layer.wantsExtendedDynamicRangeContent = YES;
}
#else
layer.wantsExtendedDynamicRangeContent = YES;
#endif
// Extended linear sRGB allows shader values >1 to map into the display's
// EDR headroom on supporting hardware.
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearSRGB);
} else {
#if !TARGET_OS_OSX
if (@available(iOS 16.0, *)) {
layer.wantsExtendedDynamicRangeContent = NO;
}
#else
layer.wantsExtendedDynamicRangeContent = NO;
#endif
// Non-extended sRGB clamps float values >1 to SDR. Linear variants
// ("kCGColorSpaceLinearSRGB") still pass values >1 through on iOS, so we
// use gamma-encoded sRGB for the standard tone-mapping mode.
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
}
if (colorSpace) {
layer.colorspace = colorSpace;
CGColorSpaceRelease(colorSpace);
}

[CATransaction commit];

#if !TARGET_OS_OSX
CGFloat headroom = 1.0;
if (@available(iOS 16.0, *)) {
headroom = UIScreen.mainScreen.currentEDRHeadroom;
}
NSLog(@"[RNWebGPU] HDR configure: extended=%d format=%u "
@"layer.wantsEDR=%d layer.colorspace=%@ EDRHeadroom=%.2f",
(int)config.extendedDynamicRange, (unsigned)config.format,
(int)layer.wantsExtendedDynamicRangeContent, layer.colorspace,
(double)headroom);
#else
NSLog(@"[RNWebGPU] HDR configure: extended=%d format=%u "
@"layer.wantsEDR=%d layer.colorspace=%@",
(int)config.extendedDynamicRange, (unsigned)config.format,
(int)layer.wantsExtendedDynamicRangeContent, layer.colorspace);
#endif
}

static std::span<const uint8_t> nsDataToSpan(NSData *data) {
return {static_cast<const uint8_t *>(data.bytes), data.length};
}
Expand Down
15 changes: 11 additions & 4 deletions packages/webgpu/apple/MetalView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ - (void)configure {
auto gpu = manager->_gpu;
auto surface = manager->_platformContext->makeSurface(
gpu, nativeSurface, size.width, size.height);
registry
.getSurfaceInfoOrCreate([_contextId intValue], gpu, size.width,
size.height)
->switchToOnscreen(nativeSurface, surface);
auto info = registry.getSurfaceInfoOrCreate([_contextId intValue], gpu,
size.width, size.height);
info->switchToOnscreen(nativeSurface, surface);
// If a previous configure() call from JS already requested an HDR / color
// config for this surface, replay it onto the freshly attached layer. This
// covers the race where JS calls context.configure() before this MetalView
// mounts (e.g., on a key-driven Canvas remount).
if (auto colorConfig = info->getColorConfig()) {
manager->_platformContext->configureSurfaceColor(nativeSurface,
*colorConfig);
}
}

- (void)update {
Expand Down
Loading
Loading