diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index a03b723bb8..c737980c82 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -112,7 +112,7 @@ const App = () => { screenOptions={{ headerLeft: HeaderLeft, }} - initialRouteName={CI ? "Tests" : "Home"} + initialRouteName={CI ? "Tests" : "WebGPU"} > { + const { navigate } = + useNavigation>(); + return ( + + {examples.map((example) => ( + { + navigate(example.screen as keyof Routes); + }} + testID={example.screen} + > + + {example.title} + {example.description} + + + ))} + + ); +}; diff --git a/apps/example/src/Examples/WebGPU/Routes.ts b/apps/example/src/Examples/WebGPU/Routes.ts new file mode 100644 index 0000000000..66cb2da81b --- /dev/null +++ b/apps/example/src/Examples/WebGPU/Routes.ts @@ -0,0 +1,6 @@ +export type Routes = { + List: undefined; + Wireframes: undefined; + Triangle: undefined; + TexturedCube: undefined; +}; diff --git a/apps/example/src/Examples/WebGPU/Shaders.ts b/apps/example/src/Examples/WebGPU/Shaders.ts index cc88c0760d..f469047e9d 100644 --- a/apps/example/src/Examples/WebGPU/Shaders.ts +++ b/apps/example/src/Examples/WebGPU/Shaders.ts @@ -29,6 +29,42 @@ struct VSOut { return vec4f(uni.color.rgb * light, uni.color.a); }`; +export const basicVertWGSL = /* wgsl */ `struct Uniforms { + modelViewProjectionMatrix : mat4x4f, +} +@binding(0) @group(0) var uniforms : Uniforms; + +struct VertexOutput { + @builtin(position) Position : vec4f, + @location(0) fragUV : vec2f, + @location(1) fragPosition: vec4f, +} + +@vertex +fn main( + @location(0) position : vec4f, + @location(1) uv : vec2f +) -> VertexOutput { + var output : VertexOutput; + output.Position = uniforms.modelViewProjectionMatrix * position; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} +`; + +export const sampleTextureMixColorWGSL = /* wgsl */ `@group(0) @binding(1) var mySampler: sampler; +@group(0) @binding(2) var myTexture: texture_2d; + +@fragment +fn main( + @location(0) fragUV: vec2f, + @location(1) fragPosition: vec4f +) -> @location(0) vec4f { + return textureSample(myTexture, mySampler, fragUV);// * fragPosition; +} +`; + export const wireframeWGSL = /*wgsl*/ `struct Uniforms { worldViewProjectionMatrix: mat4x4f, worldMatrix: mat4x4f, diff --git a/apps/example/src/Examples/WebGPU/TexturedCube.tsx b/apps/example/src/Examples/WebGPU/TexturedCube.tsx new file mode 100644 index 0000000000..dd7b170f5e --- /dev/null +++ b/apps/example/src/Examples/WebGPU/TexturedCube.tsx @@ -0,0 +1,521 @@ +import React, { useEffect, useRef } from "react"; +import { StyleSheet, View, Text } from "react-native"; +import type { + SkCanvas, + SkParagraph, + SkTextStyle, + WebGPUCanvasRef, +} from "@shopify/react-native-skia"; +import { + WebGPUCanvas, + Skia, + BlurStyle, + BlendMode, + FontWeight, + FontSlant, + PaintStyle, + TextDecoration, + TextDecorationStyle, + useFonts, + fitbox, + rect, + processTransform3d, + StrokeCap, + StrokeJoin, + TileMode, +} from "@shopify/react-native-skia"; + +import { + cubePositionOffset, + cubeUVOffset, + cubeVertexArray, + cubeVertexSize, +} from "./cube"; +import { basicVertWGSL, sampleTextureMixColorWGSL } from "./Shaders"; +import { + mat4Identity, + mat4Multiply, + mat4Perspective, + mat4Rotate, + mat4Translate, + type Mat4, + type Vec3, +} from "./matrix"; + +const videoURL = "https://bit.ly/skia-video"; +const TEXTURE_SIZE = 512; +const paint = Skia.Paint(); + +// Breathe drawing resources +const breathePaint = Skia.Paint(); +breathePaint.setMaskFilter(Skia.MaskFilter.MakeBlur(BlurStyle.Solid, 40, true)); +breathePaint.setBlendMode(BlendMode.Screen); +const breatheColors = ["#529ca0", "#61bea2"]; + +// Hello path +const helloSvg = + "M13.6 247.8C13.6 247.8 51.8 206.1 84.2 168.8 140.8 103.4 202.8 27.1 150.1 14.3 131 9.7 116.4 29.3 107.3 44.8 69.7 108.4 58 213.8 57.5 302 67.7 271.3 104.4 190.3 140.2 192.5 181.5 195.1 145.3 257 154.5 283.8 168.8 321.6 208.2 292.3 230 276.9 265.9 251.5 289 230.7 289 199.9 289 161 235.3 173.5 223.3 204.6 213.9 228.9 214.3 265.3 229.3 283.6 247.5 305.7 287.7 309.4 312.2 287.9 337 266.2 354.7 234 368.7 212.5 403.9 158.3 464.4 85.6 449.1 29.5 447 21.9 440.4 16 432.5 15.7 393.6 14.2 381.8 98.6 375.3 128.8 368.8 159.3 345.2 260.8 373.1 292.5 404.4 328 446.3 261.9 464.7 231.1 468.7 224.8 472.6 217.9 476.1 212.5 511.3 158.4 571.8 85.6 556.5 29.5 554.4 21.9 547.8 16.1 539.9 15.8 501 14.2 489.2 98.7 482.8 128.8 476.2 159.3 452.6 260.8 480.5 292.6 511.8 328.1 562.4 265 572.6 232.3 587.3 185.4 620.9 171 660.9 179.7M660.9 179.7C616 166.1 580.9 199.1 572.6 232.6 566.8 256.4 573.5 281.6 599.2 295.2 668.5 331.9 742.8 211.1 660.9 179.7ZM660.9 179.7C643.7 181.3 636.1 204.2 643.3 227.2 654.3 263.4 704.3 267.7 733.1 255.5"; +const helloPath = Skia.Path.MakeFromSVGString(helloSvg)!; +const helloBounds = helloPath.computeTightBounds(); +helloPath.transform( + processTransform3d( + fitbox( + "contain", + helloBounds, + rect(30, 30, TEXTURE_SIZE - 60, TEXTURE_SIZE - 60) + ) + ) +); + +// Paragraph fonts +const paragraphFonts = { + Roboto: [ + require("../../Tests/assets/Roboto-Medium.ttf"), + require("../../Tests/assets/Roboto-Regular.ttf"), + ], +}; + +function drawTexture1(canvas: SkCanvas, t: number) { + canvas.drawColor(Skia.Color("white")); + const progress = (Math.sin(t * 0.8) + 1) / 2; + const trimmed = helloPath.copy(); + trimmed.trim(0, progress, false); + trimmed.stroke({ width: 25, join: StrokeJoin.Round, cap: StrokeCap.Round }); + paint.setShader( + Skia.Shader.MakeLinearGradient( + { x: 0, y: 0 }, + { x: 512, y: 0 }, + [ + "#3FCEBC", + "#3CBCEB", + "#5F96E7", + "#816FE3", + "#9F5EE2", + "#DE589F", + "#FF645E", + "#FDA859", + "#FAEC54", + "#9EE671", + "#41E08D", + ].map((c) => Skia.Color(c)), + null, + TileMode.Clamp + ) + ); + canvas.drawPath(trimmed, paint); +} + +function drawTexture2(canvas: SkCanvas, t: number, paragraph: SkParagraph) { + canvas.drawColor(Skia.Color("white")); + const progress = (Math.sin(t) + 1) / 2; + const minW = TEXTURE_SIZE * 0.3; + const maxW = TEXTURE_SIZE * 0.8; + const width = minW + progress * (maxW - minW); + paragraph.layout(width); + paragraph.paint(canvas, 30, 30); +} + +function easeInOut(t: number): number { + // Attempt to match Easing.inOut(Easing.ease) — a cubic bezier ease-in-out + // Approximation using smoothstep: 3t² - 2t³ + return t * t * (3 - 2 * t); +} + +function drawTexture3(canvas: SkCanvas, t: number) { + const cx = TEXTURE_SIZE / 2; + const cy = TEXTURE_SIZE / 2; + const R = TEXTURE_SIZE / 4; + const total = 6; + + // Triangle wave: 0→1→0 over 6 seconds (matching useLoop({duration: 3000})) + const period = 6; + const phase = (t % period) / period; // 0..1 + const triangle = phase < 0.5 ? phase * 2 : 2 - phase * 2; // 0→1→0 + const progress = easeInOut(triangle); + + // Background + canvas.drawColor(Skia.Color("rgb(36,43,56)")); + + canvas.save(); + + // Group rotation: mix(progress, -PI, 0) around center + const angle = (-Math.PI * (1 - progress) * 180) / Math.PI; + canvas.translate(cx, cy); + canvas.rotate(angle, 0, 0); + canvas.translate(-cx, -cy); + + // Draw 6 circles + for (let i = 0; i < total; i++) { + const theta = (i * 2 * Math.PI) / total; + const r = progress * R; + const x = cx + r * Math.cos(theta); + const y = cy + r * Math.sin(theta); + const scale = 0.3 + 0.7 * progress; + breathePaint.setColor(Skia.Color(breatheColors[i % 2])); + canvas.drawCircle(x, y, R * scale, breathePaint); + } + + canvas.restore(); +} + +export function TexturedCube() { + const canvasRef = useRef(null); + const animationRef = useRef(0); + const cleanupRef = useRef<(() => void) | null>(null); + const customFontMgr = useFonts(paragraphFonts); + + useEffect(() => { + const timeoutId = setTimeout(async () => { + if (!canvasRef.current) { + return; + } + if (typeof RNWebGPU === "undefined") { + return; + } + if (!customFontMgr) { + return; + } + const ctx = canvasRef.current.getContext("webgpu"); + if (!ctx) { + return; + } + + const device = Skia.getDevice(); + const canvas = ctx.canvas as HTMLCanvasElement; + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + + ctx.configure({ + device, + format: presentationFormat, + alphaMode: "premultiplied", + }); + + const verticesBuffer = device.createBuffer({ + size: cubeVertexArray.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, + }); + new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray); + verticesBuffer.unmap(); + + const pipeline = device.createRenderPipeline({ + layout: "auto", + vertex: { + module: device.createShaderModule({ code: basicVertWGSL }), + buffers: [ + { + arrayStride: cubeVertexSize, + attributes: [ + { + shaderLocation: 0, + offset: cubePositionOffset, + format: "float32x4", + }, + { + shaderLocation: 1, + offset: cubeUVOffset, + format: "float32x2", + }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + code: sampleTextureMixColorWGSL, + }), + targets: [{ format: presentationFormat }], + }, + primitive: { topology: "triangle-list", cullMode: "back" }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: "less", + format: "depth24plus", + }, + }); + + const depthTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: "depth24plus", + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const uniformBuffer = device.createBuffer({ + size: 4 * 16, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const surface1 = Skia.Surface.MakeOffscreen(TEXTURE_SIZE, TEXTURE_SIZE)!; + const surface2 = Skia.Surface.MakeOffscreen(TEXTURE_SIZE, TEXTURE_SIZE)!; + const surface3 = Skia.Surface.MakeOffscreen(TEXTURE_SIZE, TEXTURE_SIZE)!; + + // Video player — managed directly on the JS thread + const video = await Skia.Video(videoURL); + video.setLooping(true); + video.setVolume(0); + video.play(); + + // Build paragraph for texture 2 + const fontSize = 40; + const strokePaint = Skia.Paint(); + strokePaint.setStyle(PaintStyle.Stroke); + strokePaint.setStrokeWidth(1); + + const textStyle = { + fontSize, + fontFamilies: ["Roboto"], + color: Skia.Color("#000"), + }; + + const coloredTextStyle = { + fontSize: fontSize * 1.3, + fontFamilies: ["Roboto"], + fontStyle: { weight: FontWeight.Medium }, + color: Skia.Color("#61bea2"), + }; + + const crazyStyle: SkTextStyle = { + color: Skia.Color("#000"), + backgroundColor: Skia.Color("#CECECE"), + fontSize: fontSize * 1.3, + fontFamilies: ["Roboto"], + letterSpacing: -1, + wordSpacing: 20, + fontStyle: { + slant: FontSlant.Italic, + weight: FontWeight.ExtraBlack, + }, + shadows: [ + { + color: Skia.Color("#00000044"), + blurRadius: 4, + offset: { x: 4, y: 4 }, + }, + ], + decorationColor: Skia.Color("#00223A"), + decorationThickness: 2, + decoration: TextDecoration.Underline, + decorationStyle: TextDecorationStyle.Dotted, + }; + + const paragraph = Skia.ParagraphBuilder.Make({}, customFontMgr) + .pushStyle(textStyle) + .addText("Hello ") + .pushStyle({ + ...textStyle, + fontStyle: { weight: FontWeight.Medium }, + }) + .addText("Skia") + .pop() + .addText("\n\nThis text rendered using the ") + .pushStyle(coloredTextStyle) + .addText("SkParagraph ") + .pop() + .addText("module with ") + .pushStyle({ ...coloredTextStyle, color: Skia.Color("#f5a623") }) + .addText("libgrapheme ") + .pop() + .addText("on iOS.") + .pushStyle(textStyle) + .addText( + "\n\nOn Android we use built-in ICU while on web we use CanvasKit's." + ) + .pop() + .pushStyle(crazyStyle, strokePaint) + .addText("\n\nWow - this is cool.") + .pop() + .build(); + + const sampler = device.createSampler({ + magFilter: "linear", + minFilter: "linear", + }); + + const bindGroupLayout = pipeline.getBindGroupLayout(0); + + function createBindGroup(texture: GPUTexture) { + return device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: uniformBuffer } }, + { binding: 1, resource: sampler }, + { binding: 2, resource: texture.createView() }, + ], + }); + } + + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + view: undefined as any, + clearValue: [0.5, 0.5, 0.5, 1.0], + loadOp: "clear", + storeOp: "store", + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + depthClearValue: 1.0, + depthLoadOp: "clear", + depthStoreOp: "store", + }, + }; + + const aspect = canvas.width / canvas.height; + const projectionMatrix = mat4Perspective( + (2 * Math.PI) / 5, + aspect, + 1, + 100.0 + ); + const mvp = mat4Identity(); + + function getTransformationMatrix(): Mat4 { + const view = mat4Identity(); + mat4Translate(view, [0, 0, -4] as Vec3, view); + const now = Date.now() / 1000; + mat4Rotate(view, [Math.sin(now), Math.cos(now), 0] as Vec3, 1, view); + mat4Multiply(projectionMatrix, view, mvp); + return mvp; + } + + let running = true; + + const render = () => { + if (!running) { + return; + } + const t = Date.now() / 1000; + + // Update Skia textures + drawTexture1(surface1.getCanvas(), t); + surface1.flush(); + const tex1 = Skia.Image.MakeTextureFromImage( + surface1.makeImageSnapshot() + ); + const bindGroup1 = createBindGroup(tex1); + + drawTexture2(surface2.getCanvas(), t, paragraph); + surface2.flush(); + const tex2 = Skia.Image.MakeTextureFromImage( + surface2.makeImageSnapshot() + ); + const bindGroup2 = createBindGroup(tex2); + + drawTexture3(surface3.getCanvas(), t); + surface3.flush(); + const tex3 = Skia.Image.MakeTextureFromImage( + surface3.makeImageSnapshot() + ); + const bindGroup3 = createBindGroup(tex3); + + const frame = video.nextImage(); + const tex4 = frame ? Skia.Image.MakeTextureFromImage(frame) : null; + const bindGroup4 = tex4 ? createBindGroup(tex4) : null; + + // Update MVP matrix + const mat = getTransformationMatrix(); + device.queue.writeBuffer( + uniformBuffer, + 0, + mat.buffer, + mat.byteOffset, + mat.byteLength + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (renderPassDescriptor.colorAttachments as any)[0].view = ctx + .getCurrentTexture() + .createView(); + + const commandEncoder = device.createCommandEncoder(); + const pass = commandEncoder.beginRenderPass(renderPassDescriptor); + pass.setPipeline(pipeline); + pass.setVertexBuffer(0, verticesBuffer); + + // 1 face each for hello/paragraph/breathe, 3 faces for video + pass.setBindGroup(0, bindGroup1); + pass.draw(6, 1, 0, 0); + + pass.setBindGroup(0, bindGroup2); + pass.draw(6, 1, 6, 0); + + pass.setBindGroup(0, bindGroup3); + pass.draw(6, 1, 12, 0); + + if (bindGroup4) { + pass.setBindGroup(0, bindGroup4); + pass.draw(18, 1, 18, 0); + } + + pass.end(); + device.queue.submit([commandEncoder.finish()]); + ctx.present(); + + animationRef.current = requestAnimationFrame(render); + }; + + animationRef.current = requestAnimationFrame(render); + + cleanupRef.current = () => { + running = false; + cancelAnimationFrame(animationRef.current); + video.dispose(); + }; + }, 100); + + return () => { + clearTimeout(timeoutId); + cleanupRef.current?.(); + }; + }, [customFontMgr]); + + if (typeof RNWebGPU === "undefined") { + return ( + + + + WebGPU Canvas requires SK_GRAPHITE to be enabled. + + + Build react-native-skia with Graphite support to use this feature. + + + + ); + } + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#1a1a1a", + }, + canvas: { + flex: 1, + }, + messageContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + message: { + color: "#fff", + fontSize: 18, + textAlign: "center", + marginBottom: 10, + }, + submessage: { + color: "#888", + fontSize: 14, + textAlign: "center", + }, +}); diff --git a/apps/example/src/Examples/WebGPU/Triangle.tsx b/apps/example/src/Examples/WebGPU/Triangle.tsx new file mode 100644 index 0000000000..622fc7b137 --- /dev/null +++ b/apps/example/src/Examples/WebGPU/Triangle.tsx @@ -0,0 +1,181 @@ +import React, { useEffect, useRef } from "react"; +import { StyleSheet, View, Text } from "react-native"; +import type { WebGPUCanvasRef } from "@shopify/react-native-skia"; +import { WebGPUCanvas } from "@shopify/react-native-skia"; + +const triangleShader = ` +@vertex +fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4f { + var pos = array( + vec2f( 0.0, 0.5), + vec2f(-0.5, -0.5), + vec2f( 0.5, -0.5) + ); + return vec4f(pos[vertexIndex], 0.0, 1.0); +} + +@fragment +fn fs_main() -> @location(0) vec4f { + return vec4f(1.0, 0.5, 0.2, 1.0); +} +`; + +export function Triangle() { + const canvasRef = useRef(null); + const animationRef = useRef(0); + const cleanupRef = useRef<(() => void) | null>(null); + + useEffect(() => { + // Small delay to ensure the canvas is mounted + const timeoutId = setTimeout(async () => { + if (!canvasRef.current) { + return; + } + + // Check if RNWebGPU is available (SK_GRAPHITE must be enabled) + if (typeof RNWebGPU === "undefined") { + console.warn( + "RNWebGPU is not available. Make sure SK_GRAPHITE is enabled." + ); + return; + } + + const ctx = canvasRef.current.getContext("webgpu"); + if (!ctx) { + console.warn("Failed to get WebGPU context"); + return; + } + + // Get the device from navigator.gpu + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + console.warn("Failed to get GPU adapter"); + return; + } + + const device = await adapter.requestDevice(); + + // Configure the context + const format = navigator.gpu.getPreferredCanvasFormat(); + ctx.configure({ + device, + format, + alphaMode: "opaque", + }); + + // Create shader module + const shaderModule = device.createShaderModule({ + code: triangleShader, + }); + + // Create render pipeline + const pipeline = device.createRenderPipeline({ + layout: "auto", + vertex: { + module: shaderModule, + entryPoint: "vs_main", + }, + fragment: { + module: shaderModule, + entryPoint: "fs_main", + targets: [{ format }], + }, + primitive: { + topology: "triangle-list", + }, + }); + + let running = true; + + const render = () => { + if (!running) { + return; + } + + const texture = ctx.getCurrentTexture(); + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: texture.createView(), + clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }, + loadOp: "clear", + storeOp: "store", + }, + ], + }; + + const commandEncoder = device.createCommandEncoder(); + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.draw(3); + passEncoder.end(); + + device.queue.submit([commandEncoder.finish()]); + ctx.present(); + + animationRef.current = requestAnimationFrame(render); + }; + + animationRef.current = requestAnimationFrame(render); + + cleanupRef.current = () => { + running = false; + cancelAnimationFrame(animationRef.current); + }; + }, 100); + + return () => { + clearTimeout(timeoutId); + cleanupRef.current?.(); + }; + }, []); + + // Check if WebGPU is available + if (typeof RNWebGPU === "undefined") { + return ( + + + + WebGPU Canvas requires SK_GRAPHITE to be enabled. + + + Build react-native-skia with Graphite support to use this feature. + + + + ); + } + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#1a1a1a", + }, + canvas: { + flex: 1, + }, + messageContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + message: { + color: "#fff", + fontSize: 18, + textAlign: "center", + marginBottom: 10, + }, + submessage: { + color: "#888", + fontSize: 14, + textAlign: "center", + }, +}); diff --git a/apps/example/src/Examples/WebGPU/WebGPU.tsx b/apps/example/src/Examples/WebGPU/Wireframes.tsx similarity index 99% rename from apps/example/src/Examples/WebGPU/WebGPU.tsx rename to apps/example/src/Examples/WebGPU/Wireframes.tsx index 0564fdf6d1..5695cfe9bb 100644 --- a/apps/example/src/Examples/WebGPU/WebGPU.tsx +++ b/apps/example/src/Examples/WebGPU/Wireframes.tsx @@ -124,7 +124,7 @@ type ObjectInfo = { // 0, 0, 0, 1, 0, // ]; -export function WebGPU() { +export function Wireframes() { const { width, height } = useWindowDimensions(); const [image, setImage] = useState(null); const renderRef = useRef<((ts: number) => void) | null>(null); diff --git a/apps/example/src/Examples/WebGPU/assets/Di-3d.png b/apps/example/src/Examples/WebGPU/assets/Di-3d.png new file mode 100644 index 0000000000..ebbff45ead Binary files /dev/null and b/apps/example/src/Examples/WebGPU/assets/Di-3d.png differ diff --git a/apps/example/src/Examples/WebGPU/cube.ts b/apps/example/src/Examples/WebGPU/cube.ts new file mode 100644 index 0000000000..9953984e8f --- /dev/null +++ b/apps/example/src/Examples/WebGPU/cube.ts @@ -0,0 +1,51 @@ +export const cubeVertexSize = 4 * 10; // Byte size of one cube vertex. +export const cubePositionOffset = 0; +export const cubeColorOffset = 4 * 4; // Byte offset of cube vertex color attribute. +export const cubeUVOffset = 4 * 8; +export const cubeVertexCount = 36; + +// prettier-ignore +export const cubeVertexArray = new Float32Array([ + // float4 position, float4 color, float2 uv, + 1, -1, 1, 1, 1, 0, 1, 1, 0, 1, + -1, -1, 1, 1, 0, 0, 1, 1, 1, 1, + -1, -1, -1, 1, 0, 0, 0, 1, 1, 0, + 1, -1, -1, 1, 1, 0, 0, 1, 0, 0, + 1, -1, 1, 1, 1, 0, 1, 1, 0, 1, + -1, -1, -1, 1, 0, 0, 0, 1, 1, 0, + + 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, + 1, -1, 1, 1, 1, 0, 1, 1, 1, 1, + 1, -1, -1, 1, 1, 0, 0, 1, 1, 0, + 1, 1, -1, 1, 1, 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, + 1, -1, -1, 1, 1, 0, 0, 1, 1, 0, + + -1, 1, 1, 1, 0, 1, 1, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, -1, 1, 1, 1, 0, 1, 1, 0, + -1, 1, -1, 1, 0, 1, 0, 1, 0, 0, + -1, 1, 1, 1, 0, 1, 1, 1, 0, 1, + 1, 1, -1, 1, 1, 1, 0, 1, 1, 0, + + -1, -1, 1, 1, 0, 0, 1, 1, 0, 1, + -1, 1, 1, 1, 0, 1, 1, 1, 1, 1, + -1, 1, -1, 1, 0, 1, 0, 1, 1, 0, + -1, -1, -1, 1, 0, 0, 0, 1, 0, 0, + -1, -1, 1, 1, 0, 0, 1, 1, 0, 1, + -1, 1, -1, 1, 0, 1, 0, 1, 1, 0, + + 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, + -1, 1, 1, 1, 0, 1, 1, 1, 1, 1, + -1, -1, 1, 1, 0, 0, 1, 1, 1, 0, + -1, -1, 1, 1, 0, 0, 1, 1, 1, 0, + 1, -1, 1, 1, 1, 0, 1, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, + + 1, -1, -1, 1, 1, 0, 0, 1, 0, 1, + -1, -1, -1, 1, 0, 0, 0, 1, 1, 1, + -1, 1, -1, 1, 0, 1, 0, 1, 1, 0, + 1, 1, -1, 1, 1, 1, 0, 1, 0, 0, + 1, -1, -1, 1, 1, 0, 0, 1, 0, 1, + -1, 1, -1, 1, 0, 1, 0, 1, 1, 0, +]); diff --git a/apps/example/src/Examples/WebGPU/index.ts b/apps/example/src/Examples/WebGPU/index.ts deleted file mode 100644 index 89aa395b91..0000000000 --- a/apps/example/src/Examples/WebGPU/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { WebGPU } from "./WebGPU"; diff --git a/apps/example/src/Examples/WebGPU/index.tsx b/apps/example/src/Examples/WebGPU/index.tsx new file mode 100644 index 0000000000..292bd85c23 --- /dev/null +++ b/apps/example/src/Examples/WebGPU/index.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; + +import type { Routes } from "./Routes"; +import { List } from "./List"; +import { Triangle } from "./Triangle"; +import { Wireframes } from "./Wireframes"; +import { TexturedCube } from "./TexturedCube"; + +const Stack = createNativeStackNavigator(); + +export const WebGPU = () => { + return ( + + null, + }} + /> + + null, + }} + /> + + + ); +}; diff --git a/apps/example/src/Examples/WebGPU/matrix.ts b/apps/example/src/Examples/WebGPU/matrix.ts index f7fc215227..6b202f66ce 100644 --- a/apps/example/src/Examples/WebGPU/matrix.ts +++ b/apps/example/src/Examples/WebGPU/matrix.ts @@ -216,6 +216,60 @@ export function mat4RotateY(m: Mat4, angle: number, dst?: Mat4): Mat4 { return dst; } +export function mat4Rotate(m: Mat4, axis: Vec3, angle: number, dst?: Mat4): Mat4 { + dst = dst || new Float32Array(16); + let x = axis[0], + y = axis[1], + z = axis[2]; + const len = Math.sqrt(x * x + y * y + z * z); + if (len === 0) { + return m; + } + x /= len; + y /= len; + z /= len; + + const s = Math.sin(angle); + const c = Math.cos(angle); + const t = 1 - c; + + const r00 = t * x * x + c; + const r01 = t * x * y + s * z; + const r02 = t * x * z - s * y; + const r10 = t * x * y - s * z; + const r11 = t * y * y + c; + const r12 = t * y * z + s * x; + const r20 = t * x * z + s * y; + const r21 = t * y * z - s * x; + const r22 = t * z * z + c; + + const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3]; + const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7]; + const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11]; + + dst[0] = m00 * r00 + m10 * r01 + m20 * r02; + dst[1] = m01 * r00 + m11 * r01 + m21 * r02; + dst[2] = m02 * r00 + m12 * r01 + m22 * r02; + dst[3] = m03 * r00 + m13 * r01 + m23 * r02; + dst[4] = m00 * r10 + m10 * r11 + m20 * r12; + dst[5] = m01 * r10 + m11 * r11 + m21 * r12; + dst[6] = m02 * r10 + m12 * r11 + m22 * r12; + dst[7] = m03 * r10 + m13 * r11 + m23 * r12; + dst[8] = m00 * r20 + m10 * r21 + m20 * r22; + dst[9] = m01 * r20 + m11 * r21 + m21 * r22; + dst[10] = m02 * r20 + m12 * r21 + m22 * r22; + dst[11] = m03 * r20 + m13 * r21 + m23 * r22; + + if (dst !== m) { + dst[12] = m[12]; + dst[13] = m[13]; + dst[14] = m[14]; + dst[15] = m[15]; + } + + return dst; +} + export function mat3FromMat4(m: Mat4, dst?: Float32Array): Float32Array { dst = dst || new Float32Array(12); dst[0] = m[0]; diff --git a/externals/skia b/externals/skia index ee20d565ac..f4ed99d244 160000 --- a/externals/skia +++ b/externals/skia @@ -1 +1 @@ -Subproject commit ee20d565acb08dece4a32e3f209cdd41119015ca +Subproject commit f4ed99d2443962782cf5f8b4dd27179f131e7cbe diff --git a/packages/skia/android/CMakeLists.txt b/packages/skia/android/CMakeLists.txt index d72f0cb5b0..756120783b 100644 --- a/packages/skia/android/CMakeLists.txt +++ b/packages/skia/android/CMakeLists.txt @@ -167,6 +167,7 @@ if(SK_GRAPHITE) "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUAdapter.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUBindGroup.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUBuffer.cpp" + "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUCanvasContext.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUCommandEncoder.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUComputePassEncoder.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUComputePipeline.cpp" @@ -180,6 +181,9 @@ if(SK_GRAPHITE) "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUShaderModule.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUSupportedLimits.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUTexture.cpp" + + # WebGPU Canvas JNI bindings + "${PROJECT_SOURCE_DIR}/cpp/jni/JniWebGPUView.cpp" ) endif() diff --git a/packages/skia/android/cpp/jni/JniWebGPUView.cpp b/packages/skia/android/cpp/jni/JniWebGPUView.cpp new file mode 100644 index 0000000000..ecff186f20 --- /dev/null +++ b/packages/skia/android/cpp/jni/JniWebGPUView.cpp @@ -0,0 +1,67 @@ +#include +#include + +#ifdef SK_GRAPHITE +#include "webgpu/webgpu_cpp.h" +#include "rnwgpu/SurfaceRegistry.h" +#include "rnskia/RNDawnContext.h" +#endif + +extern "C" JNIEXPORT void JNICALL +Java_com_shopify_reactnative_skia_WebGPUView_onSurfaceCreate( + JNIEnv *env, jobject thiz, jobject jSurface, jint contextId, jfloat width, + jfloat height) { +#ifdef SK_GRAPHITE + auto window = ANativeWindow_fromSurface(env, jSurface); + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + auto &dawnContext = RNSkia::DawnContext::getInstance(); + auto gpu = dawnContext.getWGPUInstance(); + + // Create surface from ANativeWindow + wgpu::SurfaceDescriptorFromAndroidNativeWindow androidSurfaceDesc; + androidSurfaceDesc.window = window; + wgpu::SurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &androidSurfaceDesc; + auto surface = gpu.CreateSurface(&surfaceDescriptor); + + registry + .getSurfaceInfoOrCreate(contextId, gpu, static_cast(width), + static_cast(height)) + ->switchToOnscreen(window, surface); +#endif +} + +extern "C" JNIEXPORT void JNICALL +Java_com_shopify_reactnative_skia_WebGPUView_onSurfaceChanged( + JNIEnv *env, jobject thiz, jobject surface, jint contextId, jfloat width, + jfloat height) { +#ifdef SK_GRAPHITE + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + auto surfaceInfo = registry.getSurfaceInfo(contextId); + if (surfaceInfo) { + surfaceInfo->resize(static_cast(width), static_cast(height)); + } +#endif +} + +extern "C" JNIEXPORT void JNICALL +Java_com_shopify_reactnative_skia_WebGPUView_switchToOffscreenSurface( + JNIEnv *env, jobject thiz, jint contextId) { +#ifdef SK_GRAPHITE + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + auto surfaceInfo = registry.getSurfaceInfo(contextId); + if (surfaceInfo) { + surfaceInfo->switchToOffscreen(); + } +#endif +} + +extern "C" JNIEXPORT void JNICALL +Java_com_shopify_reactnative_skia_WebGPUView_onSurfaceDestroy(JNIEnv *env, + jobject thiz, + jint contextId) { +#ifdef SK_GRAPHITE + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + registry.removeSurfaceInfo(contextId); +#endif +} diff --git a/packages/skia/android/cpp/rnskia-android/SkiaPlatformContext.h b/packages/skia/android/cpp/rnskia-android/SkiaPlatformContext.h new file mode 100644 index 0000000000..23632892fd --- /dev/null +++ b/packages/skia/android/cpp/rnskia-android/SkiaPlatformContext.h @@ -0,0 +1,26 @@ +#pragma once + +#ifdef SK_GRAPHITE + +#include "rnwgpu/PlatformContext.h" + +namespace rnwgpu { + +class SkiaPlatformContext : public PlatformContext { +public: + SkiaPlatformContext() = default; + ~SkiaPlatformContext() = default; + + wgpu::Surface makeSurface(wgpu::Instance instance, void *surface, int width, + int height) override { + wgpu::SurfaceDescriptorFromAndroidNativeWindow androidSurfaceDesc; + androidSurfaceDesc.window = surface; + wgpu::SurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &androidSurfaceDesc; + return instance.CreateSurface(&surfaceDescriptor); + } +}; + +} // namespace rnwgpu + +#endif // SK_GRAPHITE diff --git a/packages/skia/android/src/main/java/com/shopify/reactnative/skia/RNSkiaPackage.java b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/RNSkiaPackage.java index fa83eeebc8..08c598a392 100644 --- a/packages/skia/android/src/main/java/com/shopify/reactnative/skia/RNSkiaPackage.java +++ b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/RNSkiaPackage.java @@ -42,7 +42,10 @@ public NativeModule getModule(String s, ReactApplicationContext reactApplication @Override public List createViewManagers(ReactApplicationContext reactContext) { - return Arrays.asList(new SkiaPictureViewManager()); + return Arrays.asList( + new SkiaPictureViewManager(), + new WebGPUViewManager() + ); } @Override diff --git a/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUSurfaceView.java b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUSurfaceView.java new file mode 100644 index 0000000000..787bc0ac97 --- /dev/null +++ b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUSurfaceView.java @@ -0,0 +1,41 @@ +package com.shopify.reactnative.skia; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import androidx.annotation.NonNull; + +@SuppressLint("ViewConstructor") +public class WebGPUSurfaceView extends SurfaceView implements SurfaceHolder.Callback { + + WebGPUViewAPI mApi; + + public WebGPUSurfaceView(Context context, WebGPUViewAPI api) { + super(context); + mApi = api; + getHolder().addCallback(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mApi.surfaceDestroyed(); + } + + @Override + public void surfaceCreated(@NonNull SurfaceHolder holder) { + mApi.surfaceCreated(holder.getSurface()); + } + + @Override + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { + mApi.surfaceChanged(holder.getSurface()); + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + mApi.surfaceOffscreen(); + } +} diff --git a/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUTextureView.java b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUTextureView.java new file mode 100644 index 0000000000..747cd1b210 --- /dev/null +++ b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUTextureView.java @@ -0,0 +1,44 @@ +package com.shopify.reactnative.skia; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.view.Surface; +import android.view.TextureView; +import androidx.annotation.NonNull; + +@SuppressLint("ViewConstructor") +public class WebGPUTextureView extends TextureView implements TextureView.SurfaceTextureListener { + + WebGPUViewAPI mApi; + + public WebGPUTextureView(Context context, WebGPUViewAPI api) { + super(context); + mApi = api; + setOpaque(false); + setSurfaceTextureListener(this); + } + + @Override + public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int width, int height) { + Surface surface = new Surface(surfaceTexture); + mApi.surfaceCreated(surface); + } + + @Override + public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surfaceTexture, int width, int height) { + Surface surface = new Surface(surfaceTexture); + mApi.surfaceChanged(surface); + } + + @Override + public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surfaceTexture) { + mApi.surfaceDestroyed(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surfaceTexture) { + // No implementation needed + } +} diff --git a/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUView.java b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUView.java new file mode 100644 index 0000000000..b8f75d3ca2 --- /dev/null +++ b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUView.java @@ -0,0 +1,95 @@ +package com.shopify.reactnative.skia; + +import android.content.Context; +import android.view.Surface; +import android.view.View; + +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.views.view.ReactViewGroup; + +public class WebGPUView extends ReactViewGroup implements WebGPUViewAPI { + + private int mContextId; + private boolean mTransparent = false; + private View mView = null; + + WebGPUView(Context context) { + super(context); + } + + public void setContextId(int contextId) { + mContextId = contextId; + } + + public void setTransparent(boolean value) { + Context ctx = getContext(); + if (value != mTransparent || mView == null) { + if (mView != null) { + removeView(mView); + } + mTransparent = value; + if (mTransparent) { + mView = new WebGPUTextureView(ctx, this); + } else { + mView = new WebGPUSurfaceView(ctx, this); + } + addView(mView); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mView != null) { + mView.layout(0, 0, this.getMeasuredWidth(), this.getMeasuredHeight()); + } + } + + @Override + public void surfaceCreated(Surface surface) { + float density = getResources().getDisplayMetrics().density; + float width = getWidth() / density; + float height = getHeight() / density; + onSurfaceCreate(surface, mContextId, width, height); + } + + @Override + public void surfaceChanged(Surface surface) { + float density = getResources().getDisplayMetrics().density; + float width = getWidth() / density; + float height = getHeight() / density; + onSurfaceChanged(surface, mContextId, width, height); + } + + @Override + public void surfaceDestroyed() { + onSurfaceDestroy(mContextId); + } + + @Override + public void surfaceOffscreen() { + switchToOffscreenSurface(mContextId); + } + + @DoNotStrip + private native void onSurfaceCreate( + Surface surface, + int contextId, + float width, + float height + ); + + @DoNotStrip + private native void onSurfaceChanged( + Surface surface, + int contextId, + float width, + float height + ); + + @DoNotStrip + private native void onSurfaceDestroy(int contextId); + + @DoNotStrip + private native void switchToOffscreenSurface(int contextId); +} diff --git a/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUViewAPI.java b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUViewAPI.java new file mode 100644 index 0000000000..897a8bcb52 --- /dev/null +++ b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUViewAPI.java @@ -0,0 +1,14 @@ +package com.shopify.reactnative.skia; + +import android.view.Surface; + +public interface WebGPUViewAPI { + + void surfaceCreated(Surface surface); + + void surfaceChanged(Surface surface); + + void surfaceDestroyed(); + + void surfaceOffscreen(); +} diff --git a/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUViewManager.java b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUViewManager.java new file mode 100644 index 0000000000..3065299e2b --- /dev/null +++ b/packages/skia/android/src/main/java/com/shopify/reactnative/skia/WebGPUViewManager.java @@ -0,0 +1,58 @@ +package com.shopify.reactnative.skia; + +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.views.view.ReactViewGroup; +import com.facebook.react.views.view.ReactViewManager; +import com.facebook.react.viewmanagers.WebGPUViewManagerDelegate; +import com.facebook.react.viewmanagers.WebGPUViewManagerInterface; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +@ReactModule(name = WebGPUViewManager.NAME) +public class WebGPUViewManager extends ReactViewManager implements WebGPUViewManagerInterface { + + public static final String NAME = "WebGPUView"; + + protected WebGPUViewManagerDelegate mDelegate; + + public WebGPUViewManager() { + mDelegate = new WebGPUViewManagerDelegate(this); + } + + protected WebGPUViewManagerDelegate getDelegate() { + return mDelegate; + } + + @NonNull + @Override + public String getName() { + return NAME; + } + + @NonNull + @Override + public WebGPUView createViewInstance(@NonNull ThemedReactContext context) { + return new WebGPUView(context); + } + + @Override + @ReactProp(name = "transparent") + public void setTransparent(WebGPUView view, boolean value) { + view.setTransparent(value); + } + + @Override + @ReactProp(name = "contextId") + public void setContextId(WebGPUView view, int value) { + view.setContextId(value); + } + + @Override + public void onDropViewInstance(@NonNull ReactViewGroup view) { + super.onDropViewInstance(view); + ((WebGPUView) view).surfaceDestroyed(); + } +} diff --git a/packages/skia/apple/RNSkUIKit.h b/packages/skia/apple/RNSkUIKit.h new file mode 100644 index 0000000000..615fd149fa --- /dev/null +++ b/packages/skia/apple/RNSkUIKit.h @@ -0,0 +1,13 @@ +#pragma once + +#if !TARGET_OS_OSX +#import +#else +#import +#endif + +#if !TARGET_OS_OSX +typedef UIView RNSkPlatformView; +#else +typedef NSView RNSkPlatformView; +#endif diff --git a/packages/skia/apple/SkiaPlatformContext.h b/packages/skia/apple/SkiaPlatformContext.h new file mode 100644 index 0000000000..f755309205 --- /dev/null +++ b/packages/skia/apple/SkiaPlatformContext.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef SK_GRAPHITE + +#include "rnwgpu/PlatformContext.h" + +namespace rnwgpu { + +class SkiaPlatformContext : public PlatformContext { +public: + SkiaPlatformContext() = default; + ~SkiaPlatformContext() = default; + + wgpu::Surface makeSurface(wgpu::Instance instance, void *surface, int width, + int height) override; +}; + +} // namespace rnwgpu + +#endif // SK_GRAPHITE diff --git a/packages/skia/apple/SkiaPlatformContext.mm b/packages/skia/apple/SkiaPlatformContext.mm new file mode 100644 index 0000000000..f2f50acd13 --- /dev/null +++ b/packages/skia/apple/SkiaPlatformContext.mm @@ -0,0 +1,21 @@ +#ifdef SK_GRAPHITE + +#include "SkiaPlatformContext.h" + +#include + +namespace rnwgpu { + +wgpu::Surface SkiaPlatformContext::makeSurface(wgpu::Instance instance, + void *surface, int width, + int height) { + wgpu::SurfaceSourceMetalLayer metalSurfaceDesc; + metalSurfaceDesc.layer = surface; + wgpu::SurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &metalSurfaceDesc; + return instance.CreateSurface(&surfaceDescriptor); +} + +} // namespace rnwgpu + +#endif // SK_GRAPHITE diff --git a/packages/skia/apple/WebGPUMetalView.h b/packages/skia/apple/WebGPUMetalView.h new file mode 100644 index 0000000000..c17a710611 --- /dev/null +++ b/packages/skia/apple/WebGPUMetalView.h @@ -0,0 +1,12 @@ +#pragma once + +#import "RNSkUIKit.h" + +@interface WebGPUMetalView : RNSkPlatformView + +@property (nonatomic, strong) NSNumber *contextId; + +- (void)configure; +- (void)update; + +@end diff --git a/packages/skia/apple/WebGPUMetalView.mm b/packages/skia/apple/WebGPUMetalView.mm new file mode 100644 index 0000000000..b7404646fc --- /dev/null +++ b/packages/skia/apple/WebGPUMetalView.mm @@ -0,0 +1,93 @@ +#import "WebGPUMetalView.h" + +#ifdef SK_GRAPHITE + +#import "webgpu/webgpu_cpp.h" +#import + +#import "rnwgpu/SurfaceRegistry.h" +#import "rnskia/RNDawnContext.h" + +@implementation WebGPUMetalView { + BOOL _isConfigured; +} + +#if !TARGET_OS_OSX ++ (Class)layerClass { + return [CAMetalLayer class]; +} +#else // !TARGET_OS_OSX +- (instancetype)init { + self = [super init]; + if (self) { + self.wantsLayer = true; + self.layer = [CAMetalLayer layer]; + } + return self; +} +#endif // !TARGET_OS_OSX + +- (void)configure { + auto size = self.frame.size; + void *nativeSurface = (__bridge void *)self.layer; + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + auto &dawnContext = RNSkia::DawnContext::getInstance(); + auto gpu = dawnContext.getWGPUInstance(); + + // Create the surface using Dawn's API directly + wgpu::SurfaceSourceMetalLayer metalSurfaceDesc; + metalSurfaceDesc.layer = nativeSurface; + wgpu::SurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &metalSurfaceDesc; + auto surface = gpu.CreateSurface(&surfaceDescriptor); + + registry + .getSurfaceInfoOrCreate([_contextId intValue], gpu, size.width, + size.height) + ->switchToOnscreen(nativeSurface, surface); +} + +- (void)update { + auto size = self.frame.size; + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + auto surfaceInfo = registry.getSurfaceInfo([_contextId intValue]); + if (surfaceInfo) { + surfaceInfo->resize(size.width, size.height); + } +} + +- (void)dealloc { + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + // Remove the surface info from the registry + registry.removeSurfaceInfo([_contextId intValue]); +} + +@end + +#else // SK_GRAPHITE + +// Stub implementation when GRAPHITE is not enabled +@implementation WebGPUMetalView + +#if !TARGET_OS_OSX ++ (Class)layerClass { + return [CAMetalLayer class]; +} +#else // !TARGET_OS_OSX +- (instancetype)init { + self = [super init]; + return self; +} +#endif // !TARGET_OS_OSX + +- (void)configure { + // No-op when GRAPHITE is not enabled +} + +- (void)update { + // No-op when GRAPHITE is not enabled +} + +@end + +#endif // SK_GRAPHITE diff --git a/packages/skia/apple/WebGPUView.h b/packages/skia/apple/WebGPUView.h new file mode 100644 index 0000000000..a3657d8727 --- /dev/null +++ b/packages/skia/apple/WebGPUView.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef RCT_NEW_ARCH_ENABLED + +#import "WebGPUMetalView.h" +#import +#if !TARGET_OS_OSX +#import +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface WebGPUView : RCTViewComponentView +@end + +NS_ASSUME_NONNULL_END + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/packages/skia/apple/WebGPUView.mm b/packages/skia/apple/WebGPUView.mm new file mode 100644 index 0000000000..04f99e2c62 --- /dev/null +++ b/packages/skia/apple/WebGPUView.mm @@ -0,0 +1,77 @@ +#ifdef RCT_NEW_ARCH_ENABLED + +#import "WebGPUView.h" + +#import +#import +#import +#import + +#import "WebGPUMetalView.h" +#import "RCTFabricComponentsPlugins.h" + +using namespace facebook::react; + +@implementation WebGPUView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + } + return self; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider { + return concreteComponentDescriptorProvider(); +} + +- (void)prepareForRecycle { + [super prepareForRecycle]; + /* + It's important to destroy the Metal Layer before releasing a view + to the recycled pool to prevent displaying outdated content from + the last usage in the new context. + */ + self.contentView = nil; +} + +- (WebGPUMetalView *)getContentView { + if (!self.contentView) { + self.contentView = [WebGPUMetalView new]; + } + return (WebGPUMetalView *)self.contentView; +} + +- (void)updateProps:(const Props::Shared &)props + oldProps:(const Props::Shared &)oldProps { + const auto &oldViewProps = + *std::static_pointer_cast(_props); + const auto &newViewProps = + *std::static_pointer_cast(props); + + if (newViewProps.contextId != oldViewProps.contextId) { + /* + The context is set only once during mounting the component + and never changes because it isn't available for users to modify. + */ + WebGPUMetalView *metalView = [WebGPUMetalView new]; + self.contentView = metalView; + [metalView setContextId:@(newViewProps.contextId)]; + [metalView configure]; + } + + [super updateProps:props oldProps:oldProps]; +} + +- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics + oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics { + [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; + [(WebGPUMetalView *)self.contentView update]; +} + +@end + +Class WebGPUViewCls(void) { return WebGPUView.class; } + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/packages/skia/cpp/rnskia/RNSkManager.cpp b/packages/skia/cpp/rnskia/RNSkManager.cpp index 1ba913a8d0..4a824e22be 100644 --- a/packages/skia/cpp/rnskia/RNSkManager.cpp +++ b/packages/skia/cpp/rnskia/RNSkManager.cpp @@ -14,6 +14,8 @@ #ifdef SK_GRAPHITE #include "RNDawnContext.h" #include "rnwgpu/api/GPU.h" +#include "rnwgpu/api/GPUUncapturedErrorEvent.h" +#include "rnwgpu/api/RNWebGPU.h" #include "rnwgpu/api/descriptors/GPUBufferUsage.h" #include "rnwgpu/api/descriptors/GPUColorWrite.h" #include "rnwgpu/api/descriptors/GPUMapMode.h" @@ -73,8 +75,9 @@ void RNSkManager::installBindings() { jsi::Object::createFromHostObject(*_jsRuntime, _viewApi)); #ifdef SK_GRAPHITE - // Install WebGPU GPU constructor + // Install WebGPU constructors rnwgpu::GPU::installConstructor(*_jsRuntime); + rnwgpu::GPUUncapturedErrorEvent::installConstructor(*_jsRuntime); // Create and expose navigator.gpu using DawnContext's instance auto &dawnContext = DawnContext::getInstance(); auto gpu = @@ -103,9 +106,13 @@ void RNSkManager::installBindings() { rnwgpu::GPUMapMode::create(*_jsRuntime)); _jsRuntime->global().setProperty(*_jsRuntime, "GPUShaderStage", rnwgpu::GPUShaderStage::create(*_jsRuntime)); - _jsRuntime->global().setProperty( - *_jsRuntime, "GPUTextureUsage", - rnwgpu::GPUTextureUsage::create(*_jsRuntime)); + _jsRuntime->global().setProperty(*_jsRuntime, "GPUTextureUsage", + rnwgpu::GPUTextureUsage::create(*_jsRuntime)); + + // Install RNWebGPU global object for WebGPU Canvas support + auto rnWebGPU = std::make_shared(gpu, nullptr); + _jsRuntime->global().setProperty(*_jsRuntime, "RNWebGPU", + rnwgpu::RNWebGPU::create(*_jsRuntime, rnWebGPU)); #endif } } // namespace RNSkia diff --git a/packages/skia/cpp/rnwgpu/Canvas.h b/packages/skia/cpp/rnwgpu/Canvas.h new file mode 100644 index 0000000000..b92aa0e9cd --- /dev/null +++ b/packages/skia/cpp/rnwgpu/Canvas.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include "jsi2/NativeObject.h" + +namespace rnwgpu { + +namespace jsi = facebook::jsi; + +class Canvas : public NativeObject { +public: + static constexpr const char *CLASS_NAME = "Canvas"; + + Canvas(void *nativeSurface, int width, int height) + : NativeObject(CLASS_NAME), _nativeSurface(nativeSurface), _width(width), + _height(height), _clientWidth(width), _clientHeight(height) {} + + void *getNativeSurface() { return _nativeSurface; } + + int getWidth() { return _width; } + int getHeight() { return _height; } + + int getClientWidth() { return _clientWidth; } + int getClientHeight() { return _clientHeight; } + + void setClientWidth(int width) { _clientWidth = width; } + void setClientHeight(int height) { _clientHeight = height; } + + static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { + installGetter(runtime, prototype, "width", &Canvas::getWidth); + installGetter(runtime, prototype, "height", &Canvas::getHeight); + installGetter(runtime, prototype, "clientWidth", &Canvas::getClientWidth); + installGetter(runtime, prototype, "clientHeight", &Canvas::getClientHeight); + } + +private: + void *_nativeSurface; + int _width; + int _height; + int _clientWidth; + int _clientHeight; +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/PlatformContext.h b/packages/skia/cpp/rnwgpu/PlatformContext.h new file mode 100644 index 0000000000..988ffb5ab9 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/PlatformContext.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "webgpu/webgpu_cpp.h" + +namespace rnwgpu { + +class PlatformContext { +public: + PlatformContext() = default; + virtual ~PlatformContext() = default; + + virtual wgpu::Surface makeSurface(wgpu::Instance instance, void *surface, + int width, int height) = 0; +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/SurfaceRegistry.h b/packages/skia/cpp/rnwgpu/SurfaceRegistry.h new file mode 100644 index 0000000000..e41de864a6 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/SurfaceRegistry.h @@ -0,0 +1,229 @@ +#pragma once + +#include +#include +#include +#include + +#include "webgpu/webgpu_cpp.h" + +namespace rnwgpu { + +struct NativeInfo { + void *nativeSurface; + int width; + int height; +}; + +struct Size { + int width; + int height; +}; + +class SurfaceInfo { +public: + SurfaceInfo(wgpu::Instance gpu, int width, int height) + : gpu(std::move(gpu)), width(width), height(height) {} + + ~SurfaceInfo() { surface = nullptr; } + + void reconfigure(int newWidth, int newHeight) { + std::unique_lock lock(_mutex); + config.width = newWidth; + config.height = newHeight; + _configure(); + } + + void configure(wgpu::SurfaceConfiguration &newConfig) { + std::unique_lock lock(_mutex); + config = newConfig; + config.width = width; + config.height = height; + config.presentMode = wgpu::PresentMode::Fifo; + _configure(); + } + + void unconfigure() { + std::unique_lock lock(_mutex); + if (surface) { + surface.Unconfigure(); + } else { + texture = nullptr; + } + } + + void *switchToOffscreen() { + std::unique_lock lock(_mutex); + // We only do this if the onscreen surface is configured. + auto isConfigured = config.device != nullptr; + if (isConfigured) { + wgpu::TextureDescriptor textureDesc; + textureDesc.usage = wgpu::TextureUsage::RenderAttachment | + wgpu::TextureUsage::CopySrc | + wgpu::TextureUsage::TextureBinding; + textureDesc.format = config.format; + textureDesc.size.width = config.width; + textureDesc.size.height = config.height; + texture = config.device.CreateTexture(&textureDesc); + } + surface = nullptr; + return nativeSurface; + } + + void switchToOnscreen(void *newNativeSurface, wgpu::Surface newSurface) { + std::unique_lock lock(_mutex); + nativeSurface = newNativeSurface; + surface = std::move(newSurface); + // If we are comming from an offscreen context, we need to configure the new + // surface + if (texture != nullptr) { + config.usage = config.usage | wgpu::TextureUsage::CopyDst; + _configure(); + // We flush the offscreen texture to the onscreen one + wgpu::CommandEncoderDescriptor encoderDesc; + auto device = config.device; + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(&encoderDesc); + + wgpu::TexelCopyTextureInfo sourceTexture = {}; + sourceTexture.texture = texture; + + wgpu::TexelCopyTextureInfo destinationTexture = {}; + wgpu::SurfaceTexture surfaceTexture; + surface.GetCurrentTexture(&surfaceTexture); + destinationTexture.texture = surfaceTexture.texture; + + wgpu::Extent3D size = {sourceTexture.texture.GetWidth(), + sourceTexture.texture.GetHeight(), + sourceTexture.texture.GetDepthOrArrayLayers()}; + + encoder.CopyTextureToTexture(&sourceTexture, &destinationTexture, &size); + + wgpu::CommandBuffer commands = encoder.Finish(); + wgpu::Queue queue = device.GetQueue(); + queue.Submit(1, &commands); + surface.Present(); + texture = nullptr; + } + } + + void resize(int newWidth, int newHeight) { + std::unique_lock lock(_mutex); + width = newWidth; + height = newHeight; + } + + void present() { + std::unique_lock lock(_mutex); + if (surface) { + surface.Present(); + } + } + + wgpu::Texture getCurrentTexture() { + std::shared_lock lock(_mutex); + if (surface) { + wgpu::SurfaceTexture surfaceTexture; + surface.GetCurrentTexture(&surfaceTexture); + return surfaceTexture.texture; + } else { + return texture; + } + } + + NativeInfo getNativeInfo() { + std::shared_lock lock(_mutex); + return {.nativeSurface = nativeSurface, .width = width, .height = height}; + } + + Size getSize() { + std::shared_lock lock(_mutex); + return {.width = width, .height = height}; + } + + wgpu::SurfaceConfiguration getConfig() { + std::shared_lock lock(_mutex); + return config; + } + + wgpu::Device getDevice() { + std::shared_lock lock(_mutex); + return config.device; + } + +private: + void _configure() { + if (surface) { + surface.Configure(&config); + } else { + wgpu::TextureDescriptor textureDesc; + textureDesc.format = config.format; + textureDesc.size.width = config.width; + textureDesc.size.height = config.height; + textureDesc.usage = wgpu::TextureUsage::RenderAttachment | + wgpu::TextureUsage::CopySrc | + wgpu::TextureUsage::TextureBinding; + texture = config.device.CreateTexture(&textureDesc); + } + } + + mutable std::shared_mutex _mutex; + void *nativeSurface = nullptr; + wgpu::Surface surface = nullptr; + wgpu::Texture texture = nullptr; + wgpu::Instance gpu; + wgpu::SurfaceConfiguration config; + int width; + int height; +}; + +class SurfaceRegistry { +public: + static SurfaceRegistry &getInstance() { + static SurfaceRegistry instance; + return instance; + } + + SurfaceRegistry(const SurfaceRegistry &) = delete; + SurfaceRegistry &operator=(const SurfaceRegistry &) = delete; + + std::shared_ptr getSurfaceInfo(int id) { + std::shared_lock lock(_mutex); + auto it = _registry.find(id); + if (it != _registry.end()) { + return it->second; + } + return nullptr; + } + + void removeSurfaceInfo(int id) { + std::unique_lock lock(_mutex); + _registry.erase(id); + } + + std::shared_ptr addSurfaceInfo(int id, wgpu::Instance gpu, + int width, int height) { + std::unique_lock lock(_mutex); + auto info = std::make_shared(gpu, width, height); + _registry[id] = info; + return info; + } + + std::shared_ptr + getSurfaceInfoOrCreate(int id, wgpu::Instance gpu, int width, int height) { + std::unique_lock lock(_mutex); + auto it = _registry.find(id); + if (it != _registry.end()) { + return it->second; + } + auto info = std::make_shared(gpu, width, height); + _registry[id] = info; + return info; + } + +private: + SurfaceRegistry() = default; + mutable std::shared_mutex _mutex; + std::unordered_map> _registry; +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp b/packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp index e252f7eebf..195b143336 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp +++ b/packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp @@ -10,6 +10,9 @@ #include "Convertors.h" #include "GPUFeatures.h" +#include "GPUInternalError.h" +#include "GPUOutOfMemoryError.h" +#include "GPUValidationError.h" #include "jsi2/JSIConverter.h" namespace rnwgpu { @@ -73,6 +76,29 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( std::string(message.data, message.length) : "no message"; fprintf(stderr, "%s", fullMessage.c_str()); + + // Look up the GPUDevice from the registry and notify it + auto *gpuDevice = GPUDeviceRegistry::getInstance().getDevice(device.Get()); + if (gpuDevice != nullptr) { + std::string messageStr = + message.length > 0 ? std::string(message.data, message.length) : ""; + + GPUErrorVariant error; + switch (type) { + case wgpu::ErrorType::Validation: + error = std::make_shared(messageStr); + break; + case wgpu::ErrorType::OutOfMemory: + error = std::make_shared(messageStr); + break; + case wgpu::ErrorType::Internal: + case wgpu::ErrorType::Unknown: + default: + error = std::make_shared(messageStr); + break; + } + gpuDevice->notifyUncapturedError(std::move(error)); + } }); std::string label = descriptor.has_value() ? descriptor.value()->label.value_or("") : ""; diff --git a/packages/skia/cpp/rnwgpu/api/GPUCanvasContext.cpp b/packages/skia/cpp/rnwgpu/api/GPUCanvasContext.cpp new file mode 100644 index 0000000000..217d8445d3 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/GPUCanvasContext.cpp @@ -0,0 +1,64 @@ +#include "GPUCanvasContext.h" +#include "Convertors.h" +#include + +#ifdef __APPLE__ +namespace dawn::native::metal { + +void WaitForCommandsToBeScheduled(WGPUDevice device); + +} +#endif + +namespace rnwgpu { + +void GPUCanvasContext::configure( + std::shared_ptr configuration) { + Convertor conv; + wgpu::SurfaceConfiguration surfaceConfiguration; + surfaceConfiguration.device = configuration->device->get(); + if (configuration->viewFormats.has_value()) { + if (!conv(surfaceConfiguration.viewFormats, + surfaceConfiguration.viewFormatCount, + configuration->viewFormats.value())) { + throw std::runtime_error("Error with SurfaceConfiguration"); + } + } + if (!conv(surfaceConfiguration.usage, configuration->usage) || + !conv(surfaceConfiguration.format, configuration->format)) { + throw std::runtime_error("Error with SurfaceConfiguration"); + } + +#ifdef __APPLE__ + surfaceConfiguration.alphaMode = configuration->alphaMode; +#endif + surfaceConfiguration.presentMode = wgpu::PresentMode::Fifo; + _surfaceInfo->configure(surfaceConfiguration); +} + +void GPUCanvasContext::unconfigure() {} + +std::shared_ptr GPUCanvasContext::getCurrentTexture() { + auto prevSize = _surfaceInfo->getConfig(); + auto width = _canvas->getWidth(); + auto height = _canvas->getHeight(); + auto sizeHasChanged = prevSize.width != width || prevSize.height != height; + if (sizeHasChanged) { + _surfaceInfo->reconfigure(width, height); + } + auto texture = _surfaceInfo->getCurrentTexture(); + return std::make_shared(texture, ""); +} + +void GPUCanvasContext::present() { +#ifdef __APPLE__ + dawn::native::metal::WaitForCommandsToBeScheduled( + _surfaceInfo->getDevice().Get()); +#endif + auto size = _surfaceInfo->getSize(); + _canvas->setClientWidth(size.width); + _canvas->setClientHeight(size.height); + _surfaceInfo->present(); +} + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUCanvasContext.h b/packages/skia/cpp/rnwgpu/api/GPUCanvasContext.h new file mode 100644 index 0000000000..b005bbbe76 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/GPUCanvasContext.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +#include "descriptors/Unions.h" + +#include "webgpu/webgpu_cpp.h" + +#include "jsi2/NativeObject.h" + +#include "rnwgpu/Canvas.h" +#include "GPU.h" +#include "descriptors/GPUCanvasConfiguration.h" +#include "GPUTexture.h" +#include "rnwgpu/SurfaceRegistry.h" + +namespace rnwgpu { + +namespace jsi = facebook::jsi; + +class GPUCanvasContext : public NativeObject { +public: + static constexpr const char *CLASS_NAME = "GPUCanvasContext"; + + GPUCanvasContext(std::shared_ptr gpu, int contextId, int width, + int height) + : NativeObject(CLASS_NAME), _gpu(std::move(gpu)) { + _canvas = std::make_shared(nullptr, width, height); + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + _surfaceInfo = + registry.getSurfaceInfoOrCreate(contextId, _gpu->get(), width, height); + } + +public: + std::string getBrand() { return CLASS_NAME; } + + std::shared_ptr getCanvas() { return _canvas; } + + static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { + installGetter(runtime, prototype, "__brand", &GPUCanvasContext::getBrand); + installGetter(runtime, prototype, "canvas", &GPUCanvasContext::getCanvas); + installMethod(runtime, prototype, "configure", + &GPUCanvasContext::configure); + installMethod(runtime, prototype, "unconfigure", + &GPUCanvasContext::unconfigure); + installMethod(runtime, prototype, "getCurrentTexture", + &GPUCanvasContext::getCurrentTexture); + installMethod(runtime, prototype, "present", &GPUCanvasContext::present); + } + + inline const wgpu::Surface get() { return nullptr; } + void configure(std::shared_ptr configuration); + void unconfigure(); + std::shared_ptr getCurrentTexture(); + void present(); + +private: + std::shared_ptr _canvas; + std::shared_ptr _surfaceInfo; + std::shared_ptr _gpu; +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUDevice.cpp b/packages/skia/cpp/rnwgpu/api/GPUDevice.cpp index edc181b38c..19e2b1d977 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUDevice.cpp +++ b/packages/skia/cpp/rnwgpu/api/GPUDevice.cpp @@ -431,4 +431,56 @@ async::AsyncTaskHandle GPUDevice::getLost() { _lostHandle = handle; return handle; } +void GPUDevice::addEventListener(std::string type, jsi::Function callback) { + std::lock_guard lock(_listenersMutex); + auto sharedCallback = std::make_shared(std::move(callback)); + _eventListeners[type].push_back(sharedCallback); +} + +void GPUDevice::removeEventListener(std::string type, jsi::Function callback) { + std::lock_guard lock(_listenersMutex); + auto it = _eventListeners.find(type); + if (it != _eventListeners.end()) { + auto &listeners = it->second; + // Remove the last listener of this type (simple approach since we can't + // easily compare jsi::Function objects) + if (!listeners.empty()) { + listeners.pop_back(); + } + } +} + +void GPUDevice::notifyUncapturedError(GPUErrorVariant error) { + auto runtime = getCreationRuntime(); + if (runtime == nullptr) { + return; + } + + std::vector> listeners; + { + std::lock_guard lock(_listenersMutex); + auto it = _eventListeners.find("uncapturederror"); + if (it != _eventListeners.end()) { + listeners = it->second; + } + } + + if (listeners.empty()) { + return; + } + + // Create the event object + auto event = std::make_shared(std::move(error)); + auto eventValue = GPUUncapturedErrorEvent::create(*runtime, event); + + // Call all listeners + for (const auto &listener : listeners) { + try { + listener->call(*runtime, eventValue); + } catch (const std::exception &e) { + fprintf(stderr, "Error in uncapturederror listener: %s\n", e.what()); + } + } +} + } // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUDevice.h b/packages/skia/cpp/rnwgpu/api/GPUDevice.h index bd40d67912..4c10eee683 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUDevice.h +++ b/packages/skia/cpp/rnwgpu/api/GPUDevice.h @@ -1,11 +1,15 @@ #pragma once +#include +#include #include +#include #include #include #include #include #include +#include #include "descriptors/Unions.h" @@ -16,6 +20,8 @@ #include "webgpu/webgpu_cpp.h" +#include "GPUUncapturedErrorEvent.h" + #include "GPUBindGroup.h" #include "GPUBindGroupLayout.h" #include "GPUBuffer.h" @@ -51,6 +57,39 @@ namespace rnwgpu { namespace jsi = facebook::jsi; +// Forward declaration +class GPUDevice; + +// Static registry to map wgpu::Device handles to GPUDevice instances +class GPUDeviceRegistry { +public: + static GPUDeviceRegistry &getInstance() { + static GPUDeviceRegistry instance; + return instance; + } + + void registerDevice(WGPUDevice handle, GPUDevice *device) { + std::lock_guard lock(_mutex); + _devices[handle] = device; + } + + void unregisterDevice(WGPUDevice handle) { + std::lock_guard lock(_mutex); + _devices.erase(handle); + } + + GPUDevice *getDevice(WGPUDevice handle) { + std::lock_guard lock(_mutex); + auto it = _devices.find(handle); + return it != _devices.end() ? it->second : nullptr; + } + +private: + GPUDeviceRegistry() = default; + std::mutex _mutex; + std::map _devices; +}; + class GPUDevice : public NativeObject { public: static constexpr const char *CLASS_NAME = "GPUDevice"; @@ -59,7 +98,15 @@ class GPUDevice : public NativeObject { std::shared_ptr async, std::string label) : NativeObject(CLASS_NAME), _instance(instance), _async(async), - _label(label) {} + _label(label) { + // Register this device in the global registry + GPUDeviceRegistry::getInstance().registerDevice(_instance.Get(), this); + } + + ~GPUDevice() { + // Unregister from the global registry + GPUDeviceRegistry::getInstance().unregisterDevice(_instance.Get()); + } public: std::string getBrand() { return CLASS_NAME; } @@ -105,6 +152,11 @@ class GPUDevice : public NativeObject { void notifyDeviceLost(wgpu::DeviceLostReason reason, std::string message); void forceLossForTesting(); + // Event listener methods + void addEventListener(std::string type, jsi::Function callback); + void removeEventListener(std::string type, jsi::Function callback); + void notifyUncapturedError(GPUErrorVariant error); + std::string getLabel() { return _label; } void setLabel(const std::string &label) { _label = label; @@ -155,6 +207,10 @@ class GPUDevice : public NativeObject { &GPUDevice::setLabel); installMethod(runtime, prototype, "forceLossForTesting", &GPUDevice::forceLossForTesting); + installMethod(runtime, prototype, "addEventListener", + &GPUDevice::addEventListener); + installMethod(runtime, prototype, "removeEventListener", + &GPUDevice::removeEventListener); } inline const wgpu::Device get() { return _instance; } @@ -169,6 +225,11 @@ class GPUDevice : public NativeObject { std::shared_ptr _lostInfo; bool _lostSettled = false; std::optional _lostResolve; + + // Event listeners storage + std::mutex _listenersMutex; + std::map>> + _eventListeners; }; } // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h b/packages/skia/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h new file mode 100644 index 0000000000..baa23da5b5 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include "jsi2/NativeObject.h" + +#include "GPUError.h" +#include "GPUInternalError.h" +#include "GPUOutOfMemoryError.h" +#include "GPUValidationError.h" + +namespace rnwgpu { + +namespace jsi = facebook::jsi; + +using GPUErrorVariant = + std::variant, + std::shared_ptr, + std::shared_ptr>; + +class GPUUncapturedErrorEvent : public NativeObject { +public: + static constexpr const char *CLASS_NAME = "GPUUncapturedErrorEvent"; + + explicit GPUUncapturedErrorEvent(GPUErrorVariant error) + : NativeObject(CLASS_NAME), _error(std::move(error)) {} + + GPUErrorVariant getError() { return _error; } + + static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { + installGetter(runtime, prototype, "error", + &GPUUncapturedErrorEvent::getError); + } + +private: + GPUErrorVariant _error; +}; + +} // namespace rnwgpu + +namespace rnwgpu { + +template <> struct JSIConverter { + static jsi::Value toJSI(jsi::Runtime &runtime, GPUErrorVariant arg) { + return std::visit( + [&runtime](auto &&error) -> jsi::Value { + using T = std::decay_t; + return JSIConverter::toJSI(runtime, error); + }, + arg); + } + + static GPUErrorVariant fromJSI(jsi::Runtime &runtime, const jsi::Value &arg, + bool outOfBounds) { + throw std::runtime_error("GPUErrorVariant::fromJSI not implemented"); + } +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/RNWebGPU.h b/packages/skia/cpp/rnwgpu/api/RNWebGPU.h new file mode 100644 index 0000000000..ace53b8980 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/RNWebGPU.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include "jsi2/NativeObject.h" + +#include "rnwgpu/Canvas.h" +#include "GPU.h" +#include "GPUCanvasContext.h" +#include "rnwgpu/PlatformContext.h" + +namespace rnwgpu { + +namespace jsi = facebook::jsi; + +class RNWebGPU : public NativeObject { +public: + static constexpr const char *CLASS_NAME = "RNWebGPU"; + + explicit RNWebGPU(std::shared_ptr gpu, + std::shared_ptr platformContext) + : NativeObject(CLASS_NAME), _gpu(gpu), _platformContext(platformContext) { + } + + std::shared_ptr getGPU() { return _gpu; } + + bool getFabric() { return true; } + + std::shared_ptr + MakeWebGPUCanvasContext(int contextId, float width, float height) { + auto ctx = + std::make_shared(_gpu, contextId, width, height); + return ctx; + } + + std::shared_ptr getNativeSurface(int contextId) { + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + auto info = registry.getSurfaceInfo(contextId); + if (info == nullptr) { + return std::make_shared(nullptr, 0, 0); + } + auto nativeInfo = info->getNativeInfo(); + return std::make_shared(nativeInfo.nativeSurface, nativeInfo.width, + nativeInfo.height); + } + + static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { + installGetter(runtime, prototype, "fabric", &RNWebGPU::getFabric); + installGetter(runtime, prototype, "gpu", &RNWebGPU::getGPU); + installMethod(runtime, prototype, "getNativeSurface", + &RNWebGPU::getNativeSurface); + installMethod(runtime, prototype, "MakeWebGPUCanvasContext", + &RNWebGPU::MakeWebGPUCanvasContext); + } + +private: + std::shared_ptr _gpu; + std::shared_ptr _platformContext; +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h index 64f7e42056..aeadee0738 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h @@ -45,6 +45,10 @@ template <> struct JSIConverter> { } else if (obj.hasNativeState(runtime)) { result->textureView = obj.getNativeState(runtime); + } else if (obj.hasNativeState(runtime)) { + auto binding = std::make_shared(); + binding->buffer = obj.getNativeState(runtime); + result->buffer = binding; } else { result->buffer = JSIConverter< std::shared_ptr>::fromJSI(runtime, diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h new file mode 100644 index 0000000000..9cccc66767 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include + +#include "webgpu/webgpu_cpp.h" + +#include "jsi2/JSIConverter.h" + +#include "GPUDevice.h" + +namespace jsi = facebook::jsi; + +namespace rnwgpu { + +struct GPUCanvasConfiguration { + std::shared_ptr device; // GPUDevice + wgpu::TextureFormat format; // GPUTextureFormat + std::optional usage; // GPUTextureUsageFlags + std::optional> + viewFormats; // Iterable + wgpu::CompositeAlphaMode alphaMode = wgpu::CompositeAlphaMode::Opaque; +}; + +} // namespace rnwgpu + +namespace rnwgpu { + +template <> +struct JSIConverter> { + static std::shared_ptr + fromJSI(jsi::Runtime &runtime, const jsi::Value &arg, bool outOfBounds) { + auto result = std::make_unique(); + if (!outOfBounds && arg.isObject()) { + auto value = arg.getObject(runtime); + if (value.hasProperty(runtime, "device")) { + auto prop = value.getProperty(runtime, "device"); + result->device = JSIConverter>::fromJSI( + runtime, prop, false); + } + if (value.hasProperty(runtime, "format")) { + auto prop = value.getProperty(runtime, "format"); + result->format = + JSIConverter::fromJSI(runtime, prop, false); + } + if (value.hasProperty(runtime, "usage")) { + auto prop = value.getProperty(runtime, "usage"); + result->usage = + JSIConverter>::fromJSI(runtime, prop, false); + } + if (value.hasProperty(runtime, "viewFormats")) { + auto prop = value.getProperty(runtime, "viewFormats"); + result->viewFormats = JSIConverter< + std::optional>>::fromJSI(runtime, + prop, + false); + } + if (value.hasProperty(runtime, "alphaMode")) { + auto prop = value.getProperty(runtime, "alphaMode") + .asString(runtime) + .utf8(runtime); + if (prop == "premultiplied") { + result->alphaMode = wgpu::CompositeAlphaMode::Premultiplied; + } + } + } + + return result; + } + static jsi::Value toJSI(jsi::Runtime &runtime, + std::shared_ptr arg) { + throw std::runtime_error("Invalid GPUCanvasConfiguration::toJSI()"); + } +}; + +} // namespace rnwgpu diff --git a/packages/skia/package.json b/packages/skia/package.json index 6de648acc1..6bcc6250f4 100644 --- a/packages/skia/package.json +++ b/packages/skia/package.json @@ -24,12 +24,12 @@ "skia-graphite": { "version": "m142b", "checksums": { - "android-armeabi-v7a": "3e40f44c804194fa0983c46903f834e8f834c6dd96534c7fa1b4ebb4f409527d", - "android-arm64-v8a": "d6c3035449fcf7ef13369b0d6c4ef41f3177d15074eeb195201e0ba4cfb2f527", - "android-x86": "6dc229847420b8a43c15098cdcb4fe45b2df20a38ad69f37e0aa91c832499c2a", - "android-x86_64": "ecae351d98af3b175d40d894520c2e3263034276bb0b0e9d2f9c19ba7dc046fa", - "apple-ios-xcframeworks": "6f60f03faaeebfb798615d09715040790ff95e75bf95028893ced6f514ab5196", - "apple-macos-xcframeworks": "07467cd1778053537528ed91c7a35d116e1a4644819f0aad8e06e7d884591dea" + "android-armeabi-v7a": "b3db57a758482d479d07a5a31dd46492b5ca58de1cdcb9fbba1f71879561057a", + "android-arm64-v8a": "099cb2491723f05bcf44fc2af7b4d33d80b3768dc4be8609d7bd2102efce8e4f", + "android-x86": "bcbeb88d1e484b64f6f87722405a9d0319ba1795b85ef3ddd3a673412dfd505f", + "android-x86_64": "285448b69b18d619f6d468461c928931ab85b73dc90c4675ffb60a336c2c78d9", + "apple-ios-xcframeworks": "5efb7b78b3e748d05f1a79ff4be4561af1a49ef178a46ace319eaa75ece0f27f", + "apple-macos-xcframeworks": "1ded51aa285b1925e2ead8a118a01a2775e0c3f940e01263262358e58fdb9b77" } }, "description": "High-performance React Native Graphics using Skia", @@ -164,7 +164,8 @@ }, "ios": { "componentProvider": { - "SkiaPictureView": "SkiaPictureView" + "SkiaPictureView": "SkiaPictureView", + "WebGPUView": "WebGPUView" } } }, diff --git a/packages/skia/src/__tests__/snapshots/text/text-path-arabic-component-node.png b/packages/skia/src/__tests__/snapshots/text/text-path-arabic-component-node.png new file mode 100644 index 0000000000..62c7ba3183 Binary files /dev/null and b/packages/skia/src/__tests__/snapshots/text/text-path-arabic-component-node.png differ diff --git a/packages/skia/src/external/reanimated/useVideo.ts b/packages/skia/src/external/reanimated/useVideo.ts index 6a78a687e1..c05b628de7 100644 --- a/packages/skia/src/external/reanimated/useVideo.ts +++ b/packages/skia/src/external/reanimated/useVideo.ts @@ -22,7 +22,7 @@ const copyFrameOnAndroid = (currentFrame: SharedValue) => { if (Platform.OS === "android") { const tex = currentFrame.value; if (tex) { - currentFrame.value = tex.makeNonTextureImage(); + currentFrame.value = tex; //.makeNonTextureImage(); tex.dispose(); } } @@ -32,9 +32,6 @@ const setFrame = (video: Video, currentFrame: SharedValue) => { "worklet"; const img = video.nextImage(); if (img) { - if (currentFrame.value) { - currentFrame.value.dispose(); - } currentFrame.value = img; copyFrameOnAndroid(currentFrame); } diff --git a/packages/skia/src/specs/WebGPUViewNativeComponent.ts b/packages/skia/src/specs/WebGPUViewNativeComponent.ts new file mode 100644 index 0000000000..7a601a307b --- /dev/null +++ b/packages/skia/src/specs/WebGPUViewNativeComponent.ts @@ -0,0 +1,11 @@ +import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent"; +import type { Int32 } from "react-native/Libraries/Types/CodegenTypes"; +import type { ViewProps } from "react-native"; + +export interface NativeProps extends ViewProps { + contextId: Int32; + transparent: boolean; +} + +// eslint-disable-next-line import/no-default-export +export default codegenNativeComponent("WebGPUView"); diff --git a/packages/skia/src/views/WebGPUCanvas.tsx b/packages/skia/src/views/WebGPUCanvas.tsx new file mode 100644 index 0000000000..9b4e4c73ea --- /dev/null +++ b/packages/skia/src/views/WebGPUCanvas.tsx @@ -0,0 +1,110 @@ +import React, { useImperativeHandle, useRef, useState } from "react"; +import type { ViewProps } from "react-native"; +import { View, Platform } from "react-native"; + +import WebGPUNativeView from "../specs/WebGPUViewNativeComponent"; + +let CONTEXT_COUNTER = 1; +function generateContextId() { + return CONTEXT_COUNTER++; +} + +declare global { + // eslint-disable-next-line no-var + var RNWebGPU: { + gpu: GPU; + fabric: boolean; + getNativeSurface: (contextId: number) => NativeCanvas; + MakeWebGPUCanvasContext: ( + contextId: number, + width: number, + height: number + ) => RNCanvasContext; + }; +} + +type SurfacePointer = bigint; + +export interface NativeCanvas { + surface: SurfacePointer; + width: number; + height: number; + clientWidth: number; + clientHeight: number; +} + +export type RNCanvasContext = GPUCanvasContext & { + present: () => void; +}; + +export interface WebGPUCanvasRef { + getContextId: () => number; + getContext(contextName: "webgpu"): RNCanvasContext | null; + getNativeSurface: () => NativeCanvas; +} + +interface WebGPUCanvasProps extends ViewProps { + transparent?: boolean; + ref?: React.Ref; +} + +export const WebGPUCanvas = ({ + transparent, + ref, + ...props +}: WebGPUCanvasProps) => { + const viewRef = useRef(null); + const [contextId] = useState(() => generateContextId()); + + useImperativeHandle(ref, () => ({ + getContextId: () => contextId, + getNativeSurface: () => { + if (typeof RNWebGPU === "undefined") { + throw new Error( + "[WebGPU] RNWebGPU is not available. Make sure SK_GRAPHITE is enabled." + ); + } + return RNWebGPU.getNativeSurface(contextId); + }, + getContext(contextName: "webgpu"): RNCanvasContext | null { + if (contextName !== "webgpu") { + throw new Error(`[WebGPU] Unsupported context: ${contextName}`); + } + if (!viewRef.current) { + throw new Error("[WebGPU] Cannot get context before mount"); + } + if (typeof RNWebGPU === "undefined") { + throw new Error( + "[WebGPU] RNWebGPU is not available. Make sure SK_GRAPHITE is enabled." + ); + } + // getBoundingClientRect became stable in RN 0.83 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const view = viewRef.current as any; + const size = + "getBoundingClientRect" in view + ? view.getBoundingClientRect() + : view.unstable_getBoundingClientRect(); + return RNWebGPU.MakeWebGPUCanvasContext( + contextId, + size.width, + size.height + ); + }, + })); + + // WebGPU Canvas is not supported on web + if (Platform.OS === "web") { + return ; + } + + return ( + + + + ); +}; diff --git a/packages/skia/src/views/index.ts b/packages/skia/src/views/index.ts index 1760ffae2e..56181da28c 100644 --- a/packages/skia/src/views/index.ts +++ b/packages/skia/src/views/index.ts @@ -1,2 +1,3 @@ export * from "./SkiaPictureView"; export * from "./types"; +export * from "./WebGPUCanvas";