diff --git a/CHANGELOG.md b/CHANGELOG.md index 456a5b9eb..b1e068bb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - New `GpuTarget` class (`rhi/yup_GpuTarget.h`): low-level render-pass-only offscreen GPU surface (`create`, `beginRenderPass`, `asTexture`, `asImage`, `readPixels`). Its backing texture is allocated from the context's main render context, so it does not reserve a dedicated `rive::gpu::RenderContext` — use it for custom `GpuPipeline` work (e.g. post-process passes) that needs no 2D drawing. - New `GpuCanvas` class (`rhi/yup_GpuCanvas.h`): consolidated backend-agnostic offscreen GPU surface that now composes a `GpuTarget` (over a `RenderableTarget`) and creates a non-owning `Graphics` lazily only when 2D drawing is requested. +#### RHI module extraction & GpuDevice + +- **New `yup_rhi` module**: the GPU abstraction layer extracted from `yup_graphics` into its own module (depends on `yup_core`, `yup_shading`, `rive_renderer`). All RHI classes (`GpuFrame`, `GpuPipeline`, `GpuBuffer`, `GpuTexture`, `GpuTarget`, `GpuRenderPass`, `GpuPipelineCache`) now live in `yup_rhi`. `yup_graphics` depends on `yup_rhi` for GPU access. +- **`GpuDevice`**: new reference-counted GPU device abstraction (was `GpuContext`). Owns the native GPU device and command queue without requiring a window — can be used for headless GPU compute (e.g. audio DSP on the GPU). Created via `GpuDevice::create(GpuPlatform, Options)`. All RHI factory methods (`GpuFrame::begin`, `GpuPipeline::compile*`, `GpuBuffer::create`, `GpuTarget::create`) now take `GpuDevice::Ptr` for safe shared ownership. +- **`GpuPlatform`** enum: standalone platform enum (`Headless`, `Metal`, `Direct3D`, `OpenGL`, `OpenGLES`, `WebGPU`) replacing the nested `GpuDevice::Api`. `GraphicsContext::getPlatform()` returns it directly — no typedef alias. +- **`GpuColor`** struct (`rhi/yup_GpuTypes.h`): lightweight 4-component GPU color for render options. Implicitly constructable from any type with `getRedFloat()`/`getGreenFloat()`/`getBlueFloat()`/`getAlphaFloat()` (e.g. `yup::Color`), so `GpuRenderOptions { true, Colors::transparentBlack }` works without code changes. +- **`GraphicsContext` simplified**: wraps a `GpuDevice::Ptr` (obtained via `getGpuDevice()` returning `GpuDevice::Ptr`). Offscreen target management (`createOffscreenTarget`, `beginOffscreen`, `endOffscreen`, `readOffscreenPixels`) delegated to `GpuDevice`. Factory accepts optional `GpuDevice::Ptr` to share an existing GPU device. +- **Backends**: `GpuDevice` has native implementations for all platforms (Metal, OpenGL, Direct3D 11, Dawn, WebGPU/Emscripten, Headless). OpenGL backend probes `GL_VERSION` at runtime to detect compute shader support (GL ≥4.3 / GLES ≥3.1). +- **`::Ptr` safety**: all RHI types that own resources (`GpuPipeline`, `GpuBuffer`, `GpuTexture`, `GpuTarget`, `GpuCanvas`) are reference-counted with `::Ptr`. Factory methods take `GpuDevice::Ptr` to keep the device alive for the resource's lifetime. `GpuFrame` is move-only stack RAII and takes `GpuDevice&` (no ownership). + #### Image Formats - Added TIFF read/write support (`TiffImageFormat`) via libtiff: RGB, RGBA, Grayscale at 8/16-bit; multi-page reading; DPI and EXIF/ICC/XMP metadata extraction. diff --git a/docs/graphics/index.md b/docs/graphics/index.md index 2fef4921c..9d4602fa2 100644 --- a/docs/graphics/index.md +++ b/docs/graphics/index.md @@ -4,7 +4,7 @@ The graphics stack renders 2D vector content and GPU-accelerated scenes across Metal, Direct3D, OpenGL / OpenGL ES, WebGL and WebGPU (WASM / Emscripten), and Vulkan (in progress). It is built on the open source [Rive](https://rive.app/) renderer. -**Modules covered:** `yup_graphics`, `yup_shading`, `yup_animation`. +**Modules covered:** `yup_rhi`, `yup_graphics`, `yup_shading`, `yup_animation`. ## In this area @@ -29,12 +29,18 @@ own [Imaging](../imaging/index.md) area. ## Key building blocks -The `yup_graphics` module provides: +The `yup_graphics` and `yup_rhi` modules provide: -- **`GraphicsContext`** - abstracts the active rendering backend (`Api::Metal`, - `Api::Direct3D`, `Api::OpenGL`, `Api::OpenGLES`, `Api::WebGPU`, `Api::Headless`), - exposes DPI scaling, offscreen target creation, and the GPU capability probe - `isGpuAvailable()`. +- **`GpuDevice`** (`yup_rhi`) - a reference-counted GPU device abstraction that + owns the native GPU device and command queue without requiring a window. + Created via `GpuDevice::create(GpuPlatform, Options)`. Supports + `GpuPlatform::Metal`, `Direct3D`, `OpenGL`, `OpenGLES`, `WebGPU`, and `Headless`. + Use `GpuDevice` directly for GPU compute (e.g. audio DSP on the GPU) — no window needed. +- **`GraphicsContext`** (`yup_graphics`) - wraps a `GpuDevice` and adds the + window/swapchain layer plus Rive vector rendering. Created via + `GraphicsContext::createContext(GpuPlatform, Options, GpuDevice::Ptr = {})`. + When an existing `GpuDevice::Ptr` is provided, it shares the GPU device + (useful when an audio processor already owns one). - **`Graphics`** - the immediate-mode 2D drawing API (fills, strokes, paths, gradients, text, images, textures). - **Primitives** - points, rectangles, sizes, affine transforms, and colors. diff --git a/docs/graphics/rhi/buffers-and-textures.md b/docs/graphics/rhi/buffers-and-textures.md index adf05b53a..e55eed3bf 100644 --- a/docs/graphics/rhi/buffers-and-textures.md +++ b/docs/graphics/rhi/buffers-and-textures.md @@ -10,7 +10,7 @@ buffer set on the pass via `GpuRenderPass::setUniformBuffer()` instead. ```cpp enum class GpuBufferType : uint8_t { vertex, index, uniform }; -static GpuBuffer::Ptr GpuBuffer::create (GraphicsContext& ctx, +static GpuBuffer::Ptr GpuBuffer::create (GpuDevice::Ptr ctx, GpuBufferType type, const void* data, size_t byteSize); diff --git a/docs/graphics/rhi/concepts.md b/docs/graphics/rhi/concepts.md index 3b554653a..75259fc21 100644 --- a/docs/graphics/rhi/concepts.md +++ b/docs/graphics/rhi/concepts.md @@ -3,10 +3,10 @@ ## The GPU bridge Every RHI type is a thin, portable wrapper over a single backend bridge: Rive's -GPU context. The `GraphicsContext` owns that context and exposes it to the -RHI layer. You never touch GPU types directly - the RHI hides them behind -YUP-native handles (`GpuPipeline`, `GpuFrame`, `GpuRenderPass`, `GpuBuffer`, -`GpuTexture`). +GPU context. The `GpuDevice` owns that context and exposes it to the RHI layer. +For windowed rendering, `GraphicsContext` wraps a `GpuDevice` and adds the +swapchain and Rive vector renderer. For headless GPU compute, use `GpuDevice` +directly — no window or Rive dependency. Because the RHI targets one common abstraction, the same code path runs on every backend: Metal, Direct3D, OpenGL / OpenGL ES (including WebGL2), and WebGPU. @@ -16,7 +16,7 @@ backend: Metal, Direct3D, OpenGL / OpenGL ES (including WebGL2), and WebGPU. Always check GPU availability before creating RHI resources: ```cpp -if (! ctx.isGpuAvailable()) +if (! device.isGpuAvailable()) return; // No GPU context - fall back or bail out. ``` @@ -25,16 +25,16 @@ not reference any GPU type, so user code stays backend-clean. RHI factory functions honor this contract: -- `GpuPipeline::compile(...)` requires `isGpuAvailable()`. -- `GpuBuffer::create(...)` returns `nullptr` if GPU context is unavailable. -- `GpuTarget::create(...)` / `GpuCanvas::create(...)` return `nullptr` if +- `GpuPipeline::compile(device, ...)` requires `isGpuAvailable()`. +- `GpuBuffer::create(device, ...)` returns `nullptr` if GPU context is unavailable. +- `GpuTarget::create(device, ...)` / `GpuCanvas::create(ctx, ...)` return `nullptr` if offscreen GPU resources cannot be allocated. ## The frame → pass → draw model RHI rendering follows a strict hierarchy: -1. **Begin a frame** with `GpuFrame::begin(ctx)`. The frame owns the transient +1. **Begin a frame** with `GpuFrame::begin(device)`. The frame owns the transient GPU resources (uniform buffers, texture views, samplers) created while encoding, and keeps them alive until submission completes. 2. **Begin one or more render passes** into a target @@ -46,9 +46,9 @@ RHI rendering follows a strict hierarchy: 5. **Submit the frame** with `submit()` (or let it submit on destruction). ```cpp -auto frame = GpuFrame::begin (ctx); +auto frame = GpuFrame::begin (device); auto pass = target->beginRenderPass (frame, { true, Colors::black }); -pass.setPipeline (*pipeline); +pass.setPipeline (pipeline); pass.draw (3); pass.finish(); frame.submit(); diff --git a/docs/graphics/rhi/frames-and-passes.md b/docs/graphics/rhi/frames-and-passes.md index e2f9ab2a0..d9630b288 100644 --- a/docs/graphics/rhi/frames-and-passes.md +++ b/docs/graphics/rhi/frames-and-passes.md @@ -8,13 +8,13 @@ GPU resources (uniform buffers, texture views, samplers) created while encoding its passes. ```cpp -static GpuFrame GpuFrame::begin (GraphicsContext& ctx); +static GpuFrame GpuFrame::begin (GpuDevice::Ptr device); ``` Begin a frame, encode one or more render passes into it, then submit: ```cpp -auto frame = GpuFrame::begin (ctx); +auto frame = GpuFrame::begin (device); if (! frame.isValid()) return; // No GPU context. @@ -62,7 +62,7 @@ auto pass = canvas->beginRenderPass (frame, { true, background }); if (! pass.isValid()) return; -pass.setPipeline (*pipeline); +pass.setPipeline (pipeline); pass.setUniformBuffer (0, 0, &uniforms, sizeof uniforms); pass.setTexture (0, 1, sceneTexture); pass.setVertexBuffer (0, vertexBuffer); @@ -89,7 +89,7 @@ For a fullscreen post-process that generates its vertices from the vertex index, bind **no** vertex buffers and issue a three-vertex draw: ```cpp -pass.setPipeline (*blurPipeline); +pass.setPipeline (blurPipeline); pass.setTexture (0, 0, sourceTexture); pass.setUniformBuffer (0, 1, &blurParams, sizeof blurParams); pass.draw (3); // fullscreen triangle @@ -103,8 +103,8 @@ Controls attachment load behavior for a pass: ```cpp struct GpuRenderOptions { - bool clear = true; // clear vs. load existing contents - Color clearColor = Colors::transparentBlack; // used when clear == true + bool clear = true; // clear vs. load existing contents + GpuColor clearColor = Colors::transparentBlack; // used when clear == true }; ``` @@ -114,7 +114,7 @@ struct GpuRenderOptions ```cpp // Clear to a solid background: -auto pass = target->beginRenderPass (frame, { true, Colors::cornflowerblue }); +auto pass = target->beginRenderPass (frame, { true, Colors::black }); // Draw over existing contents: auto overlay = target->beginRenderPass (frame, { false, Colors::transparentBlack }); diff --git a/docs/graphics/rhi/index.md b/docs/graphics/rhi/index.md index 78042c05b..28ef357fb 100644 --- a/docs/graphics/rhi/index.md +++ b/docs/graphics/rhi/index.md @@ -1,13 +1,16 @@ # RHI - GPU Rendering Hardware Interface -The **RHI** is YUP's backend-agnostic, low-level GPU layer. It sits below the 2D -`Graphics` API and above Rive's GPU abstraction, giving you direct control -over pipelines, render passes, buffers, and textures while remaining portable -across Metal, Direct3D, OpenGL and OpenGL ES, WebGL2 and WebGPU, and Vulkan (in progress). +The **RHI** is YUP's backend-agnostic, low-level GPU layer, provided by the +`yup_rhi` module. It sits below the 2D `Graphics` API and above Rive's GPU +abstraction, giving you direct control over pipelines, render passes, buffers, +and textures while remaining portable across Metal, Direct3D, OpenGL / OpenGL ES, +WebGL2, WebGPU, and Vulkan (in progress). Use the RHI when you need custom GPU work that the 2D `Graphics` API does not express - 3D geometry, post-process effects, compute-style fullscreen passes, or -offscreen render-to-texture pipelines. +offscreen render-to-texture pipelines. For GPU compute without any window or +graphics (e.g. audio DSP on the GPU), use `GpuDevice` directly — no +`GraphicsContext` or `yup_graphics` dependency needed. ## When to use the RHI @@ -21,6 +24,8 @@ offscreen render-to-texture pipelines. ## Classes at a glance +- **`GpuDevice`** - a reference-counted GPU device abstraction. Owns the native + device and command queue. Factories for all RHI resources start here. - **`GpuFrame`** - RAII scope for one frame's GPU work. Begin, encode passes, submit. - **`GpuRenderPass`** - records draw commands (pipeline, bindings, draws) into a diff --git a/docs/graphics/rhi/pipelines.md b/docs/graphics/rhi/pipelines.md index 170c50c7d..37206fcf7 100644 --- a/docs/graphics/rhi/pipelines.md +++ b/docs/graphics/rhi/pipelines.md @@ -11,7 +11,7 @@ rendering** (indexed or non-indexed) with vertex buffers, culling, and depth/stencil state. ```{note} -Compiling a pipeline requires GPU context in `GraphicsContext` to exist. +Compiling a pipeline requires a `GpuDevice` with GPU context available. Check `ctx.isGpuAvailable()` first. ``` @@ -36,7 +36,7 @@ assignment stays consistent across all targets. ```cpp ResultValue GpuPipeline::compileFromBundle ( - GraphicsContext& ctx, + GpuDevice::Ptr ctx, const ShaderBundle& bundle, const GpuPipelineOptions& options = {}); ``` @@ -70,7 +70,7 @@ pre-compiled RSTB binding-map blob - see [Binding maps](#binding-maps). ```cpp ResultValue GpuPipeline::compile ( - GraphicsContext& ctx, + GpuDevice::Ptr ctx, const GpuShaderSource& vertexShader, const GpuShaderSource& fragmentShader, const GpuPipelineOptions& options = {}); @@ -233,5 +233,5 @@ if (result.wasOk()) | `setMaxEntries (n)` / `getMaxEntries()` | LRU eviction limit (0 = unlimited; default 256). | | `generateCacheKey (bundle, options, api)` | Static SHA1-based key generation. | -The cache references an externally-owned `GraphicsContext` that must outlive it. +The cache references an keeps the `GpuDevice` alive via `Ptr`. Eviction is LRU by access order. diff --git a/docs/graphics/rhi/spinning-cube.md b/docs/graphics/rhi/spinning-cube.md index 58abe6192..a1b8538f2 100644 --- a/docs/graphics/rhi/spinning-cube.md +++ b/docs/graphics/rhi/spinning-cube.md @@ -39,11 +39,11 @@ The cube uses a vertex + fragment shader pair. With the transpiler enabled, GLSL ```cpp GpuPipelineOptions options; -options.vertexBuffers = &cubeLayout; // position/color/normal -options.vertexBufferCount = 1; -options.indexFormat = GpuIndexFormat::uint16; -options.cullMode = GpuCullMode::back; -options.winding = GpuFaceWinding::counterClockwise; +options.vertexBuffers = &cubeLayout; // position/color/normal +options.vertexBufferCount = 1; +options.indexFormat = GpuIndexFormat::uint16; +options.cullMode = GpuCullMode::back; +options.winding = GpuFaceWinding::counterClockwise; options.depthStencil.enabled = true; auto result = GpuPipeline::compileFromGlsl (ctx, vertGlsl, fragGlsl, options); @@ -85,10 +85,10 @@ Each frame, begin a `GpuFrame`, open a render pass on the scene canvas, bind the pipeline + per-frame uniforms + geometry, and issue an indexed draw: ```cpp -auto frame = GpuFrame::begin (ctx); +auto frame = GpuFrame::begin (device); auto pass = sceneCanvas->beginRenderPass (frame, { true, Colors::black }); -pass.setPipeline (*cubePipeline); +pass.setPipeline (cubePipeline); pass.setUniformBuffer (0, 0, &mvp, sizeof mvp); // per-frame transform pass.setTexture (0, 1, animatedTexture); pass.setVertexBuffer (0, cubeVerts); @@ -107,14 +107,14 @@ samples the previous result and generates its vertices from the vertex index: GpuRenderOptions load { false, Colors::transparentBlack }; auto hPass = blurCanvasH->beginRenderPass (frame, { true, Colors::transparentBlack }); -hPass.setPipeline (*blurPipeline); +hPass.setPipeline (blurPipeline); hPass.setTexture (0, 0, sceneCanvas->asTexture()); hPass.setUniformBuffer (0, 1, &horizontalParams, sizeof horizontalParams); hPass.draw (3); hPass.finish(); auto vPass = blurCanvasV->beginRenderPass (frame, { true, Colors::transparentBlack }); -vPass.setPipeline (*blurPipeline); +vPass.setPipeline (blurPipeline); vPass.setTexture (0, 0, blurCanvasH->asTexture()); vPass.setUniformBuffer (0, 1, &verticalParams, sizeof verticalParams); vPass.draw (3); diff --git a/docs/graphics/rhi/targets.md b/docs/graphics/rhi/targets.md index 6468134f7..0af29230f 100644 --- a/docs/graphics/rhi/targets.md +++ b/docs/graphics/rhi/targets.md @@ -21,16 +21,16 @@ The minimal offscreen render surface. It allocates a backing texture from the context's main render context - no dedicated 2D context is created. ```cpp -static GpuTarget::Ptr GpuTarget::create (GraphicsContext& ctx, int width, int height); +static GpuTarget::Ptr GpuTarget::create (GpuDevice::Ptr ctx, int width, int height); ``` ```cpp auto target = GpuTarget::create (ctx, 256, 256); if (target != nullptr) { - auto frame = GpuFrame::begin (ctx); + auto frame = GpuFrame::begin (device); auto pass = target->beginRenderPass (frame, { true, Colors::transparentBlack }); - pass.setPipeline (*pipeline); + pass.setPipeline (pipeline); pass.draw (3); pass.finish(); frame.submit(); @@ -52,11 +52,11 @@ if (target != nullptr) Builds on a `GpuTarget` but is backed by a **dedicated render context**, adding 2D `Graphics` drawing via `beginDraw()` / `commit()`. It consolidates creation, rendering, and readback of an offscreen surface into one object, replacing the -lower-level `GraphicsContext::createOffscreenTarget` / `beginOffscreen` / +lower-level `GpuDevice::createOffscreenTarget` / `beginOffscreen` / `endOffscreen` API. ```cpp -static GpuCanvas::Ptr GpuCanvas::create (GraphicsContext& ctx, int width, int height); +static GpuCanvas::Ptr GpuCanvas::create (GpuDevice::Ptr ctx, int width, int height); ``` ### 2D drawing path @@ -85,7 +85,7 @@ A canvas can also be the target of a `GpuRenderPass`, exactly like `GpuTarget`: ```cpp auto pass = canvas->beginRenderPass (frame, { true, background }); -pass.setPipeline (*pipeline); +pass.setPipeline (pipeline); pass.drawIndexed (indexCount); pass.finish(); ``` diff --git a/docs/modules.md b/docs/modules.md index 4b8472a5e..e05d1ab27 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -138,17 +138,37 @@ flowchart LR classDef opt fill:#fff7ed,color:#9a3412,stroke:#fb923c,stroke-dasharray:2 2; ``` +### yup_rhi + +The low-level GPU abstraction layer: device management, compute and render +pipelines, frames, render passes, buffers, textures, and offscreen targets. +This is the foundation for GPU work that does not require a window or Rive +vector rendering — use it directly for GPU compute (audio DSP, FFT) without +pulling in the 2D graphics stack. + +```mermaid +flowchart LR + yup_rhi:::self --> yup_core + yup_rhi --> yup_shading + yup_rhi --> rive_renderer:::ext + classDef self fill:#6366f1,color:#fff,stroke:#4f46e5; + classDef ext fill:#f3f4f6,color:#374151,stroke:#9ca3af,stroke-dasharray:4 3; +``` + ### yup_graphics -The 2D drawing stack and the low-level GPU RHI, rendered through the Rive -renderer. Covers the graphics context, primitives, paths, fonts, SVG, imaging, -and GPU pipelines. Image-codec support is optional: link `libpng`, `libjpeg`, -`libwebp`, and/or `libgif` to enable the corresponding [image formats](imaging/loading.md#available-formats). +The 2D drawing stack and windowed rendering, layered on `yup_rhi` and the Rive +renderer. Covers the `GraphicsContext` (window + swapchain), `Graphics` (2D +drawing API), primitives, paths, fonts, SVG, imaging, and `GpuCanvas` (offscreen +2D surfaces). Image-codec support is optional: link `libpng`, `libjpeg`, +`libwebp`, `libgif`, and/or `libtiff` to enable the corresponding +[image formats](imaging/loading.md#available-formats). ```mermaid flowchart LR yup_graphics:::self --> yup_core yup_graphics --> yup_simd + yup_graphics --> yup_rhi yup_graphics --> yup_shading yup_graphics --> rive:::ext yup_graphics --> rive_renderer:::ext @@ -157,6 +177,7 @@ flowchart LR yup_graphics -. optional .-> libjpeg:::opt yup_graphics -. optional .-> libwebp:::opt yup_graphics -. optional .-> libgif:::opt + yup_graphics -. optional .-> libtiff:::opt classDef self fill:#6366f1,color:#fff,stroke:#4f46e5; classDef ext fill:#f3f4f6,color:#374151,stroke:#9ca3af,stroke-dasharray:4 3; classDef opt fill:#fff7ed,color:#9a3412,stroke:#fb923c,stroke-dasharray:2 2; @@ -387,12 +408,15 @@ flowchart TD simd[yup_simd] --> core[yup_core] events[yup_events] --> core shading[yup_shading] --> core + rhi[yup_rhi] --> core + rhi --> shading python[yup_python] --> core ai[yup_ai] --> core ai --> events graphics[yup_graphics] --> core graphics --> simd + graphics --> rhi graphics --> shading animation[yup_animation] --> core diff --git a/examples/graphics/source/examples/ComponentEffectsDemo.h b/examples/graphics/source/examples/ComponentEffectsDemo.h index 741c2fc95..07d96b296 100644 --- a/examples/graphics/source/examples/ComponentEffectsDemo.h +++ b/examples/graphics/source/examples/ComponentEffectsDemo.h @@ -226,12 +226,12 @@ void main() { return; } - auto frame = yup::GpuFrame::begin (ctx); + auto frame = yup::GpuFrame::begin (ctx.getGpuDevice()); auto pass = [&] (yup::GpuTarget& t, const yup::GpuTexture::Ptr& in, float dx, float dy) { EffectParams p { sigma, blurR, (float) w, (float) h, dx, dy, 0, 0 }; auto rp = t.beginRenderPass (frame, { true, yup::Colors::transparentBlack }); - rp.setPipeline (*pipeline); + rp.setPipeline (pipeline); rp.setTexture (0, 0, in); rp.setUniformBuffer (0, 2, &p, sizeof (p)); rp.draw (3); @@ -254,7 +254,7 @@ void main() { { if (pipeline) return true; - auto r = yup::GpuPipeline::compileFromGlsl (ctx, kVertSource, kBlurFrag, {}); + auto r = yup::GpuPipeline::compileFromGlsl (ctx.getGpuDevice(), kVertSource, kBlurFrag, {}); if (r.wasOk()) pipeline = r.getValue(); return pipeline != nullptr; @@ -263,9 +263,9 @@ void main() { bool ensureTargets (yup::GraphicsContext& ctx, int w, int h) { if (! targetA || targetA->getWidth() != w || targetA->getHeight() != h) - targetA = yup::GpuTarget::create (ctx, w, h); + targetA = yup::GpuTarget::create (ctx.getGpuDevice(), w, h); if (! targetB || targetB->getWidth() != w || targetB->getHeight() != h) - targetB = yup::GpuTarget::create (ctx, w, h); + targetB = yup::GpuTarget::create (ctx.getGpuDevice(), w, h); return targetA != nullptr && targetB != nullptr; } @@ -310,7 +310,7 @@ void main() { { if (pipeline) return true; - auto r = yup::GpuPipeline::compileFromGlsl (ctx, kVertSource, yup::String::fromUTF8 (fragSource), {}); + auto r = yup::GpuPipeline::compileFromGlsl (ctx.getGpuDevice(), kVertSource, yup::String::fromUTF8 (fragSource), {}); if (r.wasOk()) pipeline = r.getValue(); return pipeline != nullptr; @@ -319,7 +319,7 @@ void main() { bool ensureTarget (yup::GraphicsContext& ctx, int w, int h) { if (! target || target->getWidth() != w || target->getHeight() != h) - target = yup::GpuTarget::create (ctx, w, h); + target = yup::GpuTarget::create (ctx.getGpuDevice(), w, h); return target != nullptr; } @@ -333,10 +333,10 @@ void main() { return; } - auto frame = yup::GpuFrame::begin (ctx); + auto frame = yup::GpuFrame::begin (ctx.getGpuDevice()); { auto rp = target->beginRenderPass (frame, { true, yup::Colors::transparentBlack }); - rp.setPipeline (*pipeline); + rp.setPipeline (pipeline); rp.setTexture (0, 0, input); rp.setUniformBuffer (0, 2, &p, sizeof (p)); rp.draw (3); diff --git a/examples/graphics/source/examples/SpinningCubeDemo.h b/examples/graphics/source/examples/SpinningCubeDemo.h index 6e449d57c..05a2d8e58 100644 --- a/examples/graphics/source/examples/SpinningCubeDemo.h +++ b/examples/graphics/source/examples/SpinningCubeDemo.h @@ -210,7 +210,7 @@ class SpinningCubeDemo : public yup::Component // target - no 2D drawing, so no dedicated render context is needed). if (sceneCanvas == nullptr || sceneCanvas->getWidth() != w || sceneCanvas->getHeight() != h) { - sceneCanvas = yup::GpuTarget::create (*capturedContext, w, h); + sceneCanvas = yup::GpuTarget::create (capturedContext->getGpuDevice(), w, h); if (sceneCanvas == nullptr) return; } @@ -244,22 +244,22 @@ class SpinningCubeDemo : public yup::Component // Ping-pong render targets reused across frames (recreated on resize). if (blurCanvasA == nullptr || blurCanvasA->getWidth() != w || blurCanvasA->getHeight() != h) - blurCanvasA = yup::GpuTarget::create (*capturedContext, w, h); + blurCanvasA = yup::GpuTarget::create (capturedContext->getGpuDevice(), w, h); if (blurCanvasB == nullptr || blurCanvasB->getWidth() != w || blurCanvasB->getHeight() != h) - blurCanvasB = yup::GpuTarget::create (*capturedContext, w, h); + blurCanvasB = yup::GpuTarget::create (capturedContext->getGpuDevice(), w, h); if (blurCanvasA != nullptr && blurCanvasB != nullptr) { // Both blur passes share a single GpuFrame. - auto frame = yup::GpuFrame::begin (*capturedContext); + auto frame = yup::GpuFrame::begin (capturedContext->getGpuDevice()); auto runPass = [&] (yup::GpuTarget& passCanvas, const yup::GpuTexture::Ptr& input, float dirX, float dirY) -> yup::GpuTexture::Ptr { BlurParams params { blurSigma, radius, (float) w, (float) h, dirX, dirY, 0.0f, 0.0f }; auto pass = passCanvas.beginRenderPass (frame, { true, yup::Colors::transparentBlack }); - pass.setPipeline (*blurPipeline); + pass.setPipeline (blurPipeline); pass.setTexture (0, 0, input); pass.setUniformBuffer (0, 2, ¶ms, sizeof (params)); pass.draw (3); @@ -652,7 +652,7 @@ void main() { void initBlur() { - auto result = yup::GpuPipeline::compileFromGlsl (*capturedContext, + auto result = yup::GpuPipeline::compileFromGlsl (capturedContext->getGpuDevice(), currentBlurVertSource, currentBlurFragSource, {}); @@ -684,11 +684,11 @@ void main() { if (auto* fs = bundle.findShader (yup::ShaderStage::fragment, yup::ShaderLanguage::glsl)) currentFragSource = fs->inputSource.isNotEmpty() ? fs->inputSource : fs->source; - auto result = yup::GpuPipeline::compileFromBundle (*capturedContext, + auto result = yup::GpuPipeline::compileFromBundle (capturedContext->getGpuDevice(), loaded.getReference(), cubePipelineOptions()); #else - auto result = yup::GpuPipeline::compileFromGlsl (*capturedContext, + auto result = yup::GpuPipeline::compileFromGlsl (capturedContext->getGpuDevice(), currentVertSource, currentFragSource, cubePipelineOptions()); @@ -703,8 +703,8 @@ void main() { cubePipeline = result.getValue(); // Upload immutable vertex and index buffers. - cubeVBO = yup::GpuBuffer::create (*capturedContext, yup::GpuBufferType::vertex, kCubeVerts, sizeof (kCubeVerts)); - cubeIBO = yup::GpuBuffer::create (*capturedContext, yup::GpuBufferType::index, kCubeIdx, sizeof (kCubeIdx)); + cubeVBO = yup::GpuBuffer::create (capturedContext->getGpuDevice(), yup::GpuBufferType::vertex, kCubeVerts, sizeof (kCubeVerts)); + cubeIBO = yup::GpuBuffer::create (capturedContext->getGpuDevice(), yup::GpuBufferType::index, kCubeIdx, sizeof (kCubeIdx)); if (cubeVBO == nullptr || cubeIBO == nullptr) yup::Logger::outputDebugString ("SpinningCubeDemo: cube buffer creation failed."); @@ -774,7 +774,7 @@ void main() { if (editingBlur) { - auto result = yup::GpuPipeline::compileFromGlsl (*capturedContext, + auto result = yup::GpuPipeline::compileFromGlsl (capturedContext->getGpuDevice(), currentBlurVertSource, currentBlurFragSource, {}); @@ -789,7 +789,7 @@ void main() { } else { - auto result = yup::GpuPipeline::compileFromGlsl (*capturedContext, + auto result = yup::GpuPipeline::compileFromGlsl (capturedContext->getGpuDevice(), currentVertSource, currentFragSource, cubePipelineOptions()); @@ -984,10 +984,10 @@ void main() { CubeUniforms uniforms { angleY, angleX, (float) w / (float) h, 0.0f }; - auto frame = yup::GpuFrame::begin (*capturedContext); + auto frame = yup::GpuFrame::begin (capturedContext->getGpuDevice()); auto pass = canvas.beginRenderPass (frame, { true, yup::Color (0xff1a1a2e) }); - pass.setPipeline (*cubePipeline); + pass.setPipeline (cubePipeline); pass.setUniformBuffer (0, 0, &uniforms, sizeof (uniforms)); if (lottieTexture != nullptr) pass.setTexture (0, 1, lottieTexture); diff --git a/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp b/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp index e118e5167..593ce7138 100644 --- a/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp +++ b/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp @@ -149,7 +149,7 @@ GpuPipeline::Ptr AnimationRenderResources::getMattePipeline (GraphicsContext& co options.colorTargets[0].format = GpuTextureFormat::rgba8unorm; options.colorTargets[0].blendEnabled = false; - auto result = GpuPipeline::compileFromGlsl (context, + auto result = GpuPipeline::compileFromGlsl (context.getGpuDevice(), String::fromUTF8 (kMatteVertSource, (int) sizeof (kMatteVertSource) - 1), String::fromUTF8 (kMatteFragSource, (int) sizeof (kMatteFragSource) - 1), options); diff --git a/modules/yup_animation/renderer/yup_AnimationRenderer.cpp b/modules/yup_animation/renderer/yup_AnimationRenderer.cpp index 7ed9ef5db..01baec2d5 100644 --- a/modules/yup_animation/renderer/yup_AnimationRenderer.cpp +++ b/modules/yup_animation/renderer/yup_AnimationRenderer.cpp @@ -460,7 +460,7 @@ bool AnimationRenderer::renderLayerWithMatte (Graphics& g, { MatteParams params { matteModeValue (layer.matteType), (float) w, (float) h, 0.0f }; - auto frame = GpuFrame::begin (context); + auto frame = GpuFrame::begin (context.getGpuDevice()); if (! frame.isValid()) return false; @@ -468,7 +468,7 @@ bool AnimationRenderer::renderLayerWithMatte (Graphics& g, if (! pass.isValid()) return false; - pass.setPipeline (*pipeline); + pass.setPipeline (pipeline); pass.setTexture (0, 0, targetTex); pass.setTexture (0, 1, sourceTex); pass.setUniformBuffer (0, 3, ¶ms, sizeof (params)); diff --git a/modules/yup_graphics/context/yup_GraphicsContext.cpp b/modules/yup_graphics/context/yup_GraphicsContext.cpp index 826ba86cd..327fac63e 100644 --- a/modules/yup_graphics/context/yup_GraphicsContext.cpp +++ b/modules/yup_graphics/context/yup_GraphicsContext.cpp @@ -22,35 +22,64 @@ namespace yup { -std::unique_ptr GraphicsContext::createContext (Api graphicsApi, Options options) +//============================================================================== +bool GraphicsContext::isGpuAvailable() const noexcept +{ + if (auto device = getGpuDevice()) + return device->gpuContext() != nullptr; + + return false; +} + +//============================================================================== +std::unique_ptr yup_constructHeadlessGraphicsContext (GpuDevice::Options, GpuDevice::Ptr = {}); +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) +std::unique_ptr yup_constructMetalGraphicsContext (GpuDevice::Options, GpuDevice::Ptr = {}); +#endif +#if YUP_RIVE_USE_D3D && YUP_WINDOWS +std::unique_ptr yup_constructDirect3DGraphicsContext (GpuDevice::Options, GpuDevice::Ptr = {}); +#endif +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) +std::unique_ptr yup_constructOpenGLGraphicsContext (GpuDevice::Options, GpuDevice::Ptr = {}); +#endif +#if YUP_EMSCRIPTEN && RIVE_WEBGPU +std::unique_ptr yup_constructWebGPUGraphicsContext (GpuDevice::Options, GpuDevice::Ptr = {}); +#elif YUP_RIVE_USE_DAWN +std::unique_ptr yup_constructDawnGraphicsContext (GpuDevice::Options, GpuDevice::Ptr = {}); +#endif + +//============================================================================== +std::unique_ptr GraphicsContext::createContext (GpuPlatform graphicsApi, + Options options, + GpuDevice::Ptr existingGpu) { switch (graphicsApi) { - case Api::Headless: - return yup_constructHeadlessGraphicsContext (options); + case GpuPlatform::Headless: + return yup_constructHeadlessGraphicsContext (options, std::move (existingGpu)); #if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) - case Api::Metal: - return yup_constructMetalGraphicsContext (options); + case GpuPlatform::Metal: + return yup_constructMetalGraphicsContext (options, std::move (existingGpu)); #endif #if YUP_RIVE_USE_D3D && YUP_WINDOWS - case Api::Direct3D: - return yup_constructDirect3DGraphicsContext (options); + case GpuPlatform::Direct3D: + return yup_constructDirect3DGraphicsContext (options, std::move (existingGpu)); #endif #if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) - case Api::OpenGL: - case Api::OpenGLES: - return yup_constructOpenGLGraphicsContext (options); + case GpuPlatform::OpenGL: + case GpuPlatform::OpenGLES: + return yup_constructOpenGLGraphicsContext (options, std::move (existingGpu)); #endif #if YUP_EMSCRIPTEN && RIVE_WEBGPU - case Api::WebGPU: - return yup_constructWebGPUGraphicsContext (options); + case GpuPlatform::WebGPU: + return yup_constructWebGPUGraphicsContext (options, std::move (existingGpu)); #elif YUP_RIVE_USE_DAWN - case Api::WebGPU: - return yup_constructDawnGraphicsContext (options); + case GpuPlatform::WebGPU: + return yup_constructDawnGraphicsContext (options, std::move (existingGpu)); #endif default: diff --git a/modules/yup_graphics/context/yup_GraphicsContext.h b/modules/yup_graphics/context/yup_GraphicsContext.h index 2d5c4f5c4..80942d6e9 100644 --- a/modules/yup_graphics/context/yup_GraphicsContext.h +++ b/modules/yup_graphics/context/yup_GraphicsContext.h @@ -23,11 +23,17 @@ namespace yup { //============================================================================== -/** Encapsulates a graphics context that abstracts rendering operations across various APIs. +/** Encapsulates a graphics context that abstracts windowed rendering operations + across various APIs, including Rive vector rendering and swapchain presentation. - This class serves as a base for implementing specific graphics context functionalities, such as rendering and resource management, - across different graphics APIs like OpenGL, OpenGLES, Direct3D, Metal, and WebGPU. It offers a standardized interface for operations - common to all graphics APIs. + GraphicsContext wraps a GpuDevice (GPU device abstraction) and adds the + window/swapchain layer plus Rive vector rendering support. It requires a + native window handle (via onSizeChanged) to create a swapchain for presentation. + + For GPU compute without a window (e.g. audio DSP on the GPU), use GpuDevice + directly — it does not require a window or Rive dependency. + + @see GpuDevice, ComponentNative::getGraphicsContext */ class YUP_API GraphicsContext { @@ -37,31 +43,10 @@ class YUP_API GraphicsContext using LoaderFunction = void* (*) (const char*); //============================================================================== - /** Enumerates supported graphics APIs. */ - enum Api - { - Headless, ///< Specifies the use of a headless context for rendering. - OpenGL, ///< Specifies the use of desktop OpenGL for rendering. - OpenGLES, ///< Specifies the use of OpenGL ES (GLES 3.0+) for rendering (Android, WASM). - Direct3D, ///< Specifies the use of Direct3D for rendering. - Metal, ///< Specifies the use of Metal for rendering. - WebGPU ///< Specifies the use of WebGPU (native browser WebGPU on Emscripten, Dawn elsewhere). - }; - - /** Configuration options for creating a graphics context. */ - struct Options - { - /** Default constructor, initializes the options with default values. */ - constexpr Options() noexcept = default; - - bool retinaDisplay = true; ///< Whether the context supports Retina or high-DPI displays. - bool readableFramebuffer = false; ///< Allows the framebuffer to be readable. - bool synchronousShaderCompilations = false; ///< Controls whether shader compilations are done synchronously. - bool enableReadPixels = false; ///< Enables reading pixels directly from the framebuffer. - bool disableRasterOrdering = false; ///< Disables specific raster ordering features for performance. - bool allowHeadlessRendering = false; ///< Allows rendering without a visible window (headless mode). - LoaderFunction loaderFunction = nullptr; ///< Loader function (used by GL/Vulkan). - }; + /** Configuration options for creating a graphics context. + + Extends GpuDevice::Options with window-specific settings. */ + using Options = GpuDevice::Options; //============================================================================== /** Default constructor. */ @@ -77,12 +62,27 @@ class YUP_API GraphicsContext GraphicsContext& operator= (const GraphicsContext& other) noexcept = delete; GraphicsContext& operator= (GraphicsContext&& other) noexcept = default; + //============================================================================== + /** Returns true if a GPU (ore) context is available for RHI operations. */ + bool isGpuAvailable() const noexcept; + //============================================================================== /** Returns the graphics API used by this context. - @return The Api enum value identifying the active rendering backend. + @return The GpuPlatform enum value identifying the active rendering backend. + */ + virtual GpuPlatform getPlatform() const noexcept = 0; + + //============================================================================== + /** Returns the underlying GpuDevice that owns the GPU device. + + The returned pointer is valid for the lifetime of this GraphicsContext. + Use this to create GpuPipelines, GpuBuffers, and other RHI resources + that can outlive the window. + + @return A shared pointer to the GpuDevice, or nullptr if unavailable. */ - virtual Api getApi() const noexcept = 0; + virtual GpuDevice::Ptr getGpuDevice() const noexcept = 0; //============================================================================== /** Provides access to the associated factory for resource creation. @@ -103,21 +103,6 @@ class YUP_API GraphicsContext */ virtual rive::gpu::RenderTarget* renderTarget() = 0; - /** Returns the GPU context, or nullptr when ore is unavailable on this backend. - - This is the single backend bridge used by the RHI layer (GpuPipeline, - GpuFrame, GpuRenderPass, GpuBuffer). User code should prefer the dependency-free - isGpuAvailable() capability probe instead. - */ - virtual rive::ore::Context* gpuContext() const noexcept { return nullptr; } - - /** Returns true if a GPU (ore) context is available for RHI operations. - - Equivalent to gpuContext() != nullptr but without referencing any ore - type, so user code and examples can probe GPU capability ore-free. - */ - bool isGpuAvailable() const noexcept { return gpuContext() != nullptr; } - /** Creates a renderer suitable for the specified dimensions. @param width The width of the render area. @@ -154,75 +139,22 @@ class YUP_API GraphicsContext /** Performs periodic operations, potentially related to animation or state updates. */ virtual void tick() {} - //============================================================================== - /** Creates platform-specific GPU offscreen resources for the given dimensions. - - Supported GPU backends may create targets while another offscreen target - is rendering. Backends reserve a render context only while its target - has an active frame, allowing sequential targets to share idle contexts. - A target must outlive its corresponding beginOffscreen()/endOffscreen() - pair and the GraphicsContext must outlive every target it creates. - - @param width The width of the offscreen target in pixels. - @param height The height of the offscreen target in pixels. - - @return A unique pointer to an OffscreenTarget object, or nullptr on failure. - */ - virtual std::unique_ptr createOffscreenTarget (int width, int height) = 0; - - /** Creates platform-specific GPU offscreen resources backed by a dedicated render context. - - Unlike createOffscreenTarget(), the returned RenderableTarget reserves a - backend-owned RenderContext, which is required to drive a 2D Graphics frame - (GpuCanvas::beginDraw). Prefer createOffscreenTarget() for render-pass-only - surfaces to avoid allocating a dedicated context. - - @param width The width of the offscreen target in pixels. - @param height The height of the offscreen target in pixels. - - @return A unique pointer to a RenderableTarget object, or nullptr on failure. - */ - virtual std::unique_ptr createRenderableTarget (int width, int height) = 0; - - /** Begins a GPU frame targeting the given offscreen surface. - - A target may have only one active frame. Nested frames are supported when - they use distinct OffscreenTarget instances. - - @param target The OffscreenTarget to render into. - @param frameDesc The frame descriptor that contains frame-specific data. - */ - virtual void beginOffscreen (OffscreenTarget& target, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) = 0; - - /** Flushes GPU commands into the offscreen target. - - Must be called after beginOffscreen() and before endOffscreen(). - - @param target The OffscreenTarget to flush commands into. - */ - virtual void endOffscreen (OffscreenTarget& target) = 0; - - /** Reads RGBA pixels from the completed offscreen frame into CPU memory. - - Must be called after endOffscreen(). Rows are top-to-bottom. - - @param target The OffscreenTarget to read pixels from. - @param dst Pointer to the destination buffer where pixel data will be stored. - @param dstSize The size of the destination buffer in bytes. - - @return True if the pixel read operation was successful, false otherwise. - */ - virtual bool readOffscreenPixels (OffscreenTarget& target, void* dst, size_t dstSize) = 0; - //============================================================================== /** Static factory method to create a graphics context using a specific graphics API. @param graphicsApi The graphics API to use. @param options Configuration options for the graphics context. + @param existingGpu An optional existing GpuDevice to share. When provided, + the GraphicsContext borrows the GPU device instead of + creating a new one. Useful when an audio processor + already owns a GpuDevice for compute. - @return A unique pointer to a GraphicsContext, using the specified graphics API and configured according to the options. + @return A unique pointer to a GraphicsContext, using the specified graphics + API and configured according to the options. */ - static std::unique_ptr createContext (Api graphicsApi, Options options); + static std::unique_ptr createContext (GpuPlatform graphicsApi, + Options options, + GpuDevice::Ptr existingGpu = {}); }; } // namespace yup diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index 8fa4bddff..59dac52d6 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -251,7 +251,7 @@ Graphics::Graphics (GraphicsContext& context, rive::Renderer& renderer, float sc } Graphics::Graphics (GraphicsContext& context, Image& image, uint32_t clearColor) noexcept - : Graphics (context, context.createRenderableTarget (image.getWidth(), image.getHeight()), clearColor) + : Graphics (context, context.getGpuDevice()->createRenderableTarget (image.getWidth(), image.getHeight()), clearColor) { offscreenTargetImage = std::addressof (image); } @@ -280,7 +280,7 @@ Graphics::Graphics (GraphicsContext& context, std::unique_ptr frameDesc.loadAction = rive::gpu::LoadAction::clear; frameDesc.clearColor = clearColor; - context.beginOffscreen (*offscreenTarget, frameDesc); + context.getGpuDevice()->beginOffscreen (*offscreenTarget, frameDesc); currentRenderOptions().drawingArea = { 0.0f, 0.0f, static_cast (offscreenTarget->getWidth()), static_cast (offscreenTarget->getHeight()) }; } @@ -302,7 +302,7 @@ Graphics::Graphics (GraphicsContext& context, RenderableTarget& target, uint32_t frameDesc.loadAction = rive::gpu::LoadAction::clear; frameDesc.clearColor = clearColor; - context.beginOffscreen (*offscreenTarget, frameDesc); + context.getGpuDevice()->beginOffscreen (*offscreenTarget, frameDesc); currentRenderOptions().drawingArea = { 0.0f, 0.0f, static_cast (offscreenTarget->getWidth()), static_cast (offscreenTarget->getHeight()) }; } @@ -310,7 +310,7 @@ Graphics::Graphics (GraphicsContext& context, RenderableTarget& target, uint32_t Graphics::~Graphics() { if (offscreenTarget != nullptr && ! committed) - context.endOffscreen (*offscreenTarget); + context.getGpuDevice()->endOffscreen (*offscreenTarget); } //============================================================================== @@ -338,7 +338,7 @@ bool Graphics::commitOffscreenTarget() if (offscreenTarget == nullptr || committed) return false; - context.endOffscreen (*offscreenTarget); + context.getGpuDevice()->endOffscreen (*offscreenTarget); committed = true; return true; @@ -353,7 +353,7 @@ bool Graphics::readPixelsToImage() commitToImage(); auto span = offscreenTargetImage->getRawData(); - return context.readOffscreenPixels (*offscreenTarget, span.data(), span.size()); + return context.getGpuDevice()->readOffscreenPixels (*offscreenTarget, span.data(), span.size()); } //============================================================================== @@ -450,7 +450,7 @@ Graphics::TransparencyLayer::TransparencyLayer (Graphics& parent, RectanglecreateRenderableTarget (width, height); if (target == nullptr) return; diff --git a/modules/yup_graphics/imaging/yup_Image.cpp b/modules/yup_graphics/imaging/yup_Image.cpp index 577f43afe..66477b2d8 100644 --- a/modules/yup_graphics/imaging/yup_Image.cpp +++ b/modules/yup_graphics/imaging/yup_Image.cpp @@ -202,6 +202,19 @@ Image Image::fromTexture (GpuTexture::Ptr tex) return image; } +Image Image::fromTarget (GpuTarget& target) +{ + auto img = fromTexture (target.asTexture()); + + if (img.isValid()) + { + auto span = img.getRawData(); + target.readPixels (span.data(), span.size()); + } + + return img; +} + //============================================================================== ResultValue Image::loadFromData (Span imageData, diff --git a/modules/yup_graphics/imaging/yup_Image.h b/modules/yup_graphics/imaging/yup_Image.h index 6a6829f73..588de204d 100644 --- a/modules/yup_graphics/imaging/yup_Image.h +++ b/modules/yup_graphics/imaging/yup_Image.h @@ -181,6 +181,15 @@ class Image */ static Image fromTexture (GpuTexture::Ptr tex); + /** Creates an Image from a GpuTarget, reading back pixels into CPU memory. + + Equivalent to fromTexture(target.asTexture()) followed by target.readPixels(). + Returns an empty Image on failure. + + @param target A GPU render target whose contents will be read back. + */ + static Image fromTarget (GpuTarget& target); + //============================================================================== /** Creates a texture on the GPU for the image if it doesn't already exist. diff --git a/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp b/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp index 6ca247952..94ced0c71 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp @@ -23,9 +23,7 @@ #include "rive/renderer/rive_renderer.hpp" #include "rive/renderer/d3d11/render_context_d3d_impl.hpp" #include "rive/renderer/d3d11/d3d11.hpp" -#include "rive/renderer/ore/ore_context_d3d11.hpp" #include -#include namespace yup { @@ -38,19 +36,24 @@ class LowLevelRenderContextD3D : public GraphicsContext ComPtr gpuContext, bool isHeadless, const rive::gpu::D3DContextOptions& contextOptions, - Options options) + Options options, + GpuDevice::Ptr existingGpu = {}) : m_isHeadless (isHeadless) , m_options (options) - , m_renderContextOptions (contextOptions) , m_d3dFactory (std::move (d3dFactory)) , m_gpu (std::move (gpu)) , m_gpuContext (std::move (gpuContext)) - , m_renderContext (rive::gpu::RenderContextD3DImpl::MakeContext (m_gpu, m_gpuContext, m_renderContextOptions)) - , m_oreContext (rive::ore::ContextD3D11::Make (m_gpu.Get(), m_gpuContext.Get())) + , m_renderContext (rive::gpu::RenderContextD3DImpl::MakeContext (m_gpu, m_gpuContext, contextOptions)) { + if (existingGpu != nullptr) + m_gpuContextPtr = std::move (existingGpu); + else + m_gpuContextPtr = GpuDevice::create (GpuPlatform::Direct3D, options); } - Api getApi() const noexcept override { return Api::Direct3D; } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Direct3D; } + + GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContextPtr; } rive::Factory* factory() override { return m_renderContext.get(); } @@ -58,14 +61,11 @@ class LowLevelRenderContextD3D : public GraphicsContext rive::gpu::RenderTarget* renderTarget() override { return m_renderTarget.get(); } - rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } - void onSizeChanged (void* window, int width, int height, float dpiScale, uint32_t sampleCount) override { if (! m_isHeadless) { m_swapchain.Reset(); - DXGI_SWAP_CHAIN_DESC1 scd {}; scd.Width = width; scd.Height = height; @@ -75,9 +75,6 @@ class LowLevelRenderContextD3D : public GraphicsContext scd.BufferCount = 2; scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; - fprintf (stderr, "D3D: CreateSwapChainForHwnd hwnd=%p size=%dx%d\n", window, width, height); - fflush (stderr); - VERIFY_OK (m_d3dFactory->CreateSwapChainForHwnd (m_gpu.Get(), (HWND) window, &scd, @@ -121,25 +118,18 @@ class LowLevelRenderContextD3D : public GraphicsContext if (m_renderTarget->targetTexture() == nullptr) { if (m_isHeadless) - { m_renderTarget->setTargetTexture (m_headlessDrawTexture); - } else { ComPtr backbuffer; - HRESULT hr = m_swapchain->GetBuffer (0, - __uuidof (ID3D11Texture2D), - reinterpret_cast (backbuffer.ReleaseAndGetAddressOf())); - + HRESULT hr = m_swapchain->GetBuffer (0, __uuidof (ID3D11Texture2D), reinterpret_cast (backbuffer.ReleaseAndGetAddressOf())); if (FAILED (hr)) { auto reason = m_gpu->GetDeviceRemovedReason(); fprintf (stderr, "D3D: GetBuffer failed: hr=0x%08X, deviceRemovedReason=0x%08X\n", static_cast (hr), static_cast (reason)); - fflush (stderr); m_renderTarget->setTargetTexture (nullptr); return; } - m_renderTarget->setTargetTexture (backbuffer); } } @@ -151,248 +141,36 @@ class LowLevelRenderContextD3D : public GraphicsContext if (! m_isHeadless) { HRESULT hr = m_swapchain->Present (0, 0); - if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) { auto reason = m_gpu->GetDeviceRemovedReason(); fprintf (stderr, "D3D: Present returned device removed/reset: hr=0x%08X, deviceRemovedReason=0x%08X\n", static_cast (hr), static_cast (reason)); - fflush (stderr); } else if (FAILED (hr)) { fprintf (stderr, "D3D: Present failed: hr=0x%08X\n", static_cast (hr)); - fflush (stderr); } } m_renderTarget->setTargetTexture (nullptr); } - //============================================================================== - - struct OffscreenContextSlot - { - std::unique_ptr renderContext; - bool frameActive = false; - }; - - struct OffscreenTargetD3D : public RenderableTarget - { - int width = 0; - int height = 0; - ComPtr stagingTexture; - rive::rcp renderCanvas; - rive::gpu::RenderContext* renderContext = nullptr; - OffscreenContextSlot* contextSlot = nullptr; - - int getWidth() const noexcept override { return width; } - - int getHeight() const noexcept override { return height; } - - rive::gpu::RenderTarget* getRenderTarget() noexcept override - { - return renderCanvas != nullptr ? renderCanvas->renderTarget() : nullptr; - } - - rive::gpu::RenderContext* getRenderContext() noexcept override - { - return renderContext; - } - - rive::rcp getRenderCanvas() noexcept override - { - return renderCanvas; - } - - rive::rcp adoptAsTexture() override - { - if (renderCanvas == nullptr) - return nullptr; - - return renderCanvas->renderImage()->refTexture(); - } - }; - - ComPtr createStagingTexture (int width, int height) - { - D3D11_TEXTURE2D_DESC stagingDesc {}; - stagingDesc.Width = static_cast (width); - stagingDesc.Height = static_cast (height); - stagingDesc.MipLevels = 1; - stagingDesc.ArraySize = 1; - stagingDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - stagingDesc.SampleDesc.Count = 1; - stagingDesc.Usage = D3D11_USAGE_STAGING; - stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - - ComPtr staging; - auto hr = m_gpu->CreateTexture2D (&stagingDesc, nullptr, staging.ReleaseAndGetAddressOf()); - if (FAILED (hr)) - return nullptr; - - return staging; - } - - std::unique_ptr createOffscreenTarget (int width, int height) override - { - if (width <= 0 || height <= 0 || m_renderContext == nullptr) - return nullptr; - - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = nullptr; - target->contextSlot = nullptr; - - target->renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), - static_cast (height)); - if (target->renderCanvas == nullptr) - return nullptr; - - target->stagingTexture = createStagingTexture (width, height); - if (target->stagingTexture == nullptr) - return nullptr; - - return target; - } - - std::unique_ptr createRenderableTarget (int width, int height) override - { - if (width <= 0 || height <= 0) - return nullptr; - - auto* contextSlot = acquireOffscreenContext(); - if (contextSlot == nullptr) - return nullptr; - - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = contextSlot->renderContext.get(); - target->contextSlot = contextSlot; - - target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), - static_cast (height)); - if (target->renderCanvas == nullptr) - return nullptr; - - target->stagingTexture = createStagingTexture (width, height); - if (target->stagingTexture == nullptr) - return nullptr; - - return target; - } - - void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override - { - auto& target = static_cast (baseTarget); - auto* renderContext = target.getRenderContext(); - - if (renderContext != nullptr) - { - if (target.contextSlot == nullptr || target.contextSlot->frameActive) - return; - - renderContext->beginFrame (frameDesc); - target.contextSlot->frameActive = true; - } - } - - void endOffscreen (OffscreenTarget& baseTarget) override - { - auto& target = static_cast (baseTarget); - auto* renderContext = target.getRenderContext(); - - if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) - return; - - rive::gpu::RenderContext::FlushResources flushDesc; - flushDesc.renderTarget = target.getRenderTarget(); - renderContext->flush (flushDesc); - - if (auto* renderTarget = static_cast (target.getRenderTarget())) - m_gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); - - target.contextSlot->frameActive = false; - } - - bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override - { - auto& target = static_cast (baseTarget); - - if (target.stagingTexture == nullptr || dst == nullptr) - return false; - - const size_t bytesPerRow = static_cast (target.width) * 4u; - if (dstSize < bytesPerRow * static_cast (target.height)) - return false; - - // Light (render-pass-only) targets never run endOffscreen, so the staging - // texture is populated here on demand from the render canvas texture. - if (target.getRenderContext() == nullptr) - { - if (auto* renderTarget = static_cast (target.getRenderTarget())) - m_gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); - } - - D3D11_MAPPED_SUBRESOURCE mapped {}; - HRESULT hr = m_gpuContext->Map (target.stagingTexture.Get(), 0, D3D11_MAP_READ, 0, &mapped); - if (FAILED (hr)) - return false; - - auto* dstBytes = static_cast (dst); - const auto* srcBytes = static_cast (mapped.pData); - - for (int row = 0; row < target.height; ++row) - { - std::memcpy (dstBytes + static_cast (row) * bytesPerRow, - srcBytes + static_cast (row) * mapped.RowPitch, - bytesPerRow); - } - - m_gpuContext->Unmap (target.stagingTexture.Get(), 0); - - return true; - } - private: - OffscreenContextSlot* acquireOffscreenContext() - { - for (const auto& slot : m_offscreenContextPool) - { - if (! slot->frameActive) - return slot.get(); - } - - auto slot = std::make_unique(); - slot->renderContext = rive::gpu::RenderContextD3DImpl::MakeContext (m_gpu, m_gpuContext, m_renderContextOptions); - if (slot->renderContext == nullptr) - return nullptr; - - auto* result = slot.get(); - m_offscreenContextPool.push_back (std::move (slot)); - return result; - } - const bool m_isHeadless; - Options m_options; - rive::gpu::D3DContextOptions m_renderContextOptions; ComPtr m_d3dFactory; ComPtr m_gpu; ComPtr m_gpuContext; ComPtr m_swapchain; ComPtr m_readbackTexture; ComPtr m_headlessDrawTexture; + GpuDevice::Ptr m_gpuContextPtr; std::unique_ptr m_renderContext; - std::vector> m_offscreenContextPool; - std::unique_ptr m_oreContext; rive::rcp m_renderTarget; }; -std::unique_ptr yup_constructDirect3DGraphicsContext (GraphicsContext::Options fiddleOptions) +std::unique_ptr yup_constructDirect3DGraphicsContext (GpuDevice::Options fiddleOptions, GpuDevice::Ptr existingGpu) { - // Create a DXGIFactory object. ComPtr factory; VERIFY_OK (CreateDXGIFactory (__uuidof (IDXGIFactory2), reinterpret_cast (factory.ReleaseAndGetAddressOf()))); @@ -403,7 +181,6 @@ std::unique_ptr yup_constructDirect3DGraphicsContext (GraphicsC if (fiddleOptions.disableRasterOrdering) { contextOptions.disableRasterizerOrderedViews = true; - // Also disable typed UAVs in atomic mode, to get more complete test coverage. contextOptions.disableTypedUAVLoadStore = true; } @@ -411,7 +188,6 @@ std::unique_ptr yup_constructDirect3DGraphicsContext (GraphicsC { adapter->GetDesc (&adapterDesc); contextOptions.isIntel = adapterDesc.VendorId == 0x163C || adapterDesc.VendorId == 0x8086 || adapterDesc.VendorId == 0x8087; - break; } @@ -424,28 +200,15 @@ std::unique_ptr yup_constructDirect3DGraphicsContext (GraphicsC creationFlags |= D3D11_CREATE_DEVICE_DEBUG; #endif - VERIFY_OK (D3D11CreateDevice (adapter.Get(), - D3D_DRIVER_TYPE_UNKNOWN, - nullptr, - creationFlags, - featureLevels, - std::size (featureLevels), - D3D11_SDK_VERSION, - gpu.ReleaseAndGetAddressOf(), - nullptr, - gpuContext.ReleaseAndGetAddressOf())); + VERIFY_OK (D3D11CreateDevice (adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, creationFlags, featureLevels, std::size (featureLevels), D3D11_SDK_VERSION, gpu.ReleaseAndGetAddressOf(), nullptr, gpuContext.ReleaseAndGetAddressOf())); + if (! gpu || ! gpuContext) return nullptr; printf ("D3D device: %S\n", adapterDesc.Description); return std::make_unique ( - std::move (factory), - std::move (gpu), - std::move (gpuContext), - fiddleOptions.allowHeadlessRendering, - contextOptions, - fiddleOptions); + std::move (factory), std::move (gpu), std::move (gpuContext), fiddleOptions.allowHeadlessRendering, contextOptions, fiddleOptions, std::move (existingGpu)); } } // namespace yup diff --git a/modules/yup_graphics/native/yup_GraphicsContext_dawn.cpp b/modules/yup_graphics/native/yup_GraphicsContext_dawn.cpp index 22b4f56d3..0a4ffac69 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_dawn.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_dawn.cpp @@ -44,24 +44,18 @@ static void print_device_error (WGPUErrorType errorType, const char* message, vo case WGPUErrorType_Validation: errorTypeName = "Validation"; break; - case WGPUErrorType_OutOfMemory: errorTypeName = "Out of memory"; break; - case WGPUErrorType_Unknown: errorTypeName = "Unknown"; break; - case WGPUErrorType_DeviceLost: errorTypeName = "Device lost"; break; - default: - RIVE_UNREACHABLE(); return; } - printf ("%s error: %s\n", errorTypeName, message); } @@ -79,15 +73,11 @@ static void device_log_callback (WGPULoggingType type, const char* message, void extern float GetDawnWindowBackingScaleFactor (GLFWwindow*, bool retina); extern std::unique_ptr SetupDawnWindowAndGetSurfaceDescriptor (GLFWwindow*, bool retina); #else - #define GLFW_EXPOSE_NATIVE_WIN32 #include #include -static float GetDawnWindowBackingScaleFactor (GLFWwindow*, bool retina) -{ - return 1; -} +static float GetDawnWindowBackingScaleFactor (GLFWwindow*, bool retina) { return 1; } static std::unique_ptr SetupDawnWindowAndGetSurfaceDescriptor (GLFWwindow* window, bool retina) { @@ -101,9 +91,15 @@ static std::unique_ptr SetupDawnWindowAndGetSurfaceDescript class LowLevelRenderContextDawnPLS : public GraphicsContext { public: - LowLevelRenderContextDawnPLS (Options options) + LowLevelRenderContextDawnPLS (Options options, GpuDevice::Ptr existingGpu = {}) : m_options (options) { + // Obtain or create the GpuDevice + if (existingGpu != nullptr) + m_gpuContext = std::move (existingGpu); + else + m_gpuContext = GpuDevice::create (GpuPlatform::WebGPU, options); + WGPUInstanceDescriptor instanceDescriptor {}; instanceDescriptor.features.timedWaitAnyEnable = true; m_instance = std::make_unique (&instanceDescriptor); @@ -112,14 +108,12 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext .powerPreference = wgpu::PowerPreference::HighPerformance, }; - // Get an adapter for the backend to use, and create the device. auto adapters = m_instance->EnumerateAdapters (&adapterOptions); wgpu::DawnAdapterPropertiesPowerPreference power_props {}; wgpu::AdapterProperties adapterProperties {}; adapterProperties.nextInChain = &power_props; - // Find the first adapter which satisfies the adapterType requirement. auto isAdapterType = [&adapterProperties] (const auto& adapter) -> bool { adapter.GetProperties (&adapterProperties); @@ -129,45 +123,22 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext auto preferredAdapter = std::find_if (adapters.begin(), adapters.end(), isAdapterType); if (preferredAdapter == adapters.end()) { - fprintf (stderr, "Failed to find an adapter! Please try another adapter type.\n"); + fprintf (stderr, "Failed to find an adapter!\n"); return; } - std::vector enableToggleNames = { - "allow_unsafe_apis", - "turn_off_vsync", - // "skip_validation", - }; - + std::vector enableToggleNames = { "allow_unsafe_apis", "turn_off_vsync" }; std::vector disabledToggleNames; WGPUDawnTogglesDescriptor toggles = { - .chain = { - .next = nullptr, - .sType = WGPUSType_DawnTogglesDescriptor, - }, + .chain = { .next = nullptr, .sType = WGPUSType_DawnTogglesDescriptor }, .enabledToggleCount = enableToggleNames.size(), .enabledToggles = enableToggleNames.data(), .disabledToggleCount = disabledToggleNames.size(), .disabledToggles = disabledToggleNames.data(), }; - std::vector requiredFeatures = { - // WGPUFeatureName_IndirectFirstInstance, - // WGPUFeatureName_ShaderF16, - // WGPUFeatureName_BGRA8UnormStorage, - // WGPUFeatureName_Float32Filterable, - // WGPUFeatureName_DawnInternalUsages, - // WGPUFeatureName_DawnMultiPlanarFormats, - // WGPUFeatureName_DawnNative, - // WGPUFeatureName_ImplicitDeviceSynchronization, - WGPUFeatureName_SurfaceCapabilities, - // WGPUFeatureName_TransientAttachments, - // WGPUFeatureName_DualSourceBlending, - // WGPUFeatureName_Norm16TextureFormats, - // WGPUFeatureName_HostMappedPointer, - // WGPUFeatureName_ChromiumExperimentalReadWriteStorageTexture, - }; + std::vector requiredFeatures = { WGPUFeatureName_SurfaceCapabilities }; WGPUDeviceDescriptor deviceDesc = { .nextInChain = reinterpret_cast (&toggles), @@ -185,13 +156,13 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext m_device = wgpu::Device::Acquire (m_backendDevice); m_queue = m_device.GetQueue(); - m_plsContext = - PLSRenderContextWebGPUImpl::MakeContext (m_device, - m_queue, - PLSRenderContextWebGPUImpl::ContextOptions()); + m_plsContext = PLSRenderContextWebGPUImpl::MakeContext ( + m_device, m_queue, PLSRenderContextWebGPUImpl::ContextOptions()); } - Api getApi() const noexcept override { return Api::WebGPU; } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::WebGPU; } + + GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContext; } Factory* factory() override { return m_plsContext.get(); } @@ -203,7 +174,6 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext { DawnProcTable backendProcs = dawn::native::GetProcs(); - // Create the swapchain auto surfaceChainedDesc = SetupDawnWindowAndGetSurfaceDescriptor (window, m_options.retinaDisplay); WGPUSurfaceDescriptor surfaceDesc = { .nextInChain = reinterpret_cast (surfaceChainedDesc.get()), @@ -215,7 +185,7 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext .format = WGPUTextureFormat_BGRA8Unorm, .width = static_cast (width), .height = static_cast (height), - .presentMode = WGPUPresentMode_Immediate, // No vsync. + .presentMode = WGPUPresentMode_Immediate, }; if (m_options.enableReadPixels) @@ -239,38 +209,22 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext { assert (m_swapchain.GetCurrentTexture().GetWidth() == m_renderTarget->width()); assert (m_swapchain.GetCurrentTexture().GetHeight() == m_renderTarget->height()); - m_renderTarget->setTargetTextureView (m_swapchain.GetCurrentTextureView()); - frameDescriptor.renderTarget = m_renderTarget; - m_plsContext->beginFrame (std::move (frameDescriptor)); } void end (void* window) override { m_plsContext->flush(); - m_swapchain.Present(); } - void tick() override - { - m_device.Tick(); - } - - std::unique_ptr createOffscreenTarget (int, int) override { return nullptr; } - - std::unique_ptr createRenderableTarget (int, int) override { return nullptr; } - - void beginOffscreen (OffscreenTarget&, const rive::gpu::RenderContext::FrameDescriptor&) override {} - - void endOffscreen (OffscreenTarget&) override {} - - bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override { return false; } + void tick() override { m_device.Tick(); } private: - const LowLevelRenderContext::Options m_options; + Options m_options; + GpuDevice::Ptr m_gpuContext; WGPUDevice m_backendDevice = {}; wgpu::Device m_device = {}; wgpu::Queue m_queue = {}; @@ -281,9 +235,9 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext wgpu::Buffer m_pixelReadBuff; }; -std::unique_ptr yup_constructDawnGraphicsContext (GraphicsContext::Options options) +std::unique_ptr yup_constructDawnGraphicsContext (GpuDevice::Options options, GpuDevice::Ptr existingGpu) { - return std::make_unique (options); + return std::make_unique (options, std::move (existingGpu)); } } // namespace yup diff --git a/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp b/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp index c96071120..155b6b45a 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp @@ -188,9 +188,14 @@ class NoOpRenderer : public rive::Renderer class NoOpGraphicsContext : public GraphicsContext { public: - NoOpGraphicsContext() = default; + NoOpGraphicsContext() + { + gpuCtx = GpuDevice::create (GpuPlatform::Headless, {}); + } + + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Headless; } - Api getApi() const noexcept override { return Api::Headless; } + GpuDevice::Ptr getGpuDevice() const noexcept override { return gpuCtx; } rive::Factory* factory() override { @@ -224,36 +229,14 @@ class NoOpGraphicsContext : public GraphicsContext { } - std::unique_ptr createOffscreenTarget (int, int) override - { - return nullptr; - } - - std::unique_ptr createRenderableTarget (int, int) override - { - return nullptr; - } - - void beginOffscreen (OffscreenTarget&, const rive::gpu::RenderContext::FrameDescriptor&) override - { - } - - void endOffscreen (OffscreenTarget&) override - { - } - - bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override - { - return false; - } - private: NoOpFactory noOpFactory; + GpuDevice::Ptr gpuCtx; }; //============================================================================== -std::unique_ptr yup_constructHeadlessGraphicsContext (GraphicsContext::Options fiddleOptions) +std::unique_ptr yup_constructHeadlessGraphicsContext (GpuDevice::Options fiddleOptions, GpuDevice::Ptr) { return std::make_unique(); } diff --git a/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp b/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp index 7cd340630..408374d9b 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp @@ -22,7 +22,6 @@ #if YUP_RIVE_USE_METAL #include "rive/renderer/rive_renderer.hpp" #include "rive/renderer/metal/render_context_metal_impl.h" -#include "rive/renderer/ore/ore_context_metal.hpp" #if YUP_MAC #include "yup_RenderShader_mac.c" @@ -79,9 +78,24 @@ class LowLevelRenderContextMetal : public GraphicsContext public: //============================================================================== - LowLevelRenderContextMetal (Options fiddleOptions) + LowLevelRenderContextMetal (Options fiddleOptions, GpuDevice::Ptr existingGpu = {}) : m_fiddleOptions (fiddleOptions) { + // Obtain or create the GpuDevice + if (existingGpu != nullptr) + { + m_gpuContext = std::move (existingGpu); + } + else + { + m_gpuContext = GpuDevice::create (GpuPlatform::Metal, fiddleOptions); + } + + // Own GpuDeviceMetal knows the native device/queue — extract them. + // GpuDeviceMetal exposes getDevice()/getCommandQueue() for sharing. + jassert (m_gpuContext != nullptr); + + // Create the Rive render context (needed for windowed rendering + vector content) if (m_fiddleOptions.synchronousShaderCompilations) m_renderContextOptions.shaderCompilationMode = rive::gpu::ShaderCompilationMode::alwaysSynchronous; @@ -89,8 +103,8 @@ class LowLevelRenderContextMetal : public GraphicsContext m_renderContextOptions.disableFramebufferReads = true; m_renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (m_gpu, m_renderContextOptions); - m_oreContext = rive::ore::ContextMetal::Make (m_gpu, m_queue); + // Compile PLS shaders for the fullscreen blit pipeline NSError* error = nil; dispatch_data_t metallibData = dispatch_data_create ( @@ -139,7 +153,9 @@ class LowLevelRenderContextMetal : public GraphicsContext //============================================================================== - Api getApi() const noexcept override { return Api::Metal; } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Metal; } + + GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContext; } //============================================================================== @@ -149,8 +165,6 @@ class LowLevelRenderContextMetal : public GraphicsContext rive::gpu::RenderTarget* renderTarget() override { return m_renderTarget.get(); } - rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } - //============================================================================== void onSizeChanged (void* window, int width, int height, float dpiScale, uint32_t sampleCount) override @@ -269,218 +283,11 @@ class LowLevelRenderContextMetal : public GraphicsContext m_renderTarget->setTargetTexture (nil); } - //============================================================================== - - struct OffscreenContextSlot - { - std::unique_ptr renderContext; - bool frameActive = false; - }; - - struct OffscreenTargetMetal : public RenderableTarget - { - int width = 0; - int height = 0; - id stagingTexture = nil; - rive::rcp renderCanvas; - rive::gpu::RenderContext* renderContext = nullptr; - OffscreenContextSlot* contextSlot = nullptr; - - int getWidth() const noexcept override { return width; } - - int getHeight() const noexcept override { return height; } - - rive::gpu::RenderTarget* getRenderTarget() noexcept override - { - return renderCanvas != nullptr ? renderCanvas->renderTarget() : nullptr; - } - - rive::gpu::RenderContext* getRenderContext() noexcept override - { - return renderContext; - } - - rive::rcp getRenderCanvas() noexcept override - { - return renderCanvas; - } - - rive::rcp adoptAsTexture() override - { - if (renderCanvas == nullptr) - return nullptr; - - return renderCanvas->renderImage()->refTexture(); - } - - id targetTexture() const - { - if (renderCanvas == nullptr) - return nil; - - if (auto* target = static_cast (renderCanvas->renderTarget())) - return target->targetTexture(); - - return nil; - } - }; - - std::unique_ptr createOffscreenTarget (int width, int height) override - { - if (width <= 0 || height <= 0 || m_renderContext == nullptr) - return nullptr; - - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = nullptr; - target->contextSlot = nullptr; - target->renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), - static_cast (height)); - if (target->renderCanvas == nullptr) - return nullptr; - - return target; - } - - std::unique_ptr createRenderableTarget (int width, int height) override - { - if (width <= 0 || height <= 0) - return nullptr; - - auto* contextSlot = acquireOffscreenContext(); - if (contextSlot == nullptr) - return nullptr; - - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = contextSlot->renderContext.get(); - target->contextSlot = contextSlot; - target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), - static_cast (height)); - if (target->renderCanvas == nullptr) - return nullptr; - - return target; - } - - void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override - { - auto& target = static_cast (baseTarget); - auto* renderContext = target.getRenderContext(); - - if (renderContext == nullptr || target.contextSlot == nullptr || target.contextSlot->frameActive) - return; - - renderContext->beginFrame (frameDesc); - target.contextSlot->frameActive = true; - } - - void endOffscreen (OffscreenTarget& baseTarget) override - { - auto& target = static_cast (baseTarget); - auto* renderContext = target.getRenderContext(); - - if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) - return; - - id commandBuffer = [m_queue commandBuffer]; - renderContext->flush ({ .renderTarget = target.getRenderTarget(), .externalCommandBuffer = (__bridge void*) commandBuffer }); - [commandBuffer commit]; - target.contextSlot->frameActive = false; - } - - bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override - { - auto& target = static_cast (baseTarget); - - if (dst == nullptr) - return false; - - id srcTexture = target.targetTexture(); - if (srcTexture == nil) - return false; - - if (target.stagingTexture == nil) - { - MTLTextureDescriptor* stagingDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm - width:static_cast (target.width) - height:static_cast (target.height) - mipmapped:NO]; - stagingDesc.usage = MTLTextureUsageShaderRead; -#if YUP_IOS - stagingDesc.storageMode = MTLStorageModeShared; -#else - stagingDesc.storageMode = MTLStorageModeManaged; -#endif - target.stagingTexture = [m_gpu newTextureWithDescriptor:stagingDesc]; - if (target.stagingTexture == nil) - return false; - } - - const auto w = static_cast (target.width); - const auto h = static_cast (target.height); - const size_t bytesPerRow = w * 4u; - - if (dstSize < bytesPerRow * h) - return false; - - // Copy the rendered target into a CPU-readable staging texture and block - // until the GPU is done. This is the only path that requires a CPU/GPU - // sync, so the stall is paid only when pixels are actually read back. - id commandBuffer = [m_queue commandBuffer]; - - id blitEncoder = [commandBuffer blitCommandEncoder]; - [blitEncoder copyFromTexture:srcTexture - sourceSlice:0 - sourceLevel:0 - sourceOrigin:MTLOriginMake (0, 0, 0) - sourceSize:MTLSizeMake (w, h, 1) - toTexture:target.stagingTexture - destinationSlice:0 - destinationLevel:0 - destinationOrigin:MTLOriginMake (0, 0, 0)]; -#if YUP_MAC - [blitEncoder synchronizeResource:target.stagingTexture]; -#endif - [blitEncoder endEncoding]; - - [commandBuffer commit]; - [commandBuffer waitUntilCompleted]; - - [target.stagingTexture getBytes:dst - bytesPerRow:bytesPerRow - fromRegion:MTLRegionMake2D (0, 0, w, h) - mipmapLevel:0]; - - return true; - } - private: - OffscreenContextSlot* acquireOffscreenContext() - { - for (const auto& slot : m_offscreenContextPool) - { - if (! slot->frameActive) - return slot.get(); - } - - auto slot = std::make_unique(); - slot->renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (m_gpu, m_renderContextOptions); - if (slot->renderContext == nullptr) - return nullptr; - - auto* result = slot.get(); - m_offscreenContextPool.push_back (std::move (slot)); - return result; - } - const Options m_fiddleOptions; rive::gpu::RenderContextMetalImpl::ContextOptions m_renderContextOptions; + GpuDevice::Ptr m_gpuContext; std::unique_ptr m_renderContext; - std::vector> m_offscreenContextPool; - std::unique_ptr m_oreContext; id m_gpu = MTLCreateSystemDefaultDevice(); id m_queue = [m_gpu newCommandQueue]; CAMetalLayer* m_swapchain = nil; @@ -493,9 +300,10 @@ class LowLevelRenderContextMetal : public GraphicsContext //============================================================================== -std::unique_ptr yup_constructMetalGraphicsContext (GraphicsContext::Options fiddleOptions) +std::unique_ptr yup_constructMetalGraphicsContext (GpuDevice::Options fiddleOptions, + GpuDevice::Ptr existingGpu) { - return std::make_unique (fiddleOptions); + return std::make_unique (fiddleOptions, std::move (existingGpu)); } } // namespace yup diff --git a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp index 0c19a666f..b74933c08 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp @@ -25,7 +25,6 @@ #include "rive/renderer/gl/render_buffer_gl_impl.hpp" #include "rive/renderer/gl/render_context_gl_impl.hpp" #include "rive/renderer/gl/render_target_gl.hpp" -#include "rive/renderer/ore/ore_context_gl.hpp" #include "rive/renderer/render_context_impl.hpp" #include "rive/renderer/rive_render_image.hpp" #include @@ -47,24 +46,16 @@ static void GLAPIENTRY err_msg_callback (GLenum source, { printf ("GL ERROR: %s\n", message); fflush (stdout); - assert (false); } else if (type == GL_DEBUG_TYPE_PERFORMANCE_KHR) { - if (strcmp (message, - "API_ID_REDUNDANT_FBO performance warning has been generated. Redundant state " - "change in glBindFramebuffer API call, FBO 0, \"\", already bound.") + if (strcmp (message, "API_ID_REDUNDANT_FBO performance warning has been generated. Redundant state " + "change in glBindFramebuffer API call, FBO 0, \"\", already bound.") == 0) - { return; - } - if (strstr (message, "is being recompiled based on GL state.")) - { return; - } - printf ("GL PERF: %s\n", message); fflush (stdout); } @@ -73,20 +64,13 @@ static void GLAPIENTRY err_msg_callback (GLenum source, //============================================================================== -/** - * OpenGL Graphics Context implementation that renders Rive content into an - * offscreen framebuffer with attached texture, then blits the result to the - * main framebuffer. This approach enables optimizations like dirty rect - * rendering and matches the approach used in other backends. - */ class LowLevelRenderContextGL : public GraphicsContext { public: - LowLevelRenderContextGL (Options options) + LowLevelRenderContextGL (Options options, GpuDevice::Ptr existingGpu = {}) : m_options (options) { #if RIVE_DESKTOP_GL - // Load the OpenGL API using glad. if (! gladLoadCustomLoader ((GLADloadfunc) options.loaderFunction)) { fprintf (stderr, "Failed to initialize glad.\n"); @@ -94,6 +78,13 @@ class LowLevelRenderContextGL : public GraphicsContext } #endif + // Obtain or create the GpuDevice for offscreen/RHI operations + if (existingGpu != nullptr) + m_gpuContext = std::move (existingGpu); + else + m_gpuContext = GpuDevice::create (getPlatform(), options); + + // Create the main window render context m_renderContext = rive::gpu::RenderContextGLImpl::MakeContext (m_renderContextOptions); if (! m_renderContext) { @@ -101,15 +92,12 @@ class LowLevelRenderContextGL : public GraphicsContext return; } - m_oreContext = rive::ore::ContextGL::Make(); - printf ("GL_VENDOR: %s\n", glGetString (GL_VENDOR)); printf ("GL_RENDERER: %s\n", glGetString (GL_RENDERER)); printf ("GL_VERSION: %s\n", glGetString (GL_VERSION)); #if RIVE_DESKTOP_GL printf ("GL_ANGLE_shader_pixel_local_storage_coherent: %i\n", GLAD_GL_ANGLE_shader_pixel_local_storage_coherent); - #if DEBUG if (GLAD_GL_KHR_debug) { @@ -133,41 +121,28 @@ class LowLevelRenderContextGL : public GraphicsContext cleanupOffscreenResources(); } - Api getApi() const noexcept override + GpuPlatform getPlatform() const noexcept override { #if RIVE_ANDROID || RIVE_WEBGL - return Api::OpenGLES; + return GpuPlatform::OpenGLES; #else - return Api::OpenGL; + return GpuPlatform::OpenGL; #endif } - rive::Factory* factory() override - { - return m_renderContext.get(); - } + GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContext; } - rive::gpu::RenderContext* renderContext() override - { - return m_renderContext.get(); - } + rive::Factory* factory() override { return m_renderContext.get(); } - rive::gpu::RenderTarget* renderTarget() override - { - return m_offscreenRenderTarget.get(); - } + rive::gpu::RenderContext* renderContext() override { return m_renderContext.get(); } - rive::ore::Context* gpuContext() const noexcept override - { - return m_oreContext.get(); - } + rive::gpu::RenderTarget* renderTarget() override { return m_offscreenRenderTarget.get(); } void onSizeChanged (void* window, int width, int height, float dpiScale, uint32_t sampleCount) override { m_width = width; m_height = height; m_sampleCount = sampleCount; - createOffscreenResources(); } @@ -179,228 +154,18 @@ class LowLevelRenderContextGL : public GraphicsContext void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDescriptor) override { m_renderContext->static_impl_cast()->invalidateGLState(); - m_renderContext->beginFrame (frameDescriptor); } void end (void*) override { - // Mid-frame GpuCanvas / ore work shares the one real GL context and unbinds the fixed texture units - // (endOffscreen's unbindGLInternalResources wipes units 0..N). Rive's flush assumes its internal - // textures (tessellation/gradient/feather/atlas) are still bound at those units, so rebind them - // right before flushing. m_renderContext->static_impl_cast()->invalidateGLState(); - m_renderContext->flush ({ m_offscreenRenderTarget.get() }); - m_renderContext->static_impl_cast()->unbindGLInternalResources(); - blitToMainFramebuffer(); } - //============================================================================== - - struct OffscreenContextSlot - { - std::unique_ptr renderContext; - bool frameActive = false; - }; - - struct OffscreenTargetGL : public RenderableTarget - { - int width = 0; - int height = 0; - rive::rcp renderCanvas; - rive::gpu::RenderContext* renderContext = nullptr; - rive::gpu::RenderContext* mirrorContext = nullptr; - mutable rive::rcp sampledMirrorTex; - OffscreenContextSlot* contextSlot = nullptr; - - int getWidth() const noexcept override { return width; } - - int getHeight() const noexcept override { return height; } - - rive::gpu::RenderTarget* getRenderTarget() noexcept override - { - return renderCanvas != nullptr ? renderCanvas->renderTarget() : nullptr; - } - - rive::gpu::RenderContext* getRenderContext() noexcept override - { - return renderContext; - } - - rive::rcp getRenderCanvas() noexcept override - { - return renderCanvas; - } - - rive::rcp adoptAsTexture() override - { - if (renderCanvas == nullptr) - return nullptr; - - return renderCanvas->renderImage()->refTexture(); - } - - rive::rcp getOrCreateSampledTexture() override - { -#if defined(ORE_BACKEND_GL) && defined(RIVE_CANVAS) - if (sampledMirrorTex != nullptr) - return sampledMirrorTex; - - if (mirrorContext == nullptr || renderCanvas == nullptr) - return nullptr; - - auto renderImage = renderCanvas->renderImage(); - if (renderImage == nullptr) - return nullptr; - - if (auto sourceTex = renderImage->refTexture()) - { - auto mirrorImage = rive::getCanvasImportMirrorGL ( - mirrorContext, sourceTex.get(), (uint32_t) width, (uint32_t) height); - - if (mirrorImage != nullptr) - sampledMirrorTex = mirrorImage->refTexture(); - } - - return sampledMirrorTex; -#else - return nullptr; -#endif - } - - rive::rcp getSampledTexture() const override - { - return sampledMirrorTex; - } - }; - - std::unique_ptr createOffscreenTarget (int width, int height) override - { - if (width <= 0 || height <= 0 || m_renderContext == nullptr) - return nullptr; - - auto renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); - if (renderCanvas == nullptr) - return nullptr; - - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = nullptr; - target->mirrorContext = m_renderContext.get(); - target->contextSlot = nullptr; - target->renderCanvas = std::move (renderCanvas); - return target; - } - - std::unique_ptr createRenderableTarget (int width, int height) override - { - if (width <= 0 || height <= 0) - return nullptr; - - auto* contextSlot = acquireOffscreenContext(); - if (contextSlot == nullptr) - return nullptr; - - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = contextSlot->renderContext.get(); - target->mirrorContext = contextSlot->renderContext.get(); - target->contextSlot = contextSlot; - - target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); - if (target->renderCanvas == nullptr) - return nullptr; - - return target; - } - - void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override - { - auto& target = static_cast (baseTarget); - - auto renderContext = target.getRenderContext(); - if (renderContext == nullptr || target.contextSlot == nullptr || target.contextSlot->frameActive) - return; - - renderContext->static_impl_cast()->invalidateGLState(); - renderContext->beginFrame (frameDesc); - target.contextSlot->frameActive = true; - } - - void endOffscreen (OffscreenTarget& baseTarget) override - { - auto& target = static_cast (baseTarget); - - auto renderContext = target.getRenderContext(); - if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) - return; - - // Rebind this context's internal textures right before flushing: any other context's work since beginOffscreen() - // (another canvas, ore passes, the main context) may have unbound the shared texture units. - renderContext->static_impl_cast()->invalidateGLState(); - - renderContext->flush ({ target.getRenderTarget() }); - - renderContext->static_impl_cast()->unbindGLInternalResources(); - target.contextSlot->frameActive = false; - } - - bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override - { - auto& target = static_cast (baseTarget); - - if (target.getRenderTarget() == nullptr || dst == nullptr) - return false; - - const size_t bytesPerRow = static_cast (target.width) * 4u; - if (dstSize < bytesPerRow * static_cast (target.height)) - return false; - - auto* renderTarget = static_cast (target.getRenderTarget()); - renderTarget->bindDestinationFramebuffer (GL_READ_FRAMEBUFFER); - glReadPixels (0, 0, target.width, target.height, GL_RGBA, GL_UNSIGNED_BYTE, dst); - glBindFramebuffer (GL_READ_FRAMEBUFFER, 0); - - // Flip rows top-to-bottom (GL returns bottom-to-top) - auto* bytes = static_cast (dst); - std::vector rowBuffer (bytesPerRow); - const int halfHeight = target.height / 2; - for (int i = 0; i < halfHeight; ++i) - { - uint8_t* top = bytes + static_cast (i) * bytesPerRow; - uint8_t* bottom = bytes + static_cast (target.height - 1 - i) * bytesPerRow; - std::memcpy (rowBuffer.data(), top, bytesPerRow); - std::memcpy (top, bottom, bytesPerRow); - std::memcpy (bottom, rowBuffer.data(), bytesPerRow); - } - - return true; - } - private: - OffscreenContextSlot* acquireOffscreenContext() - { - for (const auto& slot : m_offscreenContextPool) - { - if (! slot->frameActive) - return slot.get(); - } - - auto slot = std::make_unique(); - slot->renderContext = rive::gpu::RenderContextGLImpl::MakeContext (m_renderContextOptions); - if (slot->renderContext == nullptr) - return nullptr; - - auto* result = slot.get(); - m_offscreenContextPool.push_back (std::move (slot)); - return result; - } - void createOffscreenResources() { if (m_width <= 0 || m_height <= 0) @@ -411,7 +176,6 @@ class LowLevelRenderContextGL : public GraphicsContext cleanupOffscreenResources(); - // Create offscreen texture glGenTextures (1, &m_offscreenTexture); glBindTexture (GL_TEXTURE_2D, m_offscreenTexture); glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, m_width, m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); @@ -420,27 +184,24 @@ class LowLevelRenderContextGL : public GraphicsContext glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - // Check for GL errors after texture creation GLenum error = glGetError(); if (error != GL_NO_ERROR) fprintf (stderr, "GL error after texture creation: 0x%x\n", error); glBindTexture (GL_TEXTURE_2D, 0); - // Create framebuffer and attach texture glGenFramebuffers (1, &m_offscreenFramebuffer); glBindFramebuffer (GL_FRAMEBUFFER, m_offscreenFramebuffer); glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_offscreenTexture, 0); - // Check framebuffer completeness GLenum status = glCheckFramebufferStatus (GL_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) fprintf (stderr, "Offscreen framebuffer is not complete: 0x%x\n", status); glBindFramebuffer (GL_FRAMEBUFFER, 0); - // Create Rive render target that uses our offscreen framebuffer - m_offscreenRenderTarget = rive::make_rcp (m_width, m_height, m_offscreenFramebuffer, m_sampleCount); + m_offscreenRenderTarget = rive::make_rcp ( + m_width, m_height, m_offscreenFramebuffer, m_sampleCount); } void cleanupOffscreenResources() @@ -450,13 +211,11 @@ class LowLevelRenderContextGL : public GraphicsContext glDeleteFramebuffers (1, &m_offscreenFramebuffer); m_offscreenFramebuffer = 0; } - if (m_offscreenTexture != 0) { glDeleteTextures (1, &m_offscreenTexture); m_offscreenTexture = 0; } - m_offscreenRenderTarget.reset(); } @@ -467,21 +226,17 @@ class LowLevelRenderContextGL : public GraphicsContext fprintf (stderr, "blitToMainFramebuffer: Invalid program or texture\n"); return; } - glBindFramebuffer (GL_READ_FRAMEBUFFER, m_offscreenFramebuffer); glBindFramebuffer (GL_DRAW_FRAMEBUFFER, 0); glBlitFramebuffer (0, 0, m_width, m_height, 0, 0, m_width, m_height, GL_COLOR_BUFFER_BIT, GL_NEAREST); } -private: Options m_options; rive::gpu::RenderContextGLImpl::ContextOptions m_renderContextOptions; + GpuDevice::Ptr m_gpuContext; std::unique_ptr m_renderContext; - std::vector> m_offscreenContextPool; - std::unique_ptr m_oreContext; rive::rcp m_offscreenRenderTarget; - // Offscreen rendering resources GLuint m_offscreenFramebuffer = 0; GLuint m_offscreenTexture = 0; int m_width = 0; @@ -491,9 +246,9 @@ class LowLevelRenderContextGL : public GraphicsContext //============================================================================== -std::unique_ptr yup_constructOpenGLGraphicsContext (GraphicsContext::Options options) +std::unique_ptr yup_constructOpenGLGraphicsContext (GpuDevice::Options options, GpuDevice::Ptr existingGpu) { - return std::make_unique (options); + return std::make_unique (options, std::move (existingGpu)); } } // namespace yup diff --git a/modules/yup_graphics/native/yup_GraphicsContext_webgpu.cpp b/modules/yup_graphics/native/yup_GraphicsContext_webgpu.cpp index f8458c87f..c6fdce901 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_webgpu.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_webgpu.cpp @@ -19,45 +19,9 @@ ============================================================================== */ -/* - ============================================================================== - - Native WebGPU GraphicsContext backend for Emscripten. - - Renders Rive content through the browser's native WebGPU API using the - Emdawnwebgpu port (Dawn's implementation of webgpu.h for Emscripten), without - a native Dawn or wagyu build. - - Requirements: - - Build with `--use-port=emdawnwebgpu` (passed at BOTH compile and link time) - and define `RIVE_WEBGPU=2` so rive's `RenderContextWebGPUImpl` compiles - against the Dawn-style webgpu.h. The legacy `-sUSE_WEBGPU=1` bindings - (`RIVE_WEBGPU=1`) were removed from Emscripten and are no longer supported. - The `YUP_ENABLE_WEBGPU` CMake option wires these flags in globally. - - The device must be pre-initialized in JavaScript before `main()` runs and - exposed as `Module.preinitializedWebGPUDevice`; the yup `shell.html` does - this via a `preRun` run-dependency. Custom shells must replicate it. - - Rendering model: - - Rive renders into a persistent offscreen texture (stable across frames) so - dirty-rect / partial-update frames accumulate correctly. Each frame the - full offscreen image is copied into the browser surface's current texture, - which is NOT preserved across frames (WebGPU rotates surface textures, so - the acquired texture would otherwise contain stale/uninitialized content - and produce flicker or black overpaint on undrawn areas). - - Known limitations: - - `readOffscreenPixels()` is unsupported: browser buffer mapping is - async-only and ASYNCIFY is disabled, so there is no way to block for the - GPU-to-CPU copy. It returns false. - - ============================================================================== -*/ - #if YUP_EMSCRIPTEN && RIVE_WEBGPU #include "rive/renderer/rive_renderer.hpp" #include "rive/renderer/webgpu/render_context_webgpu_impl.hpp" -#include "rive/renderer/ore/ore_context.hpp" #include "rive/renderer/rive_render_image.hpp" #include @@ -67,19 +31,14 @@ #include #include -#include namespace yup { -//============================================================================== - class LowLevelRenderContextWebGPU : public GraphicsContext { public: - //============================================================================== - - LowLevelRenderContextWebGPU (Options options) + LowLevelRenderContextWebGPU (Options options, GpuDevice::Ptr existingGpu = {}) : m_options (options) { m_device = wgpu::Device::Acquire (emscripten_webgpu_get_device()); @@ -92,6 +51,12 @@ class LowLevelRenderContextWebGPU : public GraphicsContext m_queue = m_device.GetQueue(); + // Obtain or create the GpuDevice for RHI/offscreen operations + if (existingGpu != nullptr) + m_gpuContext = std::move (existingGpu); + else + m_gpuContext = GpuDevice::create (GpuPlatform::WebGPU, options); + m_renderContext = rive::gpu::RenderContextWebGPUImpl::MakeContext ( {}, m_device, m_queue, rive::gpu::RenderContextWebGPUImpl::ContextOptions()); @@ -100,15 +65,11 @@ class LowLevelRenderContextWebGPU : public GraphicsContext fprintf (stderr, "WebGPU: failed to create a render context.\n"); return; } - - m_oreContext = m_renderContext->static_impl_cast()->makeOreContext(); } - //============================================================================== - - Api getApi() const noexcept override { return Api::WebGPU; } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::WebGPU; } - //============================================================================== + GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContext; } rive::Factory* factory() override { return m_renderContext.get(); } @@ -116,17 +77,11 @@ class LowLevelRenderContextWebGPU : public GraphicsContext rive::gpu::RenderTarget* renderTarget() override { return m_renderTarget.get(); } - rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } - - //============================================================================== - std::unique_ptr makeRenderer (int width, int height) override { return std::make_unique (m_renderContext.get()); } - //============================================================================== - void onSizeChanged (void*, int width, int height, float dpiScale, uint32_t) override { if (m_renderContext == nullptr || width <= 0 || height <= 0) @@ -154,14 +109,10 @@ class LowLevelRenderContextWebGPU : public GraphicsContext config.width = (uint32_t) width; config.height = (uint32_t) height; config.alphaMode = wgpu::CompositeAlphaMode::Auto; - config.presentMode = wgpu::PresentMode::Fifo; // only mode the browser guarantees + config.presentMode = wgpu::PresentMode::Fifo; m_surface.Configure (&config); - // Persistent offscreen render target. Rive renders here (accumulating - // dirty-rect partial updates); the full image is copied to the surface - // each frame. CopySrc feeds both that copy and rive's advanced-blend - // destination copies. wgpu::TextureDescriptor textureDesc = {}; textureDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; textureDesc.dimension = wgpu::TextureDimension::e2D; @@ -175,15 +126,12 @@ class LowLevelRenderContextWebGPU : public GraphicsContext ->makeRenderTarget (wgpu::TextureFormat::BGRA8Unorm, (uint32_t) width, (uint32_t) height); } - //============================================================================== - void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDescriptor) override { if (m_offscreenTextureView == nullptr || m_renderTarget == nullptr) return; m_renderTarget->setTargetTextureView (m_offscreenTextureView, m_offscreenTexture); - m_renderContext->beginFrame (frameDescriptor); } @@ -199,12 +147,9 @@ class LowLevelRenderContextWebGPU : public GraphicsContext wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder(); - // Rive records its render passes into the persistent offscreen texture. m_renderContext->flush ({ .renderTarget = m_renderTarget.get(), .externalCommandBuffer = encoder.Get() }); - // Copy the full accumulated offscreen image into the non-preserved - // surface texture, then submit both in one command buffer. wgpu::TexelCopyTextureInfo copySource = {}; copySource.texture = m_offscreenTexture; copySource.aspect = wgpu::TextureAspect::All; @@ -223,154 +168,12 @@ class LowLevelRenderContextWebGPU : public GraphicsContext wgpu::CommandBuffer commands = encoder.Finish(); m_queue.Submit (1, &commands); - // No present call: the browser composites the canvas when control returns - // to the event loop. m_renderTarget->setTargetTextureView ({}, {}); } - //============================================================================== - - struct OffscreenContextSlot - { - std::unique_ptr renderContext; - bool frameActive = false; - }; - - struct OffscreenTargetWebGPU : public RenderableTarget - { - int width = 0; - int height = 0; - rive::rcp renderCanvas; - rive::gpu::RenderContext* renderContext = nullptr; - OffscreenContextSlot* contextSlot = nullptr; - - int getWidth() const noexcept override { return width; } - - int getHeight() const noexcept override { return height; } - - rive::gpu::RenderTarget* getRenderTarget() noexcept override - { - return renderCanvas != nullptr ? renderCanvas->renderTarget() : nullptr; - } - - rive::gpu::RenderContext* getRenderContext() noexcept override - { - return renderContext; - } - - rive::rcp getRenderCanvas() noexcept override - { - return renderCanvas; - } - - rive::rcp adoptAsTexture() override - { - if (renderCanvas == nullptr) - return nullptr; - - return renderCanvas->renderImage()->refTexture(); - } - }; - - std::unique_ptr createOffscreenTarget (int width, int height) override - { - if (width <= 0 || height <= 0 || m_renderContext == nullptr) - return nullptr; - - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = nullptr; - target->contextSlot = nullptr; - target->renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), - static_cast (height)); - if (target->renderCanvas == nullptr) - return nullptr; - - return target; - } - - std::unique_ptr createRenderableTarget (int width, int height) override - { - if (width <= 0 || height <= 0) - return nullptr; - - auto* contextSlot = acquireOffscreenContext(); - if (contextSlot == nullptr) - return nullptr; - - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = contextSlot->renderContext.get(); - target->contextSlot = contextSlot; - target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), - static_cast (height)); - if (target->renderCanvas == nullptr) - return nullptr; - - return target; - } - - void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override - { - auto& target = static_cast (baseTarget); - auto* renderContext = target.getRenderContext(); - - if (renderContext == nullptr || target.contextSlot == nullptr || target.contextSlot->frameActive) - return; - - renderContext->beginFrame (frameDesc); - target.contextSlot->frameActive = true; - } - - void endOffscreen (OffscreenTarget& baseTarget) override - { - auto& target = static_cast (baseTarget); - auto* renderContext = target.getRenderContext(); - - if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) - return; - - wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder(); - - renderContext->flush ({ .renderTarget = target.getRenderTarget(), - .externalCommandBuffer = encoder.Get() }); - - wgpu::CommandBuffer commands = encoder.Finish(); - m_queue.Submit (1, &commands); - - target.contextSlot->frameActive = false; - } - - bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override - { - // GPU-to-CPU buffer mapping is async-only on the web; unsupported without - // ASYNCIFY. - return false; - } - private: - OffscreenContextSlot* acquireOffscreenContext() - { - for (const auto& slot : m_offscreenContextPool) - { - if (! slot->frameActive) - return slot.get(); - } - - auto slot = std::make_unique(); - slot->renderContext = rive::gpu::RenderContextWebGPUImpl::MakeContext ( - {}, m_device, m_queue, rive::gpu::RenderContextWebGPUImpl::ContextOptions()); - if (slot->renderContext == nullptr) - return nullptr; - - auto* result = slot.get(); - m_offscreenContextPool.push_back (std::move (slot)); - return result; - } - Options m_options; + GpuDevice::Ptr m_gpuContext; wgpu::Device m_device; wgpu::Queue m_queue; wgpu::Surface m_surface; @@ -379,16 +182,12 @@ class LowLevelRenderContextWebGPU : public GraphicsContext int m_width = 0; int m_height = 0; std::unique_ptr m_renderContext; - std::vector> m_offscreenContextPool; - std::unique_ptr m_oreContext; rive::rcp m_renderTarget; }; -//============================================================================== - -std::unique_ptr yup_constructWebGPUGraphicsContext (GraphicsContext::Options options) +std::unique_ptr yup_constructWebGPUGraphicsContext (GpuDevice::Options options, GpuDevice::Ptr existingGpu) { - return std::make_unique (options); + return std::make_unique (options, std::move (existingGpu)); } } // namespace yup diff --git a/modules/yup_graphics/rhi/yup_GpuCanvas.cpp b/modules/yup_graphics/rhi/yup_GpuCanvas.cpp index 6ce858dff..2a4d42b37 100644 --- a/modules/yup_graphics/rhi/yup_GpuCanvas.cpp +++ b/modules/yup_graphics/rhi/yup_GpuCanvas.cpp @@ -27,12 +27,16 @@ GpuCanvas::Ptr GpuCanvas::create (GraphicsContext& ctx, int width, int height) if (width <= 0 || height <= 0) return nullptr; + auto gpuCtx = ctx.getGpuDevice(); + if (gpuCtx == nullptr) + return nullptr; + // GpuCanvas needs a dedicated render context for the 2D drawing path. - auto renderable = ctx.createRenderableTarget (width, height); + auto renderable = gpuCtx->createRenderableTarget (width, height); if (renderable == nullptr) return nullptr; - auto target = GpuTarget::createFromTarget (ctx, std::move (renderable)); + auto target = GpuTarget::createFromTarget (gpuCtx, std::move (renderable)); if (target == nullptr) return nullptr; diff --git a/modules/yup_graphics/yup_graphics.cpp b/modules/yup_graphics/yup_graphics.cpp index a0daf1bdf..1910116f1 100644 --- a/modules/yup_graphics/yup_graphics.cpp +++ b/modules/yup_graphics/yup_graphics.cpp @@ -136,7 +136,6 @@ YUP_END_IGNORE_WARNINGS_GCC_LIKE //============================================================================== #include "context/yup_GraphicsContext.cpp" -#include "rhi/yup_GpuTexture.cpp" #include "primitives/yup_Path.cpp" #include "primitives/yup_CubicBezier.cpp" #include "fonts/yup_Font.cpp" @@ -155,14 +154,7 @@ YUP_END_IGNORE_WARNINGS_GCC_LIKE #include "svg/yup_SVGCssParser.cpp" #include "svg/yup_SVGParser.cpp" #include "drawables/yup_Drawable.cpp" -#include "rhi/yup_ShaderBindingMap.cpp" -#include "rhi/yup_GpuBuffer.cpp" -#include "rhi/yup_GpuPipeline.cpp" -#include "rhi/yup_GpuFrame.cpp" -#include "rhi/yup_GpuRenderPass.cpp" -#include "rhi/yup_GpuTarget.cpp" #include "rhi/yup_GpuCanvas.cpp" -#include "rhi/yup_GpuPipelineCache.cpp" //============================================================================== #if YUP_IMAGE_FORMAT_BMP diff --git a/modules/yup_graphics/yup_graphics.h b/modules/yup_graphics/yup_graphics.h index 3eb086b97..b9dc60bba 100644 --- a/modules/yup_graphics/yup_graphics.h +++ b/modules/yup_graphics/yup_graphics.h @@ -32,8 +32,8 @@ website: https://github.com/kunitoki/yup license: ISC - dependencies: yup_core yup_simd yup_shading rive rive_renderer libclipper2 - optionalDeps: libpng libjpeg libwebp libgif + dependencies: yup_core yup_simd yup_rhi yup_shading rive rive_renderer libclipper2 + optionalDeps: libpng libjpeg libwebp libgif libtiff appleFrameworks: Metal searchpaths: native @@ -47,6 +47,7 @@ #include #include +#include #include //============================================================================== @@ -193,7 +194,6 @@ class Context; #include "primitives/yup_CubicBezier.h" #include "fonts/yup_Font.h" #include "fonts/yup_StyledText.h" -#include "rhi/yup_GpuTexture.h" #include "imaging/yup_ImagePixelData.h" #include "imaging/yup_ImageMetadata.h" #include "imaging/yup_ImageFormat.h" @@ -209,18 +209,9 @@ class Context; #include "graphics/yup_StrokeCap.h" #include "graphics/yup_StrokeType.h" #include "graphics/yup_FillType.h" -#include "context/yup_OffscreenTarget.h" -#include "context/yup_RenderableTarget.h" #include "context/yup_GraphicsContext.h" #include "graphics/yup_Graphics.h" -#include "rhi/yup_ShaderBindingMap.h" -#include "rhi/yup_GpuBuffer.h" -#include "rhi/yup_GpuPipeline.h" -#include "rhi/yup_GpuFrame.h" -#include "rhi/yup_GpuRenderPass.h" -#include "rhi/yup_GpuTarget.h" #include "rhi/yup_GpuCanvas.h" -#include "rhi/yup_GpuPipelineCache.h" #include "svg/yup_SVGElement.h" #include "svg/yup_SVGGradient.h" #include "svg/yup_SVGClipPath.h" diff --git a/modules/yup_gui/component/yup_ComponentNative.cpp b/modules/yup_gui/component/yup_ComponentNative.cpp index a3d93f6b2..511618621 100644 --- a/modules/yup_gui/component/yup_ComponentNative.cpp +++ b/modules/yup_gui/component/yup_ComponentNative.cpp @@ -84,7 +84,7 @@ ComponentNative::Options& ComponentNative::Options::withTemporaryWindow (bool sh return *this; } -ComponentNative::Options& ComponentNative::Options::withGraphicsApi (std::optional newGraphicsApi) noexcept +ComponentNative::Options& ComponentNative::Options::withGraphicsApi (std::optional newGraphicsApi) noexcept { graphicsApi = newGraphicsApi; return *this; diff --git a/modules/yup_gui/component/yup_ComponentNative.h b/modules/yup_gui/component/yup_ComponentNative.h index 54309c5b3..789af262f 100644 --- a/modules/yup_gui/component/yup_ComponentNative.h +++ b/modules/yup_gui/component/yup_ComponentNative.h @@ -153,7 +153,7 @@ class YUP_API ComponentNative : public ReferenceCountedObject @return Reference to this Options object for method chaining. */ - Options& withGraphicsApi (std::optional newGraphicsApi) noexcept; + Options& withGraphicsApi (std::optional newGraphicsApi) noexcept; /** Sets the target framerate for continuous rendering. @@ -190,7 +190,7 @@ class YUP_API ComponentNative : public ReferenceCountedObject /** The configuration flags for the component. */ Flags flags = defaultFlags; /** The graphics API to use for rendering. */ - std::optional graphicsApi; + std::optional graphicsApi; /** The target framerate for continuous rendering. */ std::optional framerateRedraw; /** The clear color to use when rendering. */ diff --git a/modules/yup_gui/native/yup_WindowingUtilities_sdl.cpp b/modules/yup_gui/native/yup_WindowingUtilities_sdl.cpp index 65a45f312..f9d4ffb9e 100644 --- a/modules/yup_gui/native/yup_WindowingUtilities_sdl.cpp +++ b/modules/yup_gui/native/yup_WindowingUtilities_sdl.cpp @@ -385,36 +385,36 @@ void setNativeParent (void* nativeWindow, SDL_Window* window) //============================================================================== -GraphicsContext::Api getGraphicsContextApi (const std::optional& forceContextApi) +GpuPlatform getGraphicsContextApi (const std::optional& forceContextApi) { - GraphicsContext::Api desiredApi; + GpuPlatform desiredApi; #if YUP_MAC || YUP_IOS #if YUP_RIVE_USE_METAL - desiredApi = forceContextApi.value_or (GraphicsContext::Metal); + desiredApi = forceContextApi.value_or (GpuPlatform::Metal); #elif YUP_RIVE_USE_OPENGL - desiredApi = forceContextApi.value_or (GraphicsContext::OpenGL); + desiredApi = forceContextApi.value_or (GpuPlatform::OpenGL); #endif #elif YUP_WINDOWS #if YUP_RIVE_USE_D3D - desiredApi = forceContextApi.value_or (GraphicsContext::Direct3D); + desiredApi = forceContextApi.value_or (GpuPlatform::Direct3D); #elif YUP_RIVE_USE_OPENGL - desiredApi = forceContextApi.value_or (GraphicsContext::OpenGL); + desiredApi = forceContextApi.value_or (GpuPlatform::OpenGL); #endif #elif YUP_LINUX - desiredApi = forceContextApi.value_or (GraphicsContext::OpenGL); + desiredApi = forceContextApi.value_or (GpuPlatform::OpenGL); #elif YUP_ANDROID || YUP_WASM #if YUP_EMSCRIPTEN && RIVE_WEBGPU - desiredApi = forceContextApi.value_or (GraphicsContext::WebGPU); + desiredApi = forceContextApi.value_or (GpuPlatform::WebGPU); #else - desiredApi = forceContextApi.value_or (GraphicsContext::OpenGLES); + desiredApi = forceContextApi.value_or (GpuPlatform::OpenGLES); #endif #else - desiredApi = forceContextApi.value_or (GraphicsContext::OpenGLES); + desiredApi = forceContextApi.value_or (GpuPlatform::OpenGLES); #endif @@ -423,23 +423,23 @@ GraphicsContext::Api getGraphicsContextApi (const std::optional context; std::unique_ptr renderer; diff --git a/modules/yup_rhi/context/yup_GpuDevice.cpp b/modules/yup_rhi/context/yup_GpuDevice.cpp new file mode 100644 index 000000000..27143dffc --- /dev/null +++ b/modules/yup_rhi/context/yup_GpuDevice.cpp @@ -0,0 +1,95 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +// Forward declarations for backend-specific factory functions +std::unique_ptr yup_constructHeadlessGpuDevice (GpuDevice::Options); +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) +std::unique_ptr yup_constructMetalGpuDevice (GpuDevice::Options); +#endif +#if YUP_RIVE_USE_D3D && YUP_WINDOWS +std::unique_ptr yup_constructDirect3DGpuDevice (GpuDevice::Options); +#endif +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) +std::unique_ptr yup_constructOpenGLGpuDevice (GpuDevice::Options); +#endif +#if YUP_EMSCRIPTEN && RIVE_WEBGPU +std::unique_ptr yup_constructWebGPUGpuDevice (GpuDevice::Options); +#elif YUP_RIVE_USE_DAWN +std::unique_ptr yup_constructDawnGpuDevice (GpuDevice::Options); +#endif + +GpuDevice::Ptr GpuDevice::create (GpuPlatform gpuApi, Options options) +{ + std::unique_ptr ctx; + + switch (gpuApi) + { + case GpuPlatform::Headless: + ctx = yup_constructHeadlessGpuDevice (options); + break; + +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) + case GpuPlatform::Metal: + ctx = yup_constructMetalGpuDevice (options); + break; +#endif + +#if YUP_RIVE_USE_D3D && YUP_WINDOWS + case GpuPlatform::Direct3D: + ctx = yup_constructDirect3DGpuDevice (options); + break; +#endif + +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) + case GpuPlatform::OpenGL: + case GpuPlatform::OpenGLES: + ctx = yup_constructOpenGLGpuDevice (options); + break; +#endif + +#if YUP_EMSCRIPTEN && RIVE_WEBGPU + case GpuPlatform::WebGPU: + ctx = yup_constructWebGPUGpuDevice (options); + break; +#elif YUP_RIVE_USE_DAWN + case GpuPlatform::WebGPU: + ctx = yup_constructDawnGpuDevice (options); + break; +#endif + + default: + Logger::outputDebugString ("Invalid GPU API requested for current platform"); + return nullptr; + } + + if (ctx == nullptr) + { + Logger::outputDebugString ("Failed to create the GPU context"); + return nullptr; + } + + return ctx.release(); +} + +} // namespace yup diff --git a/modules/yup_rhi/context/yup_GpuDevice.h b/modules/yup_rhi/context/yup_GpuDevice.h new file mode 100644 index 000000000..2f5c8ceae --- /dev/null +++ b/modules/yup_rhi/context/yup_GpuDevice.h @@ -0,0 +1,182 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** Encapsulates a GPU context that abstracts low-level GPU device operations + across various graphics APIs without requiring a window or framebuffer. + + GpuDevice is the entry point for GPU compute and RHI operations. It owns + the native GPU device, command queue, and the backend-agnostic ore context. + Unlike GraphicsContext, it does NOT depend on windows, swapchains, or Rive + vector rendering — it is suitable for headless GPU compute (e.g. audio DSP + on the GPU). + + GpuDevice is reference-counted, allowing multiple owners (e.g. an audio + processor and an optional UI) to share a single GPU device. + + @see GraphicsContext, GpuPipeline, GpuComputePipeline +*/ +class YUP_API GpuDevice : public ReferenceCountedObject +{ +public: + using Ptr = ReferenceCountedObjectPtr; + + //============================================================================== + /** Procedure load function used by GL and Vulkan to locate methods at runtime. */ + using LoaderFunction = void* (*) (const char*); + + //============================================================================== + /** Configuration options for creating a GPU context. */ + struct Options + { + /** Default constructor, initializes the options with default values. */ + constexpr Options() noexcept = default; + + bool retinaDisplay = true; ///< Whether the context supports Retina or high-DPI displays. + bool readableFramebuffer = false; ///< Allows the framebuffer to be readable. + bool synchronousShaderCompilations = false; ///< Controls whether shader compilations are done synchronously. + bool enableReadPixels = false; ///< Enables reading pixels directly from the framebuffer. + bool disableRasterOrdering = false; ///< Disables specific raster ordering features for performance. + bool allowHeadlessRendering = false; ///< Allows rendering without a visible window (headless mode). + LoaderFunction loaderFunction = nullptr; ///< Loader function (used by GL/Vulkan). + }; + + //============================================================================== + /** Default constructor. */ + GpuDevice() noexcept = default; + + /** Destructor. */ + ~GpuDevice() override = default; + + //============================================================================== + /** Copy and move constructors and assignment operators. */ + GpuDevice (const GpuDevice& other) noexcept = delete; + GpuDevice (GpuDevice&& other) noexcept = default; + GpuDevice& operator= (const GpuDevice& other) noexcept = delete; + GpuDevice& operator= (GpuDevice&& other) noexcept = default; + + //============================================================================== + /** Returns the GPU API used by this context. + + @return The GpuPlatform enum value identifying the active rendering backend. + */ + virtual GpuPlatform getPlatform() const noexcept = 0; + + //============================================================================== + /** Returns the backend-agnostic ore GPU context, or nullptr when ore is + unavailable on this backend. + + This is the single backend bridge used by the RHI layer (GpuPipeline, + GpuFrame, GpuRenderPass, GpuBuffer). User code should prefer the + dependency-free isGpuAvailable() capability probe instead. + */ + virtual rive::ore::Context* gpuContext() const noexcept { return nullptr; } + + /** Returns true if a GPU (ore) context is available for RHI operations. + + Equivalent to gpuContext() != nullptr but without referencing any ore + type, so user code and examples can probe GPU capability ore-free. + */ + bool isGpuAvailable() const noexcept { return gpuContext() != nullptr; } + + /** Returns true if compute shaders are available on this backend. + + Compute shaders are available on Metal, D3D11, D3D12, Vulkan, and + WebGPU backends. Not available on OpenGL/GLES or Headless. + */ + virtual bool isComputeAvailable() const noexcept { return false; } + + //============================================================================== + /** Creates platform-specific GPU offscreen resources for the given dimensions. + + Supported GPU backends may create targets while another offscreen target + is rendering. Backends reserve a render context only while its target + has an active frame, allowing sequential targets to share idle contexts. + A target must outlive its corresponding beginOffscreen()/endOffscreen() + pair and the GpuDevice must outlive every target it creates. + + @param width The width of the offscreen target in pixels. + @param height The height of the offscreen target in pixels. + + @return A unique pointer to an OffscreenTarget object, or nullptr on failure. + */ + virtual std::unique_ptr createOffscreenTarget (int width, int height) = 0; + + /** Creates platform-specific GPU offscreen resources backed by a dedicated render context. + + Unlike createOffscreenTarget(), the returned RenderableTarget reserves a + backend-owned RenderContext, which is required to drive a 2D Graphics frame + (GpuCanvas::beginDraw). Prefer createOffscreenTarget() for render-pass-only + surfaces to avoid allocating a dedicated context. + + @param width The width of the offscreen target in pixels. + @param height The height of the offscreen target in pixels. + + @return A unique pointer to a RenderableTarget object, or nullptr on failure. + */ + virtual std::unique_ptr createRenderableTarget (int width, int height) = 0; + + /** Begins a GPU frame targeting the given offscreen surface. + + A target may have only one active frame. Nested frames are supported when + they use distinct OffscreenTarget instances. + + @param target The OffscreenTarget to render into. + @param frameDesc The frame descriptor that contains frame-specific data. + */ + virtual void beginOffscreen (OffscreenTarget& target, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) = 0; + + /** Flushes GPU commands into the offscreen target. + + Must be called after beginOffscreen() and before endOffscreen(). + + @param target The OffscreenTarget to flush commands into. + */ + virtual void endOffscreen (OffscreenTarget& target) = 0; + + /** Reads RGBA pixels from the completed offscreen frame into CPU memory. + + Must be called after endOffscreen(). Rows are top-to-bottom. + + @param target The OffscreenTarget to read pixels from. + @param dst Pointer to the destination buffer where pixel data will be stored. + @param dstSize The size of the destination buffer in bytes. + + @return True if the pixel read operation was successful, false otherwise. + */ + virtual bool readOffscreenPixels (OffscreenTarget& target, void* dst, size_t dstSize) = 0; + + //============================================================================== + /** Static factory method to create a GPU context using a specific GPU API. + + @param gpuApi The GPU API to use. + @param options Configuration options for the GPU context. + + @return A reference-counted pointer to a GpuDevice, using the specified + GPU API and configured according to the options. + */ + static GpuDevice::Ptr create (GpuPlatform gpuApi, Options options); +}; + +} // namespace yup diff --git a/modules/yup_graphics/context/yup_OffscreenTarget.h b/modules/yup_rhi/context/yup_OffscreenTarget.h similarity index 100% rename from modules/yup_graphics/context/yup_OffscreenTarget.h rename to modules/yup_rhi/context/yup_OffscreenTarget.h diff --git a/modules/yup_graphics/context/yup_RenderableTarget.h b/modules/yup_rhi/context/yup_RenderableTarget.h similarity index 100% rename from modules/yup_graphics/context/yup_RenderableTarget.h rename to modules/yup_rhi/context/yup_RenderableTarget.h diff --git a/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp b/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp new file mode 100644 index 000000000..2068f8053 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp @@ -0,0 +1,318 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_RIVE_USE_D3D +#include "rive/renderer/d3d11/render_context_d3d_impl.hpp" +#include "rive/renderer/d3d11/d3d11.hpp" +#include "rive/renderer/ore/ore_context_d3d11.hpp" +#include +#include + +namespace yup +{ + +class GpuDeviceD3D : public GpuDevice +{ +public: + GpuDeviceD3D (ComPtr gpu, + ComPtr gpuContext, + const rive::gpu::D3DContextOptions& contextOptions, + Options options) + : m_options (options) + , m_renderContextOptions (contextOptions) + , m_gpu (std::move (gpu)) + , m_gpuContext (std::move (gpuContext)) + , m_renderContext (rive::gpu::RenderContextD3DImpl::MakeContext (m_gpu, m_gpuContext, m_renderContextOptions)) + , m_oreContext (rive::ore::ContextD3D11::Make (m_gpu.Get(), m_gpuContext.Get())) + { + } + + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Direct3D; } + + rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } + + bool isComputeAvailable() const noexcept override { return true; } + + //============================================================================== + + struct OffscreenContextSlot + { + std::unique_ptr renderContext; + bool frameActive = false; + }; + + struct OffscreenTargetD3D : public RenderableTarget + { + int width = 0; + int height = 0; + ComPtr stagingTexture; + rive::rcp renderCanvas; + rive::gpu::RenderContext* renderContext = nullptr; + OffscreenContextSlot* contextSlot = nullptr; + + int getWidth() const noexcept override { return width; } + + int getHeight() const noexcept override { return height; } + + rive::gpu::RenderTarget* getRenderTarget() noexcept override + { + return renderCanvas != nullptr ? renderCanvas->renderTarget() : nullptr; + } + + rive::gpu::RenderContext* getRenderContext() noexcept override + { + return renderContext; + } + + rive::rcp getRenderCanvas() noexcept override + { + return renderCanvas; + } + + rive::rcp adoptAsTexture() override + { + if (renderCanvas == nullptr) + return nullptr; + return renderCanvas->renderImage()->refTexture(); + } + }; + + ComPtr createStagingTexture (int width, int height) + { + D3D11_TEXTURE2D_DESC stagingDesc {}; + stagingDesc.Width = static_cast (width); + stagingDesc.Height = static_cast (height); + stagingDesc.MipLevels = 1; + stagingDesc.ArraySize = 1; + stagingDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + stagingDesc.SampleDesc.Count = 1; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + ComPtr staging; + auto hr = m_gpu->CreateTexture2D (&stagingDesc, nullptr, staging.ReleaseAndGetAddressOf()); + if (FAILED (hr)) + return nullptr; + return staging; + } + + std::unique_ptr createOffscreenTarget (int width, int height) override + { + if (width <= 0 || height <= 0 || m_renderContext == nullptr) + return nullptr; + + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = nullptr; + target->contextSlot = nullptr; + + target->renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), + static_cast (height)); + if (target->renderCanvas == nullptr) + return nullptr; + + target->stagingTexture = createStagingTexture (width, height); + if (target->stagingTexture == nullptr) + return nullptr; + + return target; + } + + std::unique_ptr createRenderableTarget (int width, int height) override + { + if (width <= 0 || height <= 0) + return nullptr; + + auto* contextSlot = acquireOffscreenContext(); + if (contextSlot == nullptr) + return nullptr; + + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = contextSlot->renderContext.get(); + target->contextSlot = contextSlot; + + target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), + static_cast (height)); + if (target->renderCanvas == nullptr) + return nullptr; + + target->stagingTexture = createStagingTexture (width, height); + if (target->stagingTexture == nullptr) + return nullptr; + + return target; + } + + void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override + { + auto& target = static_cast (baseTarget); + auto* renderContext = target.getRenderContext(); + + if (renderContext != nullptr) + { + if (target.contextSlot == nullptr || target.contextSlot->frameActive) + return; + + renderContext->beginFrame (frameDesc); + target.contextSlot->frameActive = true; + } + } + + void endOffscreen (OffscreenTarget& baseTarget) override + { + auto& target = static_cast (baseTarget); + auto* renderContext = target.getRenderContext(); + + if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) + return; + + rive::gpu::RenderContext::FlushResources flushDesc; + flushDesc.renderTarget = target.getRenderTarget(); + renderContext->flush (flushDesc); + + if (auto* renderTarget = static_cast (target.getRenderTarget())) + m_gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); + + target.contextSlot->frameActive = false; + } + + bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override + { + auto& target = static_cast (baseTarget); + + if (target.stagingTexture == nullptr || dst == nullptr) + return false; + + const size_t bytesPerRow = static_cast (target.width) * 4u; + if (dstSize < bytesPerRow * static_cast (target.height)) + return false; + + if (target.getRenderContext() == nullptr) + { + if (auto* renderTarget = static_cast (target.getRenderTarget())) + m_gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); + } + + D3D11_MAPPED_SUBRESOURCE mapped {}; + HRESULT hr = m_gpuContext->Map (target.stagingTexture.Get(), 0, D3D11_MAP_READ, 0, &mapped); + if (FAILED (hr)) + return false; + + auto* dstBytes = static_cast (dst); + const auto* srcBytes = static_cast (mapped.pData); + + for (int row = 0; row < target.height; ++row) + { + std::memcpy (dstBytes + static_cast (row) * bytesPerRow, + srcBytes + static_cast (row) * mapped.RowPitch, + bytesPerRow); + } + + m_gpuContext->Unmap (target.stagingTexture.Get(), 0); + return true; + } + +private: + OffscreenContextSlot* acquireOffscreenContext() + { + for (const auto& slot : m_offscreenContextPool) + { + if (! slot->frameActive) + return slot.get(); + } + + auto slot = std::make_unique(); + slot->renderContext = rive::gpu::RenderContextD3DImpl::MakeContext (m_gpu, m_gpuContext, m_renderContextOptions); + if (slot->renderContext == nullptr) + return nullptr; + + auto* result = slot.get(); + m_offscreenContextPool.push_back (std::move (slot)); + return result; + } + + Options m_options; + rive::gpu::D3DContextOptions m_renderContextOptions; + ComPtr m_gpu; + ComPtr m_gpuContext; + std::unique_ptr m_renderContext; + std::vector> m_offscreenContextPool; + std::unique_ptr m_oreContext; +}; + +//============================================================================== + +std::unique_ptr yup_constructDirect3DGpuDevice (GpuDevice::Options fiddleOptions) +{ + ComPtr adapter; + DXGI_ADAPTER_DESC adapterDesc {}; + rive::gpu::D3DContextOptions contextOptions; + + if (fiddleOptions.disableRasterOrdering) + { + contextOptions.disableRasterizerOrderedViews = true; + contextOptions.disableTypedUAVLoadStore = true; + } + + // Create a temporary factory just to enumerate adapters + ComPtr factory; + VERIFY_OK (CreateDXGIFactory (__uuidof (IDXGIFactory2), reinterpret_cast (factory.ReleaseAndGetAddressOf()))); + + for (UINT i = 0; factory->EnumAdapters (i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i) + { + adapter->GetDesc (&adapterDesc); + contextOptions.isIntel = adapterDesc.VendorId == 0x163C || adapterDesc.VendorId == 0x8086 || adapterDesc.VendorId == 0x8087; + break; + } + + ComPtr gpu; + ComPtr gpuContext; + D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1 }; + + UINT creationFlags = 0; +#ifdef DEBUG + creationFlags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + VERIFY_OK (D3D11CreateDevice (adapter.Get(), + D3D_DRIVER_TYPE_UNKNOWN, + nullptr, + creationFlags, + featureLevels, + std::size (featureLevels), + D3D11_SDK_VERSION, + gpu.ReleaseAndGetAddressOf(), + nullptr, + gpuContext.ReleaseAndGetAddressOf())); + + if (! gpu || ! gpuContext) + return nullptr; + + printf ("D3D device: %S\n", adapterDesc.Description); + + return std::make_unique (std::move (gpu), std::move (gpuContext), contextOptions, fiddleOptions); +} + +} // namespace yup +#endif diff --git a/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp b/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp new file mode 100644 index 000000000..a92e7d4d8 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp @@ -0,0 +1,174 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if RIVE_DAWN +#include "dawn/native/DawnNative.h" +#include "dawn/dawn_proc.h" + +#include "rive/pls/pls_factory.hpp" +#include "rive/pls/pls_renderer.hpp" +#include "rive/pls/webgpu/pls_render_context_webgpu_impl.hpp" + +#include +#include +#include + +namespace yup +{ + +static void print_device_error (WGPUErrorType errorType, const char* message, void*) +{ + const char* errorTypeName = ""; + switch (errorType) + { + case WGPUErrorType_Validation: + errorTypeName = "Validation"; + break; + case WGPUErrorType_OutOfMemory: + errorTypeName = "Out of memory"; + break; + case WGPUErrorType_Unknown: + errorTypeName = "Unknown"; + break; + case WGPUErrorType_DeviceLost: + errorTypeName = "Device lost"; + break; + default: + return; + } + printf ("%s error: %s\n", errorTypeName, message); +} + +static void device_lost_callback (WGPUDeviceLostReason reason, const char* message, void*) +{ + printf ("device lost: %s\n", message); +} + +class GpuDeviceDawn : public GpuDevice +{ +public: + GpuDeviceDawn (Options options) + : m_options (options) + { + WGPUInstanceDescriptor instanceDescriptor {}; + instanceDescriptor.features.timedWaitAnyEnable = true; + m_instance = std::make_unique (&instanceDescriptor); + + wgpu::RequestAdapterOptions adapterOptions = { + .powerPreference = wgpu::PowerPreference::HighPerformance, + }; + + auto adapters = m_instance->EnumerateAdapters (&adapterOptions); + + wgpu::DawnAdapterPropertiesPowerPreference power_props {}; + wgpu::AdapterProperties adapterProperties {}; + adapterProperties.nextInChain = &power_props; + + auto isAdapterType = [&adapterProperties] (const auto& adapter) -> bool + { + adapter.GetProperties (&adapterProperties); + return adapterProperties.adapterType == wgpu::AdapterType::DiscreteGPU; + }; + + auto preferredAdapter = std::find_if (adapters.begin(), adapters.end(), isAdapterType); + if (preferredAdapter == adapters.end()) + { + fprintf (stderr, "Failed to find an adapter!\n"); + return; + } + + std::vector enableToggleNames = { + "allow_unsafe_apis", + "turn_off_vsync", + }; + + WGPUDawnTogglesDescriptor toggles = { + .chain = { .next = nullptr, .sType = WGPUSType_DawnTogglesDescriptor }, + .enabledToggleCount = enableToggleNames.size(), + .enabledToggles = enableToggleNames.data(), + .disabledToggleCount = 0, + .disabledToggles = nullptr, + }; + + std::vector requiredFeatures = { + WGPUFeatureName_SurfaceCapabilities, + }; + + WGPUDeviceDescriptor deviceDesc = { + .nextInChain = reinterpret_cast (&toggles), + .requiredFeatureCount = requiredFeatures.size(), + .requiredFeatures = requiredFeatures.data(), + }; + + m_backendDevice = preferredAdapter->CreateDevice (&deviceDesc); + + DawnProcTable backendProcs = dawn::native::GetProcs(); + dawnProcSetProcs (&backendProcs); + backendProcs.deviceSetUncapturedErrorCallback (m_backendDevice, print_device_error, nullptr); + backendProcs.deviceSetDeviceLostCallback (m_backendDevice, device_lost_callback, nullptr); + + m_device = wgpu::Device::Acquire (m_backendDevice); + m_queue = m_device.GetQueue(); + } + + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::WebGPU; } + + rive::ore::Context* gpuContext() const noexcept override { return nullptr; } + + bool isComputeAvailable() const noexcept override { return true; } + + // Dawn doesn't support PLS offscreen targets through ore yet. + std::unique_ptr createOffscreenTarget (int, int) override { return nullptr; } + + std::unique_ptr createRenderableTarget (int, int) override { return nullptr; } + + void beginOffscreen (OffscreenTarget&, const rive::gpu::RenderContext::FrameDescriptor&) override {} + + void endOffscreen (OffscreenTarget&) override {} + + bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override { return false; } + + /** Returns the native WGPU device for compute operations. */ + WGPUDevice getBackendDevice() const noexcept { return m_backendDevice; } + + /** Returns the wgpu::Device for compute operations. */ + wgpu::Device getDevice() const noexcept { return m_device; } + + /** Returns the wgpu::Queue for compute operations. */ + wgpu::Queue getQueue() const noexcept { return m_queue; } + +private: + Options m_options; + WGPUDevice m_backendDevice = {}; + wgpu::Device m_device = {}; + wgpu::Queue m_queue = {}; + std::unique_ptr m_instance; +}; + +//============================================================================== + +std::unique_ptr yup_constructDawnGpuDevice (GpuDevice::Options options) +{ + return std::make_unique (options); +} + +} // namespace yup +#endif diff --git a/modules/yup_rhi/native/yup_GpuDevice_headless.cpp b/modules/yup_rhi/native/yup_GpuDevice_headless.cpp new file mode 100644 index 000000000..c4ef8ecf0 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuDevice_headless.cpp @@ -0,0 +1,65 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== + +class HeadlessGpuDevice : public GpuDevice +{ +public: + HeadlessGpuDevice() = default; + + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Headless; } + + std::unique_ptr createOffscreenTarget (int, int) override + { + return nullptr; + } + + std::unique_ptr createRenderableTarget (int, int) override + { + return nullptr; + } + + void beginOffscreen (OffscreenTarget&, const rive::gpu::RenderContext::FrameDescriptor&) override + { + } + + void endOffscreen (OffscreenTarget&) override + { + } + + bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override + { + return false; + } +}; + +//============================================================================== + +std::unique_ptr yup_constructHeadlessGpuDevice (GpuDevice::Options) +{ + return std::make_unique(); +} + +} // namespace yup diff --git a/modules/yup_rhi/native/yup_GpuDevice_metal.cpp b/modules/yup_rhi/native/yup_GpuDevice_metal.cpp new file mode 100644 index 000000000..33502c5e3 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuDevice_metal.cpp @@ -0,0 +1,290 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_RIVE_USE_METAL +#include "rive/renderer/rive_renderer.hpp" +#include "rive/renderer/metal/render_context_metal_impl.h" +#include "rive/renderer/ore/ore_context_metal.hpp" + +#include + +namespace yup +{ + +//============================================================================== + +class GpuDeviceMetal : public GpuDevice +{ +public: + GpuDeviceMetal (GpuDevice::Options options) + : fiddleOptions (options) + { + rive::gpu::RenderContextMetalImpl::ContextOptions renderCtxOpts; + + if (fiddleOptions.synchronousShaderCompilations) + renderCtxOpts.shaderCompilationMode = rive::gpu::ShaderCompilationMode::alwaysSynchronous; + + if (fiddleOptions.disableRasterOrdering) + renderCtxOpts.disableFramebufferReads = true; + + renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (gpu, renderCtxOpts); + oreContext = rive::ore::ContextMetal::Make (gpu, queue); + } + + //============================================================================== + + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Metal; } + + rive::ore::Context* gpuContext() const noexcept override { return oreContext.get(); } + + bool isComputeAvailable() const noexcept override { return true; } + + //============================================================================== + + std::unique_ptr createOffscreenTarget (int width, int height) override + { + if (width <= 0 || height <= 0 || renderContext == nullptr) + return nullptr; + + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = nullptr; + target->contextSlot = nullptr; + target->renderCanvas = renderContext->makeRenderCanvas (static_cast (width), + static_cast (height)); + if (target->renderCanvas == nullptr) + return nullptr; + + return target; + } + + std::unique_ptr createRenderableTarget (int width, int height) override + { + if (width <= 0 || height <= 0) + return nullptr; + + auto* contextSlot = acquireOffscreenContext(); + if (contextSlot == nullptr) + return nullptr; + + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = contextSlot->renderContext.get(); + target->contextSlot = contextSlot; + target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), + static_cast (height)); + if (target->renderCanvas == nullptr) + return nullptr; + + return target; + } + + void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override + { + auto& target = static_cast (baseTarget); + auto* rc = target.getRenderContext(); + + if (rc == nullptr || target.contextSlot == nullptr || target.contextSlot->frameActive) + return; + + rc->beginFrame (frameDesc); + target.contextSlot->frameActive = true; + } + + void endOffscreen (OffscreenTarget& baseTarget) override + { + auto& target = static_cast (baseTarget); + auto* rc = target.getRenderContext(); + + if (rc == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) + return; + + id commandBuffer = [queue commandBuffer]; + rc->flush ({ .renderTarget = target.getRenderTarget(), .externalCommandBuffer = (__bridge void*) commandBuffer }); + [commandBuffer commit]; + target.contextSlot->frameActive = false; + } + + bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override + { + auto& target = static_cast (baseTarget); + + if (dst == nullptr) + return false; + + id srcTexture = target.targetTexture(); + if (srcTexture == nil) + return false; + + if (target.stagingTexture == nil) + { + MTLTextureDescriptor* stagingDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm + width:static_cast (target.width) + height:static_cast (target.height) + mipmapped:NO]; + stagingDesc.usage = MTLTextureUsageShaderRead; +#if YUP_IOS + stagingDesc.storageMode = MTLStorageModeShared; +#else + stagingDesc.storageMode = MTLStorageModeManaged; +#endif + target.stagingTexture = [gpu newTextureWithDescriptor:stagingDesc]; + if (target.stagingTexture == nil) + return false; + } + + const auto w = static_cast (target.width); + const auto h = static_cast (target.height); + const size_t bytesPerRow = w * 4u; + + if (dstSize < bytesPerRow * h) + return false; + + id commandBuffer = [queue commandBuffer]; + + id blitEncoder = [commandBuffer blitCommandEncoder]; + [blitEncoder copyFromTexture:srcTexture + sourceSlice:0 + sourceLevel:0 + sourceOrigin:MTLOriginMake (0, 0, 0) + sourceSize:MTLSizeMake (w, h, 1) + toTexture:target.stagingTexture + destinationSlice:0 + destinationLevel:0 + destinationOrigin:MTLOriginMake (0, 0, 0)]; +#if YUP_MAC + [blitEncoder synchronizeResource:target.stagingTexture]; +#endif + [blitEncoder endEncoding]; + + [commandBuffer commit]; + [commandBuffer waitUntilCompleted]; + + [target.stagingTexture getBytes:dst + bytesPerRow:bytesPerRow + fromRegion:MTLRegionMake2D (0, 0, w, h) + mipmapLevel:0]; + + return true; + } + + //============================================================================== + /** Returns the native MTLDevice. Used by GraphicsContextMetal to share the device. */ + id getDevice() const noexcept { return gpu; } + + /** Returns the native MTLCommandQueue. Used by GraphicsContextMetal. */ + id getCommandQueue() const noexcept { return queue; } + +private: + struct OffscreenContextSlot + { + std::unique_ptr renderContext; + bool frameActive = false; + }; + + struct OffscreenTargetMetal : public RenderableTarget + { + int width = 0; + int height = 0; + id stagingTexture = nil; + rive::rcp renderCanvas; + rive::gpu::RenderContext* renderContext = nullptr; + OffscreenContextSlot* contextSlot = nullptr; + + int getWidth() const noexcept override { return width; } + + int getHeight() const noexcept override { return height; } + + rive::gpu::RenderTarget* getRenderTarget() noexcept override + { + return renderCanvas != nullptr ? renderCanvas->renderTarget() : nullptr; + } + + rive::gpu::RenderContext* getRenderContext() noexcept override + { + return renderContext; + } + + rive::rcp getRenderCanvas() noexcept override + { + return renderCanvas; + } + + rive::rcp adoptAsTexture() override + { + if (renderCanvas == nullptr) + return nullptr; + return renderCanvas->renderImage()->refTexture(); + } + + id targetTexture() const + { + if (renderCanvas == nullptr) + return nil; + if (auto* target = static_cast (renderCanvas->renderTarget())) + return target->targetTexture(); + return nil; + } + }; + + OffscreenContextSlot* acquireOffscreenContext() + { + for (const auto& slot : offscreenContextPool) + { + if (! slot->frameActive) + return slot.get(); + } + + rive::gpu::RenderContextMetalImpl::ContextOptions renderCtxOpts; + if (fiddleOptions.synchronousShaderCompilations) + renderCtxOpts.shaderCompilationMode = rive::gpu::ShaderCompilationMode::alwaysSynchronous; + if (fiddleOptions.disableRasterOrdering) + renderCtxOpts.disableFramebufferReads = true; + + auto slot = std::make_unique(); + slot->renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (gpu, renderCtxOpts); + if (slot->renderContext == nullptr) + return nullptr; + + auto* result = slot.get(); + offscreenContextPool.push_back (std::move (slot)); + return result; + } + + const GpuDevice::Options fiddleOptions; + std::unique_ptr renderContext; + std::vector> offscreenContextPool; + std::unique_ptr oreContext; + id gpu = MTLCreateSystemDefaultDevice(); + id queue = [gpu newCommandQueue]; +}; + +//============================================================================== + +std::unique_ptr yup_constructMetalGpuDevice (GpuDevice::Options options) +{ + return std::make_unique (options); +} + +} // namespace yup +#endif diff --git a/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp b/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp new file mode 100644 index 000000000..5becb83af --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp @@ -0,0 +1,306 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_WASM || YUP_ANDROID +#include "rive/renderer/gl/gles3.hpp" +#include "rive/renderer/gl/render_context_gl_impl.hpp" +#include "rive/renderer/gl/render_target_gl.hpp" +#include "rive/renderer/ore/ore_context_gl.hpp" +#include +#include + +namespace yup +{ + +class GpuDeviceGL : public GpuDevice +{ +public: + GpuDeviceGL (Options options) + : m_options (options) + { +#if RIVE_DESKTOP_GL + if (! gladLoadCustomLoader ((GLADloadfunc) options.loaderFunction)) + { + fprintf (stderr, "Failed to initialize glad.\n"); + return; + } +#endif + + m_renderContext = rive::gpu::RenderContextGLImpl::MakeContext (m_renderContextOptions); + if (! m_renderContext) + { + fprintf (stderr, "Failed to create a renderer.\n"); + return; + } + + m_oreContext = rive::ore::ContextGL::Make(); + +#if RIVE_DESKTOP_GL && DEBUG + if (GLAD_GL_KHR_debug) + { + glEnable (GL_DEBUG_OUTPUT_KHR); + glDebugMessageControlKHR (GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); + } +#endif + } + + ~GpuDeviceGL() override = default; + + GpuPlatform getPlatform() const noexcept override + { +#if RIVE_ANDROID || RIVE_WEBGL + return GpuPlatform::OpenGLES; +#else + return GpuPlatform::OpenGL; +#endif + } + + rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } + + bool isComputeAvailable() const noexcept override + { + // GL 4.3+ and GLES 3.1+ support compute shaders natively. + // Probe the version string at runtime. + const auto* version = (const char*) glGetString (GL_VERSION); + if (version == nullptr) + return false; + + // GLES: "OpenGL ES 3.1" or higher + if (strstr (version, "OpenGL ES") != nullptr) + { + int major = 0, minor = 0; + if (sscanf (version, "OpenGL ES %d.%d", &major, &minor) == 2) + return major > 3 || (major == 3 && minor >= 1); + } + + // Desktop GL: "4.3" or higher + int major = 0, minor = 0; + if (sscanf (version, "%d.%d", &major, &minor) == 2) + return major > 4 || (major == 4 && minor >= 3); + + return false; + } + + //============================================================================== + + struct OffscreenContextSlot + { + std::unique_ptr renderContext; + bool frameActive = false; + }; + + struct OffscreenTargetGL : public RenderableTarget + { + int width = 0; + int height = 0; + rive::rcp renderCanvas; + rive::gpu::RenderContext* renderContext = nullptr; + rive::gpu::RenderContext* mirrorContext = nullptr; + mutable rive::rcp sampledMirrorTex; + OffscreenContextSlot* contextSlot = nullptr; + + int getWidth() const noexcept override { return width; } + + int getHeight() const noexcept override { return height; } + + rive::gpu::RenderTarget* getRenderTarget() noexcept override + { + return renderCanvas != nullptr ? renderCanvas->renderTarget() : nullptr; + } + + rive::gpu::RenderContext* getRenderContext() noexcept override + { + return renderContext; + } + + rive::rcp getRenderCanvas() noexcept override + { + return renderCanvas; + } + + rive::rcp adoptAsTexture() override + { + if (renderCanvas == nullptr) + return nullptr; + return renderCanvas->renderImage()->refTexture(); + } + + rive::rcp getOrCreateSampledTexture() override + { +#if defined(ORE_BACKEND_GL) && defined(RIVE_CANVAS) + if (sampledMirrorTex != nullptr) + return sampledMirrorTex; + if (mirrorContext == nullptr || renderCanvas == nullptr) + return nullptr; + auto renderImage = renderCanvas->renderImage(); + if (renderImage == nullptr) + return nullptr; + if (auto sourceTex = renderImage->refTexture()) + { + auto mirrorImage = rive::getCanvasImportMirrorGL ( + mirrorContext, sourceTex.get(), (uint32_t) width, (uint32_t) height); + if (mirrorImage != nullptr) + sampledMirrorTex = mirrorImage->refTexture(); + } + return sampledMirrorTex; +#else + return nullptr; +#endif + } + + rive::rcp getSampledTexture() const override + { + return sampledMirrorTex; + } + }; + + std::unique_ptr createOffscreenTarget (int width, int height) override + { + if (width <= 0 || height <= 0 || m_renderContext == nullptr) + return nullptr; + + auto renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); + if (renderCanvas == nullptr) + return nullptr; + + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = nullptr; + target->mirrorContext = m_renderContext.get(); + target->contextSlot = nullptr; + target->renderCanvas = std::move (renderCanvas); + return target; + } + + std::unique_ptr createRenderableTarget (int width, int height) override + { + if (width <= 0 || height <= 0) + return nullptr; + + auto* contextSlot = acquireOffscreenContext(); + if (contextSlot == nullptr) + return nullptr; + + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = contextSlot->renderContext.get(); + target->mirrorContext = contextSlot->renderContext.get(); + target->contextSlot = contextSlot; + + target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); + if (target->renderCanvas == nullptr) + return nullptr; + + return target; + } + + void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override + { + auto& target = static_cast (baseTarget); + auto renderContext = target.getRenderContext(); + if (renderContext == nullptr || target.contextSlot == nullptr || target.contextSlot->frameActive) + return; + + renderContext->static_impl_cast()->invalidateGLState(); + renderContext->beginFrame (frameDesc); + target.contextSlot->frameActive = true; + } + + void endOffscreen (OffscreenTarget& baseTarget) override + { + auto& target = static_cast (baseTarget); + auto renderContext = target.getRenderContext(); + if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) + return; + + renderContext->static_impl_cast()->invalidateGLState(); + renderContext->flush ({ target.getRenderTarget() }); + renderContext->static_impl_cast()->unbindGLInternalResources(); + target.contextSlot->frameActive = false; + } + + bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override + { + auto& target = static_cast (baseTarget); + if (target.getRenderTarget() == nullptr || dst == nullptr) + return false; + + const size_t bytesPerRow = static_cast (target.width) * 4u; + if (dstSize < bytesPerRow * static_cast (target.height)) + return false; + + auto* renderTarget = static_cast (target.getRenderTarget()); + renderTarget->bindDestinationFramebuffer (GL_READ_FRAMEBUFFER); + glReadPixels (0, 0, target.width, target.height, GL_RGBA, GL_UNSIGNED_BYTE, dst); + glBindFramebuffer (GL_READ_FRAMEBUFFER, 0); + + auto* bytes = static_cast (dst); + std::vector rowBuffer (bytesPerRow); + const int halfHeight = target.height / 2; + for (int i = 0; i < halfHeight; ++i) + { + uint8_t* top = bytes + static_cast (i) * bytesPerRow; + uint8_t* bottom = bytes + static_cast (target.height - 1 - i) * bytesPerRow; + std::memcpy (rowBuffer.data(), top, bytesPerRow); + std::memcpy (top, bottom, bytesPerRow); + std::memcpy (bottom, rowBuffer.data(), bytesPerRow); + } + + return true; + } + +private: + OffscreenContextSlot* acquireOffscreenContext() + { + for (const auto& slot : m_offscreenContextPool) + { + if (! slot->frameActive) + return slot.get(); + } + + auto slot = std::make_unique(); + slot->renderContext = rive::gpu::RenderContextGLImpl::MakeContext (m_renderContextOptions); + if (slot->renderContext == nullptr) + return nullptr; + + auto* result = slot.get(); + m_offscreenContextPool.push_back (std::move (slot)); + return result; + } + + Options m_options; + rive::gpu::RenderContextGLImpl::ContextOptions m_renderContextOptions; + std::unique_ptr m_renderContext; + std::vector> m_offscreenContextPool; + std::unique_ptr m_oreContext; +}; + +//============================================================================== + +std::unique_ptr yup_constructOpenGLGpuDevice (GpuDevice::Options options) +{ + return std::make_unique (options); +} + +} // namespace yup +#endif diff --git a/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp b/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp new file mode 100644 index 000000000..be9b28f13 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp @@ -0,0 +1,225 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_EMSCRIPTEN && RIVE_WEBGPU +#include "rive/renderer/webgpu/render_context_webgpu_impl.hpp" +#include "rive/renderer/ore/ore_context.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace yup +{ + +class GpuDeviceWebGPU : public GpuDevice +{ +public: + GpuDeviceWebGPU (Options options) + : m_options (options) + { + m_device = wgpu::Device::Acquire (emscripten_webgpu_get_device()); + if (m_device == nullptr) + { + fprintf (stderr, "WebGPU: no device. Ensure Module.preinitializedWebGPUDevice is set before main().\n"); + return; + } + + m_queue = m_device.GetQueue(); + + m_renderContext = rive::gpu::RenderContextWebGPUImpl::MakeContext ( + {}, m_device, m_queue, rive::gpu::RenderContextWebGPUImpl::ContextOptions()); + + if (m_renderContext == nullptr) + { + fprintf (stderr, "WebGPU: failed to create a render context.\n"); + return; + } + + m_oreContext = m_renderContext->static_impl_cast()->makeOreContext(); + } + + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::WebGPU; } + + rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } + + bool isComputeAvailable() const noexcept override { return true; } + + //============================================================================== + + struct OffscreenContextSlot + { + std::unique_ptr renderContext; + bool frameActive = false; + }; + + struct OffscreenTargetWebGPU : public RenderableTarget + { + int width = 0; + int height = 0; + rive::rcp renderCanvas; + rive::gpu::RenderContext* renderContext = nullptr; + OffscreenContextSlot* contextSlot = nullptr; + + int getWidth() const noexcept override { return width; } + + int getHeight() const noexcept override { return height; } + + rive::gpu::RenderTarget* getRenderTarget() noexcept override + { + return renderCanvas != nullptr ? renderCanvas->renderTarget() : nullptr; + } + + rive::gpu::RenderContext* getRenderContext() noexcept override + { + return renderContext; + } + + rive::rcp getRenderCanvas() noexcept override + { + return renderCanvas; + } + + rive::rcp adoptAsTexture() override + { + if (renderCanvas == nullptr) + return nullptr; + return renderCanvas->renderImage()->refTexture(); + } + }; + + std::unique_ptr createOffscreenTarget (int width, int height) override + { + if (width <= 0 || height <= 0 || m_renderContext == nullptr) + return nullptr; + + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = nullptr; + target->contextSlot = nullptr; + target->renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), + static_cast (height)); + if (target->renderCanvas == nullptr) + return nullptr; + + return target; + } + + std::unique_ptr createRenderableTarget (int width, int height) override + { + if (width <= 0 || height <= 0) + return nullptr; + + auto* contextSlot = acquireOffscreenContext(); + if (contextSlot == nullptr) + return nullptr; + + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = contextSlot->renderContext.get(); + target->contextSlot = contextSlot; + target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), + static_cast (height)); + if (target->renderCanvas == nullptr) + return nullptr; + + return target; + } + + void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override + { + auto& target = static_cast (baseTarget); + auto* renderContext = target.getRenderContext(); + + if (renderContext == nullptr || target.contextSlot == nullptr || target.contextSlot->frameActive) + return; + + renderContext->beginFrame (frameDesc); + target.contextSlot->frameActive = true; + } + + void endOffscreen (OffscreenTarget& baseTarget) override + { + auto& target = static_cast (baseTarget); + auto* renderContext = target.getRenderContext(); + + if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) + return; + + wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder(); + + renderContext->flush ({ .renderTarget = target.getRenderTarget(), + .externalCommandBuffer = encoder.Get() }); + + wgpu::CommandBuffer commands = encoder.Finish(); + m_queue.Submit (1, &commands); + + target.contextSlot->frameActive = false; + } + + bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override + { + return false; // GPU-to-CPU buffer mapping is async-only on the web. + } + +private: + OffscreenContextSlot* acquireOffscreenContext() + { + for (const auto& slot : m_offscreenContextPool) + { + if (! slot->frameActive) + return slot.get(); + } + + auto slot = std::make_unique(); + slot->renderContext = rive::gpu::RenderContextWebGPUImpl::MakeContext ( + {}, m_device, m_queue, rive::gpu::RenderContextWebGPUImpl::ContextOptions()); + if (slot->renderContext == nullptr) + return nullptr; + + auto* result = slot.get(); + m_offscreenContextPool.push_back (std::move (slot)); + return result; + } + + Options m_options; + wgpu::Device m_device; + wgpu::Queue m_queue; + std::unique_ptr m_renderContext; + std::vector> m_offscreenContextPool; + std::unique_ptr m_oreContext; +}; + +//============================================================================== + +std::unique_ptr yup_constructWebGPUGpuDevice (GpuDevice::Options options) +{ + return std::make_unique (options); +} + +} // namespace yup +#endif diff --git a/modules/yup_graphics/rhi/yup_GpuBuffer.cpp b/modules/yup_rhi/rhi/yup_GpuBuffer.cpp similarity index 96% rename from modules/yup_graphics/rhi/yup_GpuBuffer.cpp rename to modules/yup_rhi/rhi/yup_GpuBuffer.cpp index 8f03e8e27..b9c0b13b5 100644 --- a/modules/yup_graphics/rhi/yup_GpuBuffer.cpp +++ b/modules/yup_rhi/rhi/yup_GpuBuffer.cpp @@ -69,12 +69,12 @@ bool GpuBuffer::isValid() const noexcept //============================================================================== -GpuBuffer::Ptr GpuBuffer::create (GraphicsContext& ctx, +GpuBuffer::Ptr GpuBuffer::create (GpuDevice::Ptr ctx, GpuBufferType type, const void* data, size_t byteSize) { - auto* oreCtx = ctx.gpuContext(); + auto* oreCtx = ctx->gpuContext(); if (oreCtx == nullptr) return nullptr; @@ -91,6 +91,7 @@ GpuBuffer::Ptr GpuBuffer::create (GraphicsContext& ctx, case GpuBufferType::index: desc.usage = rive::ore::BufferUsage::index; break; + case GpuBufferType::storage: case GpuBufferType::uniform: default: desc.usage = rive::ore::BufferUsage::uniform; diff --git a/modules/yup_graphics/rhi/yup_GpuBuffer.h b/modules/yup_rhi/rhi/yup_GpuBuffer.h similarity index 85% rename from modules/yup_graphics/rhi/yup_GpuBuffer.h rename to modules/yup_rhi/rhi/yup_GpuBuffer.h index 2c8b1544c..ea757943d 100644 --- a/modules/yup_graphics/rhi/yup_GpuBuffer.h +++ b/modules/yup_rhi/rhi/yup_GpuBuffer.h @@ -22,16 +22,7 @@ namespace yup { -class GraphicsContext; - -//============================================================================== -/** Identifies the intended usage of a GpuBuffer. */ -enum class GpuBufferType : uint8_t -{ - vertex, ///< Per-vertex attribute data, bound via GpuRenderPass::setVertexBuffer(). - index, ///< Index data, bound via GpuRenderPass::setIndexBuffer(). - uniform, ///< Uniform (constant) data. -}; +class GpuDevice; //============================================================================== /** A reference-counted GPU buffer handle. @@ -54,7 +45,7 @@ class YUP_API GpuBuffer : public ReferenceCountedObject //============================================================================== /** Creates a GPU buffer and uploads the given data. - @param ctx A GraphicsContext where GPU context is available. + @param ctx A GpuDevice where GPU context is available. @param type The intended usage of the buffer. @param data Pointer to the source data to upload (must be non-null). @param byteSize Number of bytes to upload (must be greater than zero). @@ -63,7 +54,7 @@ class YUP_API GpuBuffer : public ReferenceCountedObject @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). */ - static GpuBuffer::Ptr create (GraphicsContext& ctx, + static GpuBuffer::Ptr create (GpuDevice::Ptr ctx, GpuBufferType type, const void* data, size_t byteSize); diff --git a/modules/yup_graphics/rhi/yup_GpuFrame.cpp b/modules/yup_rhi/rhi/yup_GpuFrame.cpp similarity index 97% rename from modules/yup_graphics/rhi/yup_GpuFrame.cpp rename to modules/yup_rhi/rhi/yup_GpuFrame.cpp index 2d25e8abe..377baae84 100644 --- a/modules/yup_graphics/rhi/yup_GpuFrame.cpp +++ b/modules/yup_rhi/rhi/yup_GpuFrame.cpp @@ -50,11 +50,11 @@ const GpuFrame::Impl* GpuFrame::getImpl() const noexcept //============================================================================== -GpuFrame GpuFrame::begin (GraphicsContext& ctx) +GpuFrame GpuFrame::begin (GpuDevice::Ptr ctx) { GpuFrame frame; - auto* oreCtx = ctx.gpuContext(); + auto* oreCtx = ctx->gpuContext(); if (oreCtx == nullptr) return frame; diff --git a/modules/yup_graphics/rhi/yup_GpuFrame.h b/modules/yup_rhi/rhi/yup_GpuFrame.h similarity index 96% rename from modules/yup_graphics/rhi/yup_GpuFrame.h rename to modules/yup_rhi/rhi/yup_GpuFrame.h index 5b062db84..6f8d901a2 100644 --- a/modules/yup_graphics/rhi/yup_GpuFrame.h +++ b/modules/yup_rhi/rhi/yup_GpuFrame.h @@ -22,7 +22,7 @@ namespace yup { -class GraphicsContext; +class GpuDevice; //============================================================================== /** RAII scope for a single GPU frame. @@ -57,13 +57,13 @@ class YUP_API GpuFrame Returns an invalid frame (isValid() == false) if the context has no GPU context available on this backend. - @param ctx A GraphicsContext with GPU context available. + @param ctx A GpuDevice with GPU context available. @returns A valid GpuFrame on success, or an invalid frame on failure. @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). */ - static GpuFrame begin (GraphicsContext& ctx); + static GpuFrame begin (GpuDevice::Ptr ctx); //============================================================================== /** Move constructor. */ diff --git a/modules/yup_graphics/rhi/yup_GpuPipeline.cpp b/modules/yup_rhi/rhi/yup_GpuPipeline.cpp similarity index 97% rename from modules/yup_graphics/rhi/yup_GpuPipeline.cpp rename to modules/yup_rhi/rhi/yup_GpuPipeline.cpp index 796666a2c..6a111b86c 100644 --- a/modules/yup_graphics/rhi/yup_GpuPipeline.cpp +++ b/modules/yup_rhi/rhi/yup_GpuPipeline.cpp @@ -261,16 +261,16 @@ const GpuPipeline::Impl* GpuPipeline::getImpl() const noexcept //============================================================================== -ResultValue GpuPipeline::compile (GraphicsContext& ctx, +ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, const GpuShaderSource& vs, const GpuShaderSource& fs, const GpuPipelineOptions& pipelineOptions) { using namespace GpuPipelineHelpers; - auto oreCtx = ctx.gpuContext(); + auto oreCtx = ctx->gpuContext(); if (oreCtx == nullptr) - return makeResultValueFail ("GraphicsContext was not created with Options::enableOreContext = true"); + return makeResultValueFail ("GpuDevice was not created with Options::enableOreContext = true"); if (vs.code == nullptr || vs.codeSize == 0) return makeResultValueFail ("Vertex shader code is empty"); @@ -627,32 +627,32 @@ ResultValue GpuPipeline::compile (GraphicsContext& ctx, namespace { -ShaderLanguage shaderLanguageForApi (GraphicsContext::Api api) +ShaderLanguage shaderLanguageForApi (GpuPlatform api) { switch (api) { - case GraphicsContext::Metal: + case GpuPlatform::Metal: return ShaderLanguage::msl; - case GraphicsContext::Direct3D: + case GpuPlatform::Direct3D: return ShaderLanguage::hlsl; - case GraphicsContext::OpenGLES: + case GpuPlatform::OpenGLES: return ShaderLanguage::essl; - case GraphicsContext::WebGPU: + case GpuPlatform::WebGPU: return ShaderLanguage::wgsl; default: return ShaderLanguage::glsl; } } -GpuShaderLanguage gpuShaderLanguageForApi (GraphicsContext::Api api) +GpuShaderLanguage gpuShaderLanguageForApi (GpuPlatform api) { switch (api) { - case GraphicsContext::Metal: + case GpuPlatform::Metal: return GpuShaderLanguage::msl; - case GraphicsContext::Direct3D: + case GpuPlatform::Direct3D: return GpuShaderLanguage::hlsl; - case GraphicsContext::WebGPU: + case GpuPlatform::WebGPU: return GpuShaderLanguage::wgsl; default: return GpuShaderLanguage::glsl; @@ -661,11 +661,11 @@ GpuShaderLanguage gpuShaderLanguageForApi (GraphicsContext::Api api) } // namespace -ResultValue GpuPipeline::compileFromBundle (GraphicsContext& ctx, +ResultValue GpuPipeline::compileFromBundle (GpuDevice::Ptr ctx, const ShaderBundle& bundle, const GpuPipelineOptions& pipelineOptions) { - const auto api = ctx.getApi(); + const auto api = ctx->getPlatform(); const auto targetLang = shaderLanguageForApi (api); const auto gpuLang = gpuShaderLanguageForApi (api); @@ -738,12 +738,12 @@ ResultValue GpuPipeline::compileFromBundle (GraphicsContext& c #if YUP_ENABLE_SHADER_TRANSPILER -ResultValue GpuPipeline::compileFromGlsl (GraphicsContext& ctx, +ResultValue GpuPipeline::compileFromGlsl (GpuDevice::Ptr ctx, const String& vertexGlsl, const String& fragmentGlsl, const GpuPipelineOptions& pipelineOptions) { - const auto targetLang = shaderLanguageForApi (ctx.getApi()); + const auto targetLang = shaderLanguageForApi (ctx->getPlatform()); ShaderBundleCompiler compiler; diff --git a/modules/yup_rhi/rhi/yup_GpuPipeline.h b/modules/yup_rhi/rhi/yup_GpuPipeline.h new file mode 100644 index 000000000..d5b6fc0bc --- /dev/null +++ b/modules/yup_rhi/rhi/yup_GpuPipeline.h @@ -0,0 +1,135 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +class GpuDevice; + +//============================================================================== +/** An immutable, compiled GPU render pipeline. + + GpuPipeline wraps an ore (Rive's backend-agnostic GPU layer) render pipeline + consisting of a vertex shader and a fragment shader plus fixed pipeline + state. It supports both fullscreen post-process effects and custom geometry + rendering (indexed or non-indexed) with vertex buffers, culling, and + depth-stencil state. + + A pipeline is immutable once compiled: mutable binding state and per-draw + encoding live on GpuRenderPass. Compile a pipeline once (or fetch it from a + GpuPipelineCache) and reuse it across frames and render passes. + + @warning Requires the GpuDevice with a GPU context available on this backend. + + @see GpuRenderPass, GpuFrame, GpuPipelineCache, GpuPipelineOptions +*/ +class YUP_API GpuPipeline : public ReferenceCountedObject +{ +public: + using Ptr = ReferenceCountedObjectPtr; + + //============================================================================== + ~GpuPipeline(); + + //============================================================================== + /** Compiles a GpuPipeline from vertex and fragment shader sources. + + Both shaders must supply pre-compiled RSTB binding-map blobs via + GpuShaderSource::bindingMap. On failure the returned ResultValue holds a + human-readable description of the failure. + + @param ctx A GpuDevice with GPU context available. + @param vertexShader Vertex shader source and binding-map sidecar. + @param fragmentShader Fragment shader source and binding-map sidecar. + @param pipelineOptions Pipeline configuration. + + @returns A compiled pipeline, or a failure with a human-readable description. + + @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). + */ + static ResultValue compile (GpuDevice::Ptr ctx, + const GpuShaderSource& vertexShader, + const GpuShaderSource& fragmentShader, + const GpuPipelineOptions& pipelineOptions = {}); + + /** Compiles a GpuPipeline from a pre-built shader bundle. + + The bundle must contain both a vertex and a fragment shader stage. Picks + the native shader variant matching the context's graphics API for each + stage (Metal→MSL, Direct3D→HLSL, OpenGL(ES)→GLSL/ESSL, WebGPU→WGSL), + derives the mandatory binding-map sidecar from the bundled reflection data, + and compiles the pipeline. This is the recommended way to consume shaders + loaded from .ysl files, and works without the shader transpiler. + + @param ctx A GpuDevice with GPU context available. + @param bundle Bundle containing the vertex and fragment stages. + @param pipelineOptions Pipeline configuration. + + @returns A compiled pipeline, or a failure with a human-readable description. + + @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). + + @see ShaderBundle + */ + static ResultValue compileFromBundle (GpuDevice::Ptr ctx, + const ShaderBundle& bundle, + const GpuPipelineOptions& pipelineOptions = {}); + +#if YUP_ENABLE_SHADER_TRANSPILER + /** Compiles a GpuPipeline directly from GLSL 450 vertex and fragment sources. + + Convenience that transpiles the GLSL to the native language of the + context's graphics API, derives the binding-map sidecar via reflection, + and compiles the pipeline. Only available when the shader transpiler is + compiled in (YUP_ENABLE_SHADER_TRANSPILER = 1). + + @param ctx A GpuDevice with GPU context available. + @param vertexGlsl GLSL 450 vertex shader source. + @param fragmentGlsl GLSL 450 fragment shader source. + @param pipelineOptions Pipeline configuration. + + @returns A compiled pipeline, or a failure with a human-readable description. + + @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). + + */ + static ResultValue compileFromGlsl (GpuDevice::Ptr ctx, + const String& vertexGlsl, + const String& fragmentGlsl, + const GpuPipelineOptions& pipelineOptions = {}); +#endif + +private: + friend class GpuRenderPass; + + GpuPipeline() = default; + + struct Impl; + Impl* getImpl() noexcept; + const Impl* getImpl() const noexcept; + + static constexpr size_t ImplSizeBytes = 384; + TypeErasedObject impl; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuPipeline) +}; + +} // namespace yup diff --git a/modules/yup_graphics/rhi/yup_GpuPipelineCache.cpp b/modules/yup_rhi/rhi/yup_GpuPipelineCache.cpp similarity index 94% rename from modules/yup_graphics/rhi/yup_GpuPipelineCache.cpp rename to modules/yup_rhi/rhi/yup_GpuPipelineCache.cpp index 5ede03f7d..9d3351327 100644 --- a/modules/yup_graphics/rhi/yup_GpuPipelineCache.cpp +++ b/modules/yup_rhi/rhi/yup_GpuPipelineCache.cpp @@ -27,17 +27,17 @@ namespace yup namespace { -ShaderLanguage cacheShaderLanguageForApi (GraphicsContext::Api api) +ShaderLanguage cacheShaderLanguageForApi (GpuPlatform api) { switch (api) { - case GraphicsContext::Metal: + case GpuPlatform::Metal: return ShaderLanguage::msl; - case GraphicsContext::Direct3D: + case GpuPlatform::Direct3D: return ShaderLanguage::hlsl; - case GraphicsContext::OpenGLES: + case GpuPlatform::OpenGLES: return ShaderLanguage::essl; - case GraphicsContext::WebGPU: + case GpuPlatform::WebGPU: return ShaderLanguage::wgsl; default: return ShaderLanguage::glsl; @@ -102,8 +102,8 @@ void appendPipelineOptions (String& payload, const GpuPipelineOptions& o) //============================================================================== -GpuPipelineCache::GpuPipelineCache (GraphicsContext& contextToUse) - : context (contextToUse) +GpuPipelineCache::GpuPipelineCache (GpuDevice::Ptr contextToUse) + : context (std::move (contextToUse)) { } @@ -113,7 +113,7 @@ GpuPipelineCache::~GpuPipelineCache() = default; String GpuPipelineCache::generateCacheKey (const ShaderBundle& bundle, const GpuPipelineOptions& options, - GraphicsContext::Api api) + GpuPlatform api) { const auto targetLang = cacheShaderLanguageForApi (api); @@ -144,7 +144,7 @@ String GpuPipelineCache::generateCacheKey (const ShaderBundle& bundle, ResultValue GpuPipelineCache::getOrCompile (const ShaderBundle& bundle, const GpuPipelineOptions& options) { - const auto key = generateCacheKey (bundle, options, context.getApi()); + const auto key = generateCacheKey (bundle, options, context->getPlatform()); return getOrCompile (key, bundle, options); } diff --git a/modules/yup_graphics/rhi/yup_GpuPipelineCache.h b/modules/yup_rhi/rhi/yup_GpuPipelineCache.h similarity index 93% rename from modules/yup_graphics/rhi/yup_GpuPipelineCache.h rename to modules/yup_rhi/rhi/yup_GpuPipelineCache.h index 6121217e3..61230c276 100644 --- a/modules/yup_graphics/rhi/yup_GpuPipelineCache.h +++ b/modules/yup_rhi/rhi/yup_GpuPipelineCache.h @@ -22,7 +22,7 @@ namespace yup { -class GraphicsContext; +class GpuDevice; //============================================================================== /** A thread-safe cache for compiled GpuPipelines. @@ -32,7 +32,7 @@ class GraphicsContext; pipeline options, and graphics API. When a subsequent request matches an existing cache key, the cached pipeline is returned without recompilation. - The cache references an externally-owned GraphicsContext, which must outlive + The cache references an externally-owned GpuDevice, which must outlive the cache. Eviction uses a configurable entry-count limit (LRU by access order). @@ -49,12 +49,10 @@ class YUP_API GpuPipelineCache final //============================================================================== /** Creates a cache that uses the given context for miss compilations. - The context must outlive this cache. - - @warning Requires contextToUse.isGpuAvailable() (GPU context available on this backend). - + The cache keeps the context alive for its lifetime. + @warning Requires contextToUse->isGpuAvailable(). */ - explicit GpuPipelineCache (GraphicsContext& contextToUse); + explicit GpuPipelineCache (GpuDevice::Ptr contextToUse); /** Destructor. */ ~GpuPipelineCache(); @@ -135,7 +133,7 @@ class YUP_API GpuPipelineCache final */ static String generateCacheKey (const ShaderBundle& bundle, const GpuPipelineOptions& options, - GraphicsContext::Api api); + GpuPlatform api); private: struct Entry @@ -146,7 +144,7 @@ class YUP_API GpuPipelineCache final void evictIfNeeded(); - GraphicsContext& context; + GpuDevice::Ptr context; std::map cache; size_t maxEntries = 256; uint64 accessCounter = 0; diff --git a/modules/yup_graphics/rhi/yup_GpuRenderPass.cpp b/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp similarity index 98% rename from modules/yup_graphics/rhi/yup_GpuRenderPass.cpp rename to modules/yup_rhi/rhi/yup_GpuRenderPass.cpp index ee52aec54..8800e7b6b 100644 --- a/modules/yup_graphics/rhi/yup_GpuRenderPass.cpp +++ b/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp @@ -243,10 +243,10 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) rpDesc.colorAttachments[0].view = outputView.get(); rpDesc.colorAttachments[0].loadOp = options.clear ? rive::ore::LoadOp::clear : rive::ore::LoadOp::load; rpDesc.colorAttachments[0].storeOp = rive::ore::StoreOp::store; - rpDesc.colorAttachments[0].clearColor = { options.clearColor.getRedFloat(), - options.clearColor.getGreenFloat(), - options.clearColor.getBlueFloat(), - options.clearColor.getAlphaFloat() }; + rpDesc.colorAttachments[0].clearColor = { options.clearColor.red, + options.clearColor.green, + options.clearColor.blue, + options.clearColor.alpha }; auto renderPass = oreCtx->beginRenderPass (rpDesc); renderPass->setPipeline (orePipeline); @@ -317,15 +317,15 @@ bool GpuRenderPass::isValid() const noexcept //============================================================================== -void GpuRenderPass::setPipeline (GpuPipeline& pipeline) +void GpuRenderPass::setPipeline (GpuPipeline::Ptr pipeline) { auto* i = getImpl(); if (i == nullptr) return; - i->pipelineRef = &pipeline; + i->pipelineRef = pipeline; - if (auto* pipeImpl = pipeline.getImpl()) + if (auto* pipeImpl = pipeline->getImpl()) { i->orePipeline = pipeImpl->pipeline.get(); i->oreLayouts = &pipeImpl->layouts; diff --git a/modules/yup_graphics/rhi/yup_GpuRenderPass.h b/modules/yup_rhi/rhi/yup_GpuRenderPass.h similarity index 88% rename from modules/yup_graphics/rhi/yup_GpuRenderPass.h rename to modules/yup_rhi/rhi/yup_GpuRenderPass.h index f479b3b90..adeb8df56 100644 --- a/modules/yup_graphics/rhi/yup_GpuRenderPass.h +++ b/modules/yup_rhi/rhi/yup_GpuRenderPass.h @@ -28,28 +28,6 @@ class GpuBuffer; class GpuCanvas; class GpuFrame; -//============================================================================== -/** Per-render-pass options controlling attachment load behaviour. */ -struct GpuRenderOptions -{ - /** Default constructor. */ - constexpr GpuRenderOptions() = default; - - /** Constructs a GpuRenderOptions with the given clear flag and clear color. */ - constexpr GpuRenderOptions (bool clear, Color clearColor) - : clear (clear) - , clearColor (clearColor) - { - } - - /** Whether to clear the target before drawing (LoadOp::clear). When false - the existing contents are loaded (LoadOp::load). */ - bool clear = true; - - /** Clear color used when @c clear is true. */ - Color clearColor = Colors::transparentBlack; -}; - //============================================================================== /** A transient render-pass encoder targeting a GpuCanvas. @@ -97,7 +75,7 @@ class YUP_API GpuRenderPass @param pipeline The GpuPipeline to use. */ - void setPipeline (GpuPipeline& pipeline); + void setPipeline (GpuPipeline::Ptr pipeline); /** Binds a texture to the given (group, binding) slot. diff --git a/modules/yup_graphics/rhi/yup_GpuTarget.cpp b/modules/yup_rhi/rhi/yup_GpuTarget.cpp similarity index 86% rename from modules/yup_graphics/rhi/yup_GpuTarget.cpp rename to modules/yup_rhi/rhi/yup_GpuTarget.cpp index 03c487760..0795ed46f 100644 --- a/modules/yup_graphics/rhi/yup_GpuTarget.cpp +++ b/modules/yup_rhi/rhi/yup_GpuTarget.cpp @@ -22,28 +22,28 @@ namespace yup { -GpuTarget::Ptr GpuTarget::create (GraphicsContext& ctx, int width, int height) +GpuTarget::Ptr GpuTarget::create (GpuDevice::Ptr ctx, int width, int height) { if (width <= 0 || height <= 0) return nullptr; - auto target = ctx.createOffscreenTarget (width, height); + auto target = ctx->createOffscreenTarget (width, height); if (target == nullptr) return nullptr; GpuTarget::Ptr result = new GpuTarget(); - result->ctx = &ctx; + result->ctx = ctx; result->offscreenTarget = std::move (target); return result; } -GpuTarget::Ptr GpuTarget::createFromTarget (GraphicsContext& ctx, std::unique_ptr target) +GpuTarget::Ptr GpuTarget::createFromTarget (GpuDevice::Ptr ctx, std::unique_ptr target) { if (target == nullptr) return nullptr; GpuTarget::Ptr result = new GpuTarget(); - result->ctx = &ctx; + result->ctx = ctx; result->renderableTarget = target.get(); result->offscreenTarget = std::move (target); return result; @@ -90,19 +90,6 @@ GpuTexture::Ptr GpuTarget::asTexture() return cachedTexture; } -Image GpuTarget::asImage() -{ - auto img = Image::fromTexture (asTexture()); - - if (img.isValid()) - { - auto span = img.getRawData(); - readPixels (span.data(), span.size()); - } - - return img; -} - bool GpuTarget::readPixels (void* dst, size_t byteSize) { if (offscreenTarget == nullptr || ctx == nullptr) diff --git a/modules/yup_graphics/rhi/yup_GpuTarget.h b/modules/yup_rhi/rhi/yup_GpuTarget.h similarity index 86% rename from modules/yup_graphics/rhi/yup_GpuTarget.h rename to modules/yup_rhi/rhi/yup_GpuTarget.h index 277a4f8e6..7eb6cca66 100644 --- a/modules/yup_graphics/rhi/yup_GpuTarget.h +++ b/modules/yup_rhi/rhi/yup_GpuTarget.h @@ -22,7 +22,7 @@ namespace yup { -class GraphicsContext; +class GpuDevice; class GpuTexture; class GpuFrame; class GpuRenderPass; @@ -47,7 +47,7 @@ class Image; if (target != nullptr) { auto frame = yup::GpuFrame::begin (ctx); - auto pass = target->beginRenderPass (frame, { true, yup::Colors::transparentBlack }); + auto pass = target->beginRenderPass (frame, { true, yup::GpuColor::transparentBlack() }); pass.setPipeline (*pipeline); pass.draw (3); pass.finish(); @@ -70,7 +70,7 @@ class YUP_API GpuTarget : public ReferenceCountedObject //============================================================================== /** Creates a GpuTarget of the given pixel dimensions. - Returns nullptr if the GraphicsContext cannot allocate offscreen GPU resources + Returns nullptr if the GpuDevice cannot allocate offscreen GPU resources (e.g. headless context with no GPU). @param ctx The graphics context that owns the GPU device. @@ -81,7 +81,7 @@ class YUP_API GpuTarget : public ReferenceCountedObject @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). */ - static GpuTarget::Ptr create (GraphicsContext& ctx, int width, int height); + static GpuTarget::Ptr create (GpuDevice::Ptr ctx, int width, int height); //============================================================================== /** Returns the width of this target in pixels. */ @@ -113,17 +113,6 @@ class YUP_API GpuTarget : public ReferenceCountedObject */ GpuTexture::Ptr asTexture(); - /** Returns an Image with both GPU texture and CPU pixel data populated. - - Calls asTexture() to obtain the GPU resource, creates an Image wrapping it, - and then calls readPixels() to fill the CPU-side ImagePixelData. Returns an - empty Image on failure. For GPU-only compositing without CPU readback, prefer - using asTexture() and Graphics::drawTexture. - - @returns An Image, or an empty Image on failure. - */ - Image asImage(); - //============================================================================== /** Reads rendered pixels back to CPU memory. @@ -144,11 +133,11 @@ class YUP_API GpuTarget : public ReferenceCountedObject GpuTarget() = default; - static GpuTarget::Ptr createFromTarget (GraphicsContext& ctx, std::unique_ptr target); + static GpuTarget::Ptr createFromTarget (GpuDevice::Ptr ctx, std::unique_ptr target); RenderableTarget* getRenderableTarget() const noexcept { return renderableTarget; } - GraphicsContext* ctx = nullptr; + GpuDevice::Ptr ctx; std::unique_ptr offscreenTarget; RenderableTarget* renderableTarget = nullptr; GpuTexture::Ptr cachedTexture; diff --git a/modules/yup_graphics/rhi/yup_GpuTexture.cpp b/modules/yup_rhi/rhi/yup_GpuTexture.cpp similarity index 100% rename from modules/yup_graphics/rhi/yup_GpuTexture.cpp rename to modules/yup_rhi/rhi/yup_GpuTexture.cpp diff --git a/modules/yup_graphics/rhi/yup_GpuTexture.h b/modules/yup_rhi/rhi/yup_GpuTexture.h similarity index 100% rename from modules/yup_graphics/rhi/yup_GpuTexture.h rename to modules/yup_rhi/rhi/yup_GpuTexture.h diff --git a/modules/yup_graphics/rhi/yup_GpuPipeline.h b/modules/yup_rhi/rhi/yup_GpuTypes.h similarity index 68% rename from modules/yup_graphics/rhi/yup_GpuPipeline.h rename to modules/yup_rhi/rhi/yup_GpuTypes.h index 1898c26f5..4fbf43254 100644 --- a/modules/yup_graphics/rhi/yup_GpuPipeline.h +++ b/modules/yup_rhi/rhi/yup_GpuTypes.h @@ -22,6 +22,18 @@ namespace yup { +//============================================================================== +/** Enumerates supported GPU platforms / backends. */ +enum class GpuPlatform +{ + Headless, ///< Specifies the use of a headless context for rendering. + OpenGL, ///< Specifies the use of desktop OpenGL for rendering. + OpenGLES, ///< Specifies the use of OpenGL ES (GLES 3.0+) for rendering (Android, WASM). + Direct3D, ///< Specifies the use of Direct3D for rendering. + Metal, ///< Specifies the use of Metal for rendering. + WebGPU ///< Specifies the use of WebGPU (native browser WebGPU on Emscripten, Dawn elsewhere). +}; + //============================================================================== /** Identifies the shading language of a GpuShaderSource code block. */ enum class GpuShaderLanguage : uint8_t @@ -33,14 +45,13 @@ enum class GpuShaderLanguage : uint8_t }; //============================================================================== -/** Compiled shader source for one pipeline stage (vertex or fragment). +/** Compiled shader source for one pipeline stage (vertex, fragment, or compute). - The binding-map sidecar (@c bindingMap / @c bindingMapSize) is mandatory. - It is produced offline by the Rive scripting-workspace RSTB toolchain and - must accompany the shader code. GpuPipeline::compile() will assert and fail - if the sidecar is missing. + The binding-map sidecar (@c bindingMap / @c bindingMapSize) is mandatory for + vertex/fragment stages. Compute shaders may omit it when using the native + compute path (GpuComputePipeline). - @see GpuPipeline + @see GpuPipeline, GpuComputePipeline */ struct GpuShaderSource { @@ -55,7 +66,7 @@ struct GpuShaderSource /** Number of bytes in @c code. */ uint32_t codeSize = 0; - /** Mandatory pre-compiled RSTB binding-map sidecar blob. */ + /** Mandatory pre-compiled RSTB binding-map sidecar blob (render pipelines). */ const uint8_t* bindingMap = nullptr; /** Number of bytes in @c bindingMap. */ @@ -77,6 +88,16 @@ struct GpuShaderSource const char* entryPoint = nullptr; }; +//============================================================================== +/** Identifies the intended usage of a GpuBuffer. */ +enum class GpuBufferType : uint8_t +{ + vertex, ///< Per-vertex attribute data, bound via GpuRenderPass::setVertexBuffer(). + index, ///< Index data, bound via GpuRenderPass::setIndexBuffer(). + uniform, ///< Uniform (constant) data. + storage, ///< Storage buffer (read-write SSBO, for compute shaders). +}; + //============================================================================== /** Per-vertex attribute data format. Mirrors the ore vertex formats. */ enum class GpuVertexFormat : uint8_t @@ -319,114 +340,101 @@ struct GpuPipelineOptions }; //============================================================================== -class GraphicsContext; - -//============================================================================== -/** An immutable, compiled GPU render pipeline. +/** A lightweight 4-component color for GPU clear values and render options. - GpuPipeline wraps an ore (Rive's backend-agnostic GPU layer) render pipeline - consisting of a vertex shader and a fragment shader plus fixed pipeline - state. It supports both fullscreen post-process effects and custom geometry - rendering (indexed or non-indexed) with vertex buffers, culling, and - depth-stencil state. - - A pipeline is immutable once compiled: mutable binding state and per-draw - encoding live on GpuRenderPass. Compile a pipeline once (or fetch it from a - GpuPipelineCache) and reuse it across frames and render passes. - - @warning Requires the GraphicsContext with a GPU context available on this backend. - - @see GpuRenderPass, GpuFrame, GpuCanvas, GpuPipelineCache, GpuPipelineOptions + Lives in yup_rhi to avoid a dependency on yup_graphics. Individual components + are float in [0, 1]. Implicitly constructable from any type T that exposes + getRedFloat(), getGreenFloat(), getBlueFloat(), getAlphaFloat() — e.g. + yup::Color, so GpuRenderOptions { true, Colors::transparentBlack } just works. */ -class YUP_API GpuPipeline : public ReferenceCountedObject +struct GpuColor { -public: - using Ptr = ReferenceCountedObjectPtr; - - //============================================================================== - ~GpuPipeline(); - - //============================================================================== - /** Compiles a GpuPipeline from vertex and fragment shader sources. - - Both shaders must supply pre-compiled RSTB binding-map blobs via - GpuShaderSource::bindingMap. On failure the returned ResultValue holds a - human-readable description of the failure. - - @param ctx A GraphicsContext with GPU context available. - @param vertexShader Vertex shader source and binding-map sidecar. - @param fragmentShader Fragment shader source and binding-map sidecar. - @param pipelineOptions Pipeline configuration. - - @returns A compiled pipeline, or a failure with a human-readable description. - - @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). - */ - static ResultValue compile (GraphicsContext& ctx, - const GpuShaderSource& vertexShader, - const GpuShaderSource& fragmentShader, - const GpuPipelineOptions& pipelineOptions = {}); + constexpr GpuColor() = default; - /** Compiles a GpuPipeline from a pre-built shader bundle. - - The bundle must contain both a vertex and a fragment shader stage. Picks - the native shader variant matching the context's graphics API for each - stage (Metal→MSL, Direct3D→HLSL, OpenGL(ES)→GLSL/ESSL, WebGPU→WGSL), - derives the mandatory binding-map sidecar from the bundled reflection data, - and compiles the pipeline. This is the recommended way to consume shaders - loaded from .ysl files, and works without the shader transpiler. - - @param ctx A GraphicsContext with GPU context available. - @param bundle Bundle containing the vertex and fragment stages. - @param pipelineOptions Pipeline configuration. - - @returns A compiled pipeline, or a failure with a human-readable description. + constexpr GpuColor (float r, float g, float b, float a = 1.0f) + : red (r) + , green (g) + , blue (b) + , alpha (a) + { + } - @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). + /** Implicit conversion from any type with float-component accessors + (e.g. yup::Color). */ + template + requires requires (const T& c) { + c.getRedFloat(); + c.getGreenFloat(); + c.getBlueFloat(); + c.getAlphaFloat(); + } + constexpr GpuColor (const T& color) + : red (color.getRedFloat()) + , green (color.getGreenFloat()) + , blue (color.getBlueFloat()) + , alpha (color.getAlphaFloat()) + { + } - @see ShaderBundle - */ - static ResultValue compileFromBundle (GraphicsContext& ctx, - const ShaderBundle& bundle, - const GpuPipelineOptions& pipelineOptions = {}); + /** Explicit construct from a packed ARGB value (0xAARRGGBB). */ + explicit constexpr GpuColor (uint32_t argb) + : red (((argb >> 16) & 0xFF) / 255.0f) + , green (((argb >> 8) & 0xFF) / 255.0f) + , blue (((argb >> 0) & 0xFF) / 255.0f) + , alpha (((argb >> 24) & 0xFF) / 255.0f) + { + } -#if YUP_ENABLE_SHADER_TRANSPILER - /** Compiles a GpuPipeline directly from GLSL 450 vertex and fragment sources. + float red = 0.0f; + float green = 0.0f; + float blue = 0.0f; + float alpha = 0.0f; - Convenience that transpiles the GLSL to the native language of the - context's graphics API, derives the binding-map sidecar via reflection, - and compiles the pipeline. Only available when the shader transpiler is - compiled in (YUP_ENABLE_SHADER_TRANSPILER = 1). + /** Opaque black (0, 0, 0, 1). */ + static constexpr GpuColor black() { return { 0.0f, 0.0f, 0.0f, 1.0f }; } - @param ctx A GraphicsContext with GPU context available. - @param vertexGlsl GLSL 450 vertex shader source. - @param fragmentGlsl GLSL 450 fragment shader source. - @param pipelineOptions Pipeline configuration. + /** Transparent black (0, 0, 0, 0). */ + static constexpr GpuColor transparentBlack() { return { 0.0f, 0.0f, 0.0f, 0.0f }; } - @returns A compiled pipeline, or a failure with a human-readable description. + /** Opaque white (1, 1, 1, 1). */ + static constexpr GpuColor white() { return { 1.0f, 1.0f, 1.0f, 1.0f }; } +}; - @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). +//============================================================================== +/** Per-render-pass options controlling attachment load behaviour. */ +struct GpuRenderOptions +{ + /** Default constructor. */ + constexpr GpuRenderOptions() = default; - */ - static ResultValue compileFromGlsl (GraphicsContext& ctx, - const String& vertexGlsl, - const String& fragmentGlsl, - const GpuPipelineOptions& pipelineOptions = {}); -#endif + /** Constructs a GpuRenderOptions with the given clear flag and clear color. */ + constexpr GpuRenderOptions (bool clear, GpuColor clearColor) + : clear (clear) + , clearColor (clearColor) + { + } -private: - friend class GpuRenderPass; + /** Whether to clear the target before drawing (LoadOp::clear). When false + the existing contents are loaded (LoadOp::load). */ + bool clear = true; - GpuPipeline() = default; + /** Clear color used when @c clear is true. */ + GpuColor clearColor = GpuColor::transparentBlack(); +}; - struct Impl; - Impl* getImpl() noexcept; - const Impl* getImpl() const noexcept; +//============================================================================== +/** Workgroup size for compute shader dispatch. - static constexpr size_t ImplSizeBytes = 384; - TypeErasedObject impl; + Captures the local workgroup size declared in the compute shader via + @c layout(local_size_x = X, local_size_y = Y, local_size_z = Z). - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuPipeline) + @see GpuComputePipeline +*/ +struct GpuWorkgroupSize +{ + uint32_t x = 1; + uint32_t y = 1; + uint32_t z = 1; }; } // namespace yup diff --git a/modules/yup_graphics/rhi/yup_ShaderBindingMap.cpp b/modules/yup_rhi/rhi/yup_ShaderBindingMap.cpp similarity index 100% rename from modules/yup_graphics/rhi/yup_ShaderBindingMap.cpp rename to modules/yup_rhi/rhi/yup_ShaderBindingMap.cpp diff --git a/modules/yup_graphics/rhi/yup_ShaderBindingMap.h b/modules/yup_rhi/rhi/yup_ShaderBindingMap.h similarity index 100% rename from modules/yup_graphics/rhi/yup_ShaderBindingMap.h rename to modules/yup_rhi/rhi/yup_ShaderBindingMap.h diff --git a/modules/yup_rhi/yup_rhi.cpp b/modules/yup_rhi/yup_rhi.cpp new file mode 100644 index 000000000..d387bb240 --- /dev/null +++ b/modules/yup_rhi/yup_rhi.cpp @@ -0,0 +1,87 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +//============================================================================== +#if YUP_WINDOWS +#if YUP_RIVE_USE_D3D +#include +#include +#endif +#if YUP_RIVE_USE_OPENGL +#include +#endif + +#elif YUP_MAC || YUP_IOS +#if YUP_RIVE_USE_METAL +#import +#import +#endif + +#elif YUP_LINUX || YUP_WASM || YUP_ANDROID +#if YUP_EMSCRIPTEN && RIVE_WEBGPU +#include +#include +#include +#elif YUP_EMSCRIPTEN && RIVE_WEBGL +#include +#include +#endif +#include +#endif + +#if YUP_RIVE_USE_DAWN +#include "dawn/native/DawnNative.h" +#include "dawn/dawn_proc.h" +#endif + +//============================================================================== +#include "native/yup_GpuDevice_headless.cpp" + +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) +#include "native/yup_GpuDevice_metal.cpp" +#endif + +#if YUP_RIVE_USE_D3D && YUP_WINDOWS +#include "native/yup_GpuDevice_d3d.cpp" +#endif + +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) +#include "native/yup_GpuDevice_opengl.cpp" +#endif + +#if YUP_EMSCRIPTEN && RIVE_WEBGPU +#include "native/yup_GpuDevice_webgpu.cpp" +#elif YUP_RIVE_USE_DAWN +#include "native/yup_GpuDevice_dawn.cpp" +#endif + +//============================================================================== +#include "rhi/yup_GpuBuffer.cpp" +#include "rhi/yup_GpuFrame.cpp" +#include "rhi/yup_GpuPipeline.cpp" +#include "rhi/yup_GpuPipelineCache.cpp" +#include "rhi/yup_GpuRenderPass.cpp" +#include "rhi/yup_GpuTarget.cpp" +#include "rhi/yup_GpuTexture.cpp" +#include "rhi/yup_ShaderBindingMap.cpp" +#include "context/yup_GpuDevice.cpp" diff --git a/modules/yup_rhi/yup_rhi.h b/modules/yup_rhi/yup_rhi.h new file mode 100644 index 000000000..4b6b1c3f2 --- /dev/null +++ b/modules/yup_rhi/yup_rhi.h @@ -0,0 +1,75 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/* + ============================================================================== + + BEGIN_YUP_MODULE_DECLARATION + + ID: yup_rhi + vendor: yup + version: 2.0.0 + name: YUP RHI Classes + description: Low-level GPU abstraction layer (RHI) with compute and render-pipeline support. + website: https://github.com/kunitoki/yup + license: ISC + + dependencies: yup_core yup_shading rive_renderer + appleFrameworks: Metal + + END_YUP_MODULE_DECLARATION + + ============================================================================== +*/ + +#pragma once +#define YUP_RHI_H_INCLUDED + +#include +#include + +//============================================================================== +YUP_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") +#include +#include +#include +#include +#include +YUP_END_IGNORE_WARNINGS_GCC_LIKE + +//============================================================================== +#include +#include +#include + +//============================================================================== +#include "rhi/yup_GpuTypes.h" +#include "context/yup_OffscreenTarget.h" +#include "context/yup_RenderableTarget.h" +#include "context/yup_GpuDevice.h" +#include "rhi/yup_ShaderBindingMap.h" +#include "rhi/yup_GpuBuffer.h" +#include "rhi/yup_GpuTexture.h" +#include "rhi/yup_GpuFrame.h" +#include "rhi/yup_GpuPipeline.h" +#include "rhi/yup_GpuRenderPass.h" +#include "rhi/yup_GpuTarget.h" +#include "rhi/yup_GpuPipelineCache.h" diff --git a/modules/yup_rhi/yup_rhi.mm b/modules/yup_rhi/yup_rhi.mm new file mode 100644 index 000000000..b5d9d315e --- /dev/null +++ b/modules/yup_rhi/yup_rhi.mm @@ -0,0 +1,22 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "yup_rhi.cpp" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7f974ade2..afdc5af72 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -67,6 +67,7 @@ set (target_modules yup_data_model yup_graphics yup_animation + yup_rhi yup_shading dr_libs pffft_library diff --git a/tests/mocks/yup_graphics.h b/tests/mocks/yup_graphics.h index ec7ed8d1f..99d80ca8d 100644 --- a/tests/mocks/yup_graphics.h +++ b/tests/mocks/yup_graphics.h @@ -25,6 +25,8 @@ #include +#include "yup_rhi.h" + // ============================================================================== // Test helper: Delegate GraphicsContext that allows injecting a mock ore context. // @@ -36,12 +38,14 @@ class OreInjectedGraphicsContext : public yup::GraphicsContext { public: explicit OreInjectedGraphicsContext (rive::ore::Context* oreContextToUse) - : real (yup::GraphicsContext::createContext (yup::GraphicsContext::Headless, {})) + : real (yup::GraphicsContext::createContext (yup::GpuPlatform::Headless, {})) , injectedOreContext (oreContextToUse) { } - yup::GraphicsContext::Api getApi() const noexcept override { return real->getApi(); } + yup::GpuPlatform getPlatform() const noexcept override { return real->getPlatform(); } + + yup::GpuDevice::Ptr getGpuDevice() const noexcept override { return real->getGpuDevice(); } rive::Factory* factory() override { return real->factory(); } @@ -49,8 +53,6 @@ class OreInjectedGraphicsContext : public yup::GraphicsContext rive::gpu::RenderTarget* renderTarget() override { return real->renderTarget(); } - rive::ore::Context* gpuContext() const noexcept override { return injectedOreContext; } - std::unique_ptr makeRenderer (int width, int height) override { return real->makeRenderer (width, height); } void onSizeChanged (void* nativeHandle, int width, int height, float dpiScale, uint32_t sampleCount) override { real->onSizeChanged (nativeHandle, width, height, dpiScale, sampleCount); } @@ -59,100 +61,7 @@ class OreInjectedGraphicsContext : public yup::GraphicsContext void end (void* nativeHandle) override { real->end (nativeHandle); } - std::unique_ptr createOffscreenTarget (int width, int height) override { return real->createOffscreenTarget (width, height); } - - std::unique_ptr createRenderableTarget (int width, int height) override { return real->createRenderableTarget (width, height); } - - void beginOffscreen (yup::OffscreenTarget& target, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override { real->beginOffscreen (target, frameDesc); } - - void endOffscreen (yup::OffscreenTarget& target) override { real->endOffscreen (target); } - - bool readOffscreenPixels (yup::OffscreenTarget& target, void* dst, size_t dstSize) override { return real->readOffscreenPixels (target, dst, dstSize); } - private: std::unique_ptr real; rive::ore::Context* injectedOreContext = nullptr; }; - -// ============================================================================== -// Mock yup::GraphicsContext::RenderableTarget -// ============================================================================== - -class MockOffscreenTarget : public yup::RenderableTarget -{ -public: - explicit MockOffscreenTarget (int w, int h) - : width_ (w) - , height_ (h) - { - } - - int getWidth() const noexcept override { return width_; } - - int getHeight() const noexcept override { return height_; } - - rive::gpu::RenderTarget* getRenderTarget() noexcept override { return getRenderTargetProxy(); } - - rive::gpu::RenderContext* getRenderContext() noexcept override { return getRenderContextProxy(); } - - rive::rcp getRenderCanvas() noexcept override { return getRenderCanvasProxy(); } - - rive::rcp adoptAsTexture() override { return adoptAsTextureProxy(); } - - MOCK_METHOD (rive::gpu::RenderTarget*, getRenderTargetProxy, (), ()); - MOCK_METHOD (rive::gpu::RenderContext*, getRenderContextProxy, (), ()); - MOCK_METHOD (rive::rcp, getRenderCanvasProxy, (), ()); - MOCK_METHOD (rive::rcp, adoptAsTextureProxy, (), ()); - - /** Creates a MockOffscreenTarget pre-configured with a TestGpuTexture for adoptAsTexture. */ - static std::unique_ptr withGpuTexture (int w, int h) - { - auto t = std::make_unique<::testing::NiceMock> (w, h); - ON_CALL (*t, getRenderCanvasProxy()).WillByDefault (::testing::ReturnNull()); - ON_CALL (*t, adoptAsTextureProxy()) - .WillByDefault (::testing::Return (rive::make_rcp (w, h))); - return t; - } - -private: - int width_; - int height_; -}; - -// ============================================================================== -// Test helper: OreInjectedGraphicsContext that also injects an offscreen target. -// Overrides createOffscreenTarget / createRenderableTarget to return a pre-built -// MockOffscreenTarget. -// ============================================================================== - -class OreAndTargetGraphicsContext : public OreInjectedGraphicsContext -{ -public: - OreAndTargetGraphicsContext (rive::ore::Context* oreCtx, - std::unique_ptr target) - : OreInjectedGraphicsContext (oreCtx) - , injectedTarget (std::move (target)) - { - } - - std::unique_ptr createOffscreenTarget (int, int) override - { - // Move the target back out — the caller takes ownership. - // For repeated use, set up the mock to be reusable. - return std::move (injectedTarget); - } - - std::unique_ptr createRenderableTarget (int, int) override - { - // Move the target back out — the caller takes ownership. - return std::move (injectedTarget); - } - - void setNextOffscreenTarget (std::unique_ptr target) - { - injectedTarget = std::move (target); - } - -private: - std::unique_ptr injectedTarget; -}; diff --git a/tests/mocks/yup_rhi.h b/tests/mocks/yup_rhi.h new file mode 100644 index 000000000..87843908c --- /dev/null +++ b/tests/mocks/yup_rhi.h @@ -0,0 +1,143 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include + +#include + +// ============================================================================== +// Test helper: Delegate GpuDevice that allows injecting a mock ore context. +// +// Wraps a real (headless) GpuDevice and delegates all methods to it except +// gpuContext(), which returns the supplied rive::ore::Context*. +// ============================================================================== + +class OreInjectedGpuDevice : public yup::GpuDevice +{ +public: + explicit OreInjectedGpuDevice (rive::ore::Context* oreContextToUse) + : real (yup::GpuDevice::create (yup::GpuPlatform::Headless, {})) + , injectedOreContext (oreContextToUse) + { + } + + yup::GpuPlatform getPlatform() const noexcept override { return real->getPlatform(); } + + rive::ore::Context* gpuContext() const noexcept override { return injectedOreContext; } + + std::unique_ptr createOffscreenTarget (int width, int height) override { return real->createOffscreenTarget (width, height); } + + std::unique_ptr createRenderableTarget (int width, int height) override { return real->createRenderableTarget (width, height); } + + void beginOffscreen (yup::OffscreenTarget& target, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override { real->beginOffscreen (target, frameDesc); } + + void endOffscreen (yup::OffscreenTarget& target) override { real->endOffscreen (target); } + + bool readOffscreenPixels (yup::OffscreenTarget& target, void* dst, size_t dstSize) override { return real->readOffscreenPixels (target, dst, dstSize); } + + yup::GpuDevice::Ptr getGpuDevice() const noexcept { return real; } + +private: + yup::GpuDevice::Ptr real; + rive::ore::Context* injectedOreContext = nullptr; +}; + +// ============================================================================== +// Mock yup::RenderableTarget +// ============================================================================== + +class MockOffscreenTarget : public yup::RenderableTarget +{ +public: + explicit MockOffscreenTarget (int w, int h) + : width_ (w) + , height_ (h) + { + } + + int getWidth() const noexcept override { return width_; } + + int getHeight() const noexcept override { return height_; } + + rive::gpu::RenderTarget* getRenderTarget() noexcept override { return getRenderTargetProxy(); } + + rive::gpu::RenderContext* getRenderContext() noexcept override { return getRenderContextProxy(); } + + rive::rcp getRenderCanvas() noexcept override { return getRenderCanvasProxy(); } + + rive::rcp adoptAsTexture() override { return adoptAsTextureProxy(); } + + MOCK_METHOD (rive::gpu::RenderTarget*, getRenderTargetProxy, (), ()); + MOCK_METHOD (rive::gpu::RenderContext*, getRenderContextProxy, (), ()); + MOCK_METHOD (rive::rcp, getRenderCanvasProxy, (), ()); + MOCK_METHOD (rive::rcp, adoptAsTextureProxy, (), ()); + + /** Creates a MockOffscreenTarget pre-configured with a TestGpuTexture for adoptAsTexture. */ + static std::unique_ptr withGpuTexture (int w, int h) + { + auto t = std::make_unique<::testing::NiceMock> (w, h); + ON_CALL (*t, getRenderCanvasProxy()).WillByDefault (::testing::ReturnNull()); + ON_CALL (*t, adoptAsTextureProxy()) + .WillByDefault (::testing::Return (rive::make_rcp (w, h))); + return t; + } + +private: + int width_; + int height_; +}; + +// ============================================================================== +// Test helper: OreInjectedGpuDevice that also injects an offscreen target. +// Overrides createOffscreenTarget / createRenderableTarget to return a pre-built +// MockOffscreenTarget. +// ============================================================================== + +class OreAndTargetGpuDevice : public OreInjectedGpuDevice +{ +public: + OreAndTargetGpuDevice (rive::ore::Context* oreCtx, + std::unique_ptr target) + : OreInjectedGpuDevice (oreCtx) + , injectedTarget (std::move (target)) + { + } + + std::unique_ptr createOffscreenTarget (int, int) override + { + return std::move (injectedTarget); + } + + std::unique_ptr createRenderableTarget (int, int) override + { + return std::move (injectedTarget); + } + + void setNextOffscreenTarget (std::unique_ptr target) + { + injectedTarget = std::move (target); + } + +private: + std::unique_ptr injectedTarget; +}; diff --git a/tests/yup_animation/yup_Animation.cpp b/tests/yup_animation/yup_Animation.cpp index 4ceeb2f39..24aec442a 100644 --- a/tests/yup_animation/yup_Animation.cpp +++ b/tests/yup_animation/yup_Animation.cpp @@ -295,7 +295,7 @@ TEST_F (AnimationValidTests, GetCompositionReturnsNonNull) TEST (AnimationTests, RenderFrameOnInvalidAnimationDoesNotCrash) { Animation invalid; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); Image canvas (32, 32, PixelFormat::RGBA); @@ -307,7 +307,7 @@ TEST (AnimationTests, RenderFrameOnInvalidAnimationDoesNotCrash) TEST (AnimationTests, RenderAtTimeOnInvalidAnimationDoesNotCrash) { Animation invalid; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); Image canvas (32, 32, PixelFormat::RGBA); @@ -319,7 +319,7 @@ TEST (AnimationTests, RenderAtTimeOnInvalidAnimationDoesNotCrash) TEST (AnimationTests, RenderAtProgressOnInvalidAnimationDoesNotCrash) { Animation invalid; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); Image canvas (32, 32, PixelFormat::RGBA); @@ -337,7 +337,7 @@ class AnimationRenderTests : public ::testing::Test protected: void SetUp() override { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); anim = Animation::loadFromData (kAnimTestJson); diff --git a/tests/yup_animation/yup_AnimationFrameExporter.cpp b/tests/yup_animation/yup_AnimationFrameExporter.cpp index 2e333dddf..347ac02ad 100644 --- a/tests/yup_animation/yup_AnimationFrameExporter.cpp +++ b/tests/yup_animation/yup_AnimationFrameExporter.cpp @@ -77,7 +77,7 @@ class AnimationFrameExporterTests : public ::testing::Test protected: void SetUp() override { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); exporter = std::make_unique (*context); diff --git a/tests/yup_animation/yup_AnimationRenderResources.cpp b/tests/yup_animation/yup_AnimationRenderResources.cpp index 3ff7abebb..3b94622a7 100644 --- a/tests/yup_animation/yup_AnimationRenderResources.cpp +++ b/tests/yup_animation/yup_AnimationRenderResources.cpp @@ -64,7 +64,7 @@ TEST (AnimationRenderResourcesTests, DoubleResetDoesNotCrash) TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesWithZeroWidthReturnsInvalidLease) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -74,7 +74,7 @@ TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesWithZeroWidthReturnsInv TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesWithZeroHeightReturnsInvalidLease) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -84,7 +84,7 @@ TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesWithZeroHeightReturnsIn TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesWithNegativeDimensionsReturnsInvalidLease) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -94,7 +94,7 @@ TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesWithNegativeDimensionsR TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesHeadlessContextReturnsInvalidLease) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -106,10 +106,10 @@ TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesHeadlessContextReturnsI TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesBackendContextSwitchingResetsPool) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); - auto otherContext = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto otherContext = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (otherContext, nullptr); AnimationRenderResources resources; @@ -131,7 +131,7 @@ TEST (AnimationRenderResourcesTests, AcquireMatteCanvasesBackendContextSwitching TEST (AnimationRenderResourcesTests, GetPrecompCanvasWithZeroWidthReturnsNull) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -140,7 +140,7 @@ TEST (AnimationRenderResourcesTests, GetPrecompCanvasWithZeroWidthReturnsNull) TEST (AnimationRenderResourcesTests, GetPrecompCanvasWithZeroHeightReturnsNull) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -149,7 +149,7 @@ TEST (AnimationRenderResourcesTests, GetPrecompCanvasWithZeroHeightReturnsNull) TEST (AnimationRenderResourcesTests, GetPrecompCanvasHeadlessReturnsNull) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -158,7 +158,7 @@ TEST (AnimationRenderResourcesTests, GetPrecompCanvasHeadlessReturnsNull) TEST (AnimationRenderResourcesTests, GetPrecompCanvasSameKeyReusesSlot) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -171,7 +171,7 @@ TEST (AnimationRenderResourcesTests, GetPrecompCanvasSameKeyReusesSlot) TEST (AnimationRenderResourcesTests, GetPrecompCanvasDifferentKeysAreIndependent) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -184,10 +184,10 @@ TEST (AnimationRenderResourcesTests, GetPrecompCanvasDifferentKeysAreIndependent TEST (AnimationRenderResourcesTests, GetPrecompCanvasBackendContextSwitchingResetsPools) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); - auto otherContext = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto otherContext = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (otherContext, nullptr); AnimationRenderResources resources; @@ -205,7 +205,7 @@ TEST (AnimationRenderResourcesTests, GetPrecompCanvasBackendContextSwitchingRese TEST (AnimationRenderResourcesTests, ResetClearsPrecompCanvasCache) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -278,7 +278,7 @@ TEST (MatteCanvasLeaseTests, InvalidLeaseGetTargetCanvasTriggersAssert) TEST (AnimationRenderResourcesIntegrationTests, DestroyResourcesWhileLeaseIsValid) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); // On headless the lease is never valid, but the acquire path is exercised. @@ -292,7 +292,7 @@ TEST (AnimationRenderResourcesIntegrationTests, DestroyResourcesWhileLeaseIsVali TEST (AnimationRenderResourcesIntegrationTests, ResetWhileLeaseIsActiveAsserts) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; @@ -311,7 +311,7 @@ TEST (AnimationRenderResourcesIntegrationTests, ResetWhileLeaseIsActiveAsserts) TEST (AnimationRenderResourcesTests, FullApiSmokeTest) { - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); AnimationRenderResources resources; diff --git a/tests/yup_animation/yup_AnimationRenderer.cpp b/tests/yup_animation/yup_AnimationRenderer.cpp index 769899f09..4e7075527 100644 --- a/tests/yup_animation/yup_AnimationRenderer.cpp +++ b/tests/yup_animation/yup_AnimationRenderer.cpp @@ -1298,7 +1298,7 @@ class AnimationRendererTests : public ::testing::Test protected: void SetUp() override { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); } diff --git a/tests/yup_audio_gui/yup_AudioThumbnail.cpp b/tests/yup_audio_gui/yup_AudioThumbnail.cpp index 5a6fc9778..61bfbcd5a 100644 --- a/tests/yup_audio_gui/yup_AudioThumbnail.cpp +++ b/tests/yup_audio_gui/yup_AudioThumbnail.cpp @@ -29,7 +29,7 @@ using namespace yup; namespace yup { -extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GraphicsContext::Options); +extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GpuDevice::Options, yup::GpuDevice::Ptr); } // namespace yup namespace @@ -475,7 +475,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithValidData) auto buffer = createThumbnailTestBuffer (2, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); @@ -496,7 +496,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithInvalidChannelIndex) auto buffer = createThumbnailTestBuffer (2, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); @@ -517,7 +517,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithNegativeChannelIndex) auto buffer = createThumbnailTestBuffer (2, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); @@ -538,7 +538,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithEmptyRange) auto buffer = createThumbnailTestBuffer (1, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); @@ -553,7 +553,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithEmptyRange) TEST_F (AudioThumbnailTests, PaintChannelWithNoPeakProfile) { - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); @@ -574,7 +574,7 @@ TEST_F (AudioThumbnailTests, PaintChannelZoomedIn) auto buffer = createThumbnailTestBuffer (1, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 200); Graphics g (*context, *renderer); @@ -595,7 +595,7 @@ TEST_F (AudioThumbnailTests, PaintChannelZoomedOut) auto buffer = createThumbnailTestBuffer (1, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); @@ -616,7 +616,7 @@ TEST_F (AudioThumbnailTests, PaintChannelMultipleChannels) auto buffer = createThumbnailTestBuffer (8, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 800); Graphics g (*context, *renderer); @@ -640,7 +640,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithZeroPixelWidth) auto buffer = createThumbnailTestBuffer (1, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); @@ -661,7 +661,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithNegativePixelWidth) auto buffer = createThumbnailTestBuffer (1, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); @@ -682,7 +682,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithVeryLargePixelWidth) auto buffer = createThumbnailTestBuffer (1, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (10000, 200); Graphics g (*context, *renderer); @@ -703,7 +703,7 @@ TEST_F (AudioThumbnailTests, PaintChannelWithEmptyLane) auto buffer = createThumbnailTestBuffer (1, kThumbnailBufferSize); syncThumbnail.setSource (&buffer, kThumbnailSampleRate); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (400, 200); Graphics g (*context, *renderer); diff --git a/tests/yup_audio_gui/yup_CartesianPlane.cpp b/tests/yup_audio_gui/yup_CartesianPlane.cpp index 1afa7c81f..8a3f0b26a 100644 --- a/tests/yup_audio_gui/yup_CartesianPlane.cpp +++ b/tests/yup_audio_gui/yup_CartesianPlane.cpp @@ -27,7 +27,7 @@ using namespace yup; namespace yup { -extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GraphicsContext::Options); +extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GpuDevice::Options, yup::GpuDevice::Ptr); } // namespace yup class CartesianPlaneTests : public ::testing::Test @@ -757,7 +757,7 @@ TEST_F (CartesianPlaneTests, LogarithmicScaleForYAxis) TEST_F (CartesianPlaneTests, PaintWithoutCrashing) { - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 600); Graphics g (*context, *renderer); @@ -774,7 +774,7 @@ TEST_F (CartesianPlaneTests, PaintWithSignals) data.push_back ({ 1.0, 1.0 }); plane->updateSignalData (index, data); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 600); Graphics g (*context, *renderer); @@ -788,7 +788,7 @@ TEST_F (CartesianPlaneTests, PaintWithGridLines) plane->addVerticalGridLine (0.5); plane->addHorizontalGridLine (0.5); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 600); Graphics g (*context, *renderer); @@ -804,7 +804,7 @@ TEST_F (CartesianPlaneTests, PaintWithLabels) plane->addYAxisLabel (0.0, "0"); plane->addYAxisLabel (1.0, "1"); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 600); Graphics g (*context, *renderer); @@ -817,7 +817,7 @@ TEST_F (CartesianPlaneTests, PaintWithTitle) { plane->setTitle ("Test Plot"); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 600); Graphics g (*context, *renderer); @@ -832,7 +832,7 @@ TEST_F (CartesianPlaneTests, PaintWithLegend) plane->addSignal ("Signal 2", Colors::blue); plane->setLegendVisible (true); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 600); Graphics g (*context, *renderer); @@ -845,7 +845,7 @@ TEST_F (CartesianPlaneTests, PaintWithZeroSize) { plane->setBounds (0.0f, 0.0f, 0.0f, 0.0f); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (1, 1); Graphics g (*context, *renderer); diff --git a/tests/yup_audio_gui/yup_KMeterComponent.cpp b/tests/yup_audio_gui/yup_KMeterComponent.cpp index 67659faf1..cf298ca1c 100644 --- a/tests/yup_audio_gui/yup_KMeterComponent.cpp +++ b/tests/yup_audio_gui/yup_KMeterComponent.cpp @@ -27,7 +27,7 @@ using namespace yup; namespace yup { -extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GraphicsContext::Options); +extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GpuDevice::Options, yup::GpuDevice::Ptr); } // namespace yup //============================================================================== @@ -69,7 +69,7 @@ TEST (KMeterComponentTests, PaintWithThemeDoesNotCrash) KMeterComponent meter (meterState, 0); meter.setBounds (0.0f, 0.0f, 60.0f, 240.0f); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (120, 240); Graphics g (*context, *renderer); @@ -112,7 +112,7 @@ TEST (KMeterComponentTests, PaintLinearScaleDoesNotCrash) meter.setScaleMapping (KMeterComponent::ScaleMapping::linear); meter.setBounds (0.0f, 0.0f, 30.0f, 120.0f); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (30, 120); Graphics g (*context, *renderer); @@ -127,7 +127,7 @@ TEST (KMeterComponentTests, PaintWithShowPeakFalseDoesNotCrash) meter.setShowPeakHold (false); meter.setBounds (0.0f, 0.0f, 30.0f, 120.0f); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (30, 120); Graphics g (*context, *renderer); diff --git a/tests/yup_audio_gui/yup_SpectrogramComponent.cpp b/tests/yup_audio_gui/yup_SpectrogramComponent.cpp index 4508b235b..d882cca2a 100644 --- a/tests/yup_audio_gui/yup_SpectrogramComponent.cpp +++ b/tests/yup_audio_gui/yup_SpectrogramComponent.cpp @@ -30,7 +30,7 @@ using namespace yup; namespace yup { -extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GraphicsContext::Options); +extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GpuDevice::Options, yup::GpuDevice::Ptr); } // namespace yup namespace @@ -325,7 +325,7 @@ TEST_F (SpectrogramComponentTests, ClearHistoryDoesNotChangeConfiguration) TEST_F (SpectrogramComponentTests, PaintWithoutAudioDataDoesNotCrash) { - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 400); Graphics g (*context, *renderer); @@ -340,7 +340,7 @@ TEST_F (SpectrogramComponentTests, PaintAfterTimerCallbackDoesNotCrash) state->pushSamples (testData.data(), static_cast (testData.size())); spectrogram->timerCallback(); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 400); Graphics g (*context, *renderer); @@ -380,7 +380,7 @@ TEST_F (SpectrogramComponentTests, CompleteWorkflow) state->pushSamples (testData.data(), static_cast (testData.size())); spectrogram->timerCallback(); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 400); Graphics g (*context, *renderer); diff --git a/tests/yup_audio_gui/yup_SpectrumAnalyzerComponent.cpp b/tests/yup_audio_gui/yup_SpectrumAnalyzerComponent.cpp index 7bf80d369..e3e7d5af2 100644 --- a/tests/yup_audio_gui/yup_SpectrumAnalyzerComponent.cpp +++ b/tests/yup_audio_gui/yup_SpectrumAnalyzerComponent.cpp @@ -27,7 +27,7 @@ using namespace yup; namespace yup { -extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GraphicsContext::Options); +extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GpuDevice::Options, yup::GpuDevice::Ptr); } // namespace yup class SpectrumAnalyzerComponentTests : public ::testing::Test @@ -611,7 +611,7 @@ TEST_F (SpectrumAnalyzerComponentTests, MultipleTimerCallbacks) TEST_F (SpectrumAnalyzerComponentTests, PaintWithoutCrashing) { - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 400); Graphics g (*context, *renderer); @@ -624,7 +624,7 @@ TEST_F (SpectrumAnalyzerComponentTests, PaintWithLinesDisplayType) { analyzer->setDisplayType (SpectrumAnalyzerComponent::DisplayType::lines); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 400); Graphics g (*context, *renderer); @@ -637,7 +637,7 @@ TEST_F (SpectrumAnalyzerComponentTests, PaintWithFilledDisplayType) { analyzer->setDisplayType (SpectrumAnalyzerComponent::DisplayType::filled); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 400); Graphics g (*context, *renderer); @@ -656,7 +656,7 @@ TEST_F (SpectrumAnalyzerComponentTests, PaintWithAudioData) state->pushSamples (testData.data(), 2048); analyzer->timerCallback(); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 400); Graphics g (*context, *renderer); @@ -669,7 +669,7 @@ TEST_F (SpectrumAnalyzerComponentTests, PaintWithZeroSize) { analyzer->setBounds (0.0f, 0.0f, 0.0f, 0.0f); - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (1, 1); Graphics g (*context, *renderer); @@ -732,7 +732,7 @@ TEST_F (SpectrumAnalyzerComponentTests, CompleteWorkflow) analyzer->timerCallback(); // Render - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 400); Graphics g (*context, *renderer); diff --git a/tests/yup_graphics.cpp b/tests/yup_graphics.cpp index 68edef732..97a73625b 100644 --- a/tests/yup_graphics.cpp +++ b/tests/yup_graphics.cpp @@ -32,9 +32,6 @@ #include "yup_graphics/yup_Graphics.cpp" #include "yup_graphics/yup_GraphicsOffscreen.cpp" #include "yup_graphics/yup_GpuCanvas.cpp" -#include "yup_graphics/yup_GpuTarget.cpp" -#include "yup_graphics/yup_GpuPipeline.cpp" -#include "yup_graphics/yup_GpuPipelineMocked.cpp" #include "yup_graphics/yup_Image.cpp" #include "yup_graphics/yup_ImageFormatManager.cpp" #include "yup_graphics/yup_ImageFormatReader.cpp" diff --git a/tests/yup_graphics/yup_Drawable.cpp b/tests/yup_graphics/yup_Drawable.cpp index f019a5351..a562d4427 100644 --- a/tests/yup_graphics/yup_Drawable.cpp +++ b/tests/yup_graphics/yup_Drawable.cpp @@ -166,7 +166,7 @@ TEST (DrawableTests, ParseSVGFromString) EXPECT_EQ (20.0f, drawable.getBounds().getWidth()); EXPECT_EQ (10.0f, drawable.getBounds().getHeight()); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (32, 32); Graphics graphics (*context, *renderer); @@ -193,7 +193,7 @@ TEST (DrawableTests, PaintSVGWithClipPathUseElement) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -217,7 +217,7 @@ TEST (DrawableTests, PaintSVGWithNestedClipPath) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -243,7 +243,7 @@ TEST (DrawableTests, PaintSVGWithRadialGradientFocalPoint) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -269,7 +269,7 @@ TEST (DrawableTests, PaintSVGWithUserSpaceOnUseGradient) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -289,7 +289,7 @@ TEST (DrawableTests, PaintSVGWithStrokeWidthUnits) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -323,7 +323,7 @@ TEST (DrawableTests, PaintClipPathSVGFromFilePaints) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (128, 128); Graphics graphics (*context, *renderer); @@ -451,7 +451,7 @@ TEST (DrawableTests, PaintEmptyDrawableDoesNotCrash) { Drawable drawable; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 100); Graphics graphics (*context, *renderer); @@ -463,7 +463,7 @@ TEST (DrawableTests, PaintWithFittingDoesNotCrash) { Drawable drawable; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 100); Graphics graphics (*context, *renderer); @@ -477,7 +477,7 @@ TEST (DrawableTests, PaintWithVariousFittingModes) { Drawable drawable; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 100); Graphics graphics (*context, *renderer); @@ -508,7 +508,7 @@ TEST (DrawableTests, PaintWithVariousJustifications) { Drawable drawable; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 100); Graphics graphics (*context, *renderer); @@ -537,7 +537,7 @@ TEST (DrawableTests, PaintWithEmptyTargetArea) { Drawable drawable; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 100); Graphics graphics (*context, *renderer); @@ -551,7 +551,7 @@ TEST (DrawableTests, PaintWithNegativeArea) { Drawable drawable; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 100); Graphics graphics (*context, *renderer); @@ -755,7 +755,7 @@ TEST (DrawableTests, ParseSVGWithDefsXLinkUseAndTspanFlow) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (80, 40); Graphics graphics (*context, *renderer); @@ -794,7 +794,7 @@ TEST (DrawableTests, ParseSVGWithTransformOrigin) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (360, 360); Graphics graphics (*context, *renderer); @@ -1007,7 +1007,7 @@ TEST (DrawableTests, PaintSVGWithTransformedClipPath) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1051,7 +1051,7 @@ TEST (DrawableTests, PaintSVGWithScimitarClipPathAndGradientStroke) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (96, 96); Graphics graphics (*context, *renderer); graphics.setDrawingArea (Rectangle (23.0f, 17.0f, 96.0f, 96.0f)); @@ -1078,7 +1078,7 @@ TEST (DrawableTests, PaintSVGWithTransformedRadialGradient) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1107,7 +1107,7 @@ TEST (DrawableTests, PaintSVGWithReflectedRadialGradient) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (96, 96); Graphics graphics (*context, *renderer); @@ -1265,7 +1265,7 @@ TEST (DrawableTests, PaintSVGWithMask) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1290,7 +1290,7 @@ TEST (DrawableTests, PaintSVGWithMarkerEnd) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1315,7 +1315,7 @@ TEST (DrawableTests, PaintSVGWithPattern) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1340,7 +1340,7 @@ TEST (DrawableTests, PaintSVGWithCyclicUseDoesNotCrash) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1361,7 +1361,7 @@ TEST (DrawableTests, PaintSVGWithStrokeMiterLimit) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1387,7 +1387,7 @@ TEST (DrawableTests, PaintSVGWithClipRuleEvenOdd) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1408,7 +1408,7 @@ TEST (DrawableTests, PaintSVGWithMixBlendMode) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1431,7 +1431,7 @@ TEST (DrawableTests, PaintSVGWithFEBlendMultiply) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1461,7 +1461,7 @@ TEST (DrawableTests, PaintSVGWithFEBlendAllModes) bool result = drawable.parseSVG (svg); EXPECT_TRUE (result) << mode; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1488,7 +1488,7 @@ TEST (DrawableTests, PaintSVGWithFilterChainBlurThenBlend) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1512,7 +1512,7 @@ TEST (DrawableTests, PaintNoRectOverloadWithActualSVGContent) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1532,7 +1532,7 @@ TEST (DrawableTests, PaintNoRectOverloadWithCircleAndStroke) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 100); Graphics graphics (*context, *renderer); @@ -1556,7 +1556,7 @@ TEST (DrawableTests, PaintFittedScaleToFitWithActualContent) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1576,7 +1576,7 @@ TEST (DrawableTests, PaintFittedScaleToFillWithActualContent) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1596,7 +1596,7 @@ TEST (DrawableTests, PaintFittedFillWithActualContent) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1616,7 +1616,7 @@ TEST (DrawableTests, PaintFittedFitWidthAndFitHeightWithActualContent) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1637,7 +1637,7 @@ TEST (DrawableTests, PaintFittedCenterCropAndCenterInsideWithActualContent) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1663,7 +1663,7 @@ TEST (DrawableTests, PaintSVGWithDashedStrokeOnPath) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1684,7 +1684,7 @@ TEST (DrawableTests, PaintSVGWithDashedStrokeAndOffset) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1708,7 +1708,7 @@ TEST (DrawableTests, PaintSVGWithInheritedDashArrayOnGroup) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1738,7 +1738,7 @@ TEST (DrawableTests, PaintSVGWithStrokeLinecapVariants) bool result = drawable.parseSVG (svg); EXPECT_TRUE (result) << cap; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1765,7 +1765,7 @@ TEST (DrawableTests, PaintSVGWithStrokeLinejoinVariants) bool result = drawable.parseSVG (svg); EXPECT_TRUE (result) << join; - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1800,7 +1800,7 @@ TEST (DrawableTests, PaintSVGWithMarkerStartMidEnd) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (96, 96); Graphics graphics (*context, *renderer); @@ -1826,7 +1826,7 @@ TEST (DrawableTests, PaintSVGWithMarkerOrientAutoStartReverse) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1859,7 +1859,7 @@ TEST (DrawableTests, PaintSVGWithImageElementViaResolver) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1886,7 +1886,7 @@ TEST (DrawableTests, PaintSVGWithImageElementResolverReturnsNullopt) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -1910,7 +1910,7 @@ TEST (DrawableTests, PaintSVGWithTextAnchorMiddle) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 50); Graphics graphics (*context, *renderer); @@ -1930,7 +1930,7 @@ TEST (DrawableTests, PaintSVGWithTextAnchorEnd) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 50); Graphics graphics (*context, *renderer); @@ -1953,7 +1953,7 @@ TEST (DrawableTests, PaintSVGWithTextDxDyAttributes) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (120, 60); Graphics graphics (*context, *renderer); @@ -1980,7 +1980,7 @@ TEST (DrawableTests, PaintSVGWithFeGaussianBlurAlone) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -2005,7 +2005,7 @@ TEST (DrawableTests, PaintSVGWithFillRuleEvenOddOnPath) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -2059,7 +2059,7 @@ TEST (DrawableTests, PaintSVGWithNestedSvgViewport) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (100, 100); Graphics graphics (*context, *renderer); @@ -2086,7 +2086,7 @@ TEST (DrawableTests, PaintSVGWithDashArrayNoneOverridesParent) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -2112,7 +2112,7 @@ TEST (DrawableTests, PaintSVGWithFillOpacityAndStrokeOpacity) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -2136,7 +2136,7 @@ TEST (DrawableTests, PaintSVGWithPreserveAspectRatioNone) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); @@ -2156,7 +2156,7 @@ TEST (DrawableTests, PaintSVGWithPreserveAspectRatioXMidYMidMeet) EXPECT_TRUE (result); - auto context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto context = GraphicsContext::createContext (GpuPlatform::Headless, {}); auto renderer = context->makeRenderer (64, 64); Graphics graphics (*context, *renderer); diff --git a/tests/yup_graphics/yup_GpuCanvas.cpp b/tests/yup_graphics/yup_GpuCanvas.cpp index d64d13994..cc7bc7dba 100644 --- a/tests/yup_graphics/yup_GpuCanvas.cpp +++ b/tests/yup_graphics/yup_GpuCanvas.cpp @@ -30,7 +30,7 @@ class GpuCanvasTests : public ::testing::Test protected: void SetUp() override { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); } @@ -143,7 +143,7 @@ TEST (TextureTests, ValidTextureReportsCorrectlyViaGpuCanvas) TEST (GraphicsDrawTextureTests, NullTextureIsNoOp) { - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto renderer = ctx->makeRenderer (128, 128); @@ -161,7 +161,7 @@ TEST (GraphicsDrawTextureTests, NullTextureIsNoOp) TEST_F (GpuCanvasTests, BeginRenderPassWithNullCanvasReturnsInvalidPass) { - auto frame = GpuFrame::begin (*context); + auto frame = GpuFrame::begin (context->getGpuDevice()); if (! frame.isValid()) return; diff --git a/tests/yup_graphics/yup_Graphics.cpp b/tests/yup_graphics/yup_Graphics.cpp index 702e0ff64..e3e6be2df 100644 --- a/tests/yup_graphics/yup_Graphics.cpp +++ b/tests/yup_graphics/yup_Graphics.cpp @@ -32,7 +32,7 @@ class GraphicsTest : public ::testing::Test protected: void SetUp() override { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); renderer = context->makeRenderer (200, 200); ASSERT_NE (renderer, nullptr); @@ -1055,7 +1055,7 @@ TEST_F (GraphicsTest, CommitOffscreenTarget_FailsOnNonOffscreenGraphics) TEST_F (GraphicsTest, GetGraphicsContext_ReturnsValidContext) { auto& ctx = graphics->getGraphicsContext(); - EXPECT_EQ (ctx.getApi(), GraphicsContext::Headless); + EXPECT_EQ (ctx.getPlatform(), GpuPlatform::Headless); } TEST_F (GraphicsTest, SetMiterLimit_DoesNotCrash) diff --git a/tests/yup_graphics/yup_GraphicsOffscreen.cpp b/tests/yup_graphics/yup_GraphicsOffscreen.cpp index 024b27c6e..d70870ee5 100644 --- a/tests/yup_graphics/yup_GraphicsOffscreen.cpp +++ b/tests/yup_graphics/yup_GraphicsOffscreen.cpp @@ -52,202 +52,4 @@ class TrackingOffscreenTarget : public RenderableTarget int height; }; -class TrackingGraphicsContext : public GraphicsContext -{ -public: - TrackingGraphicsContext() - : realContext (GraphicsContext::createContext (GraphicsContext::Headless, {})) - { - } - - Api getApi() const noexcept override { return realContext->getApi(); } - - rive::Factory* factory() override { return realContext->factory(); } - - rive::gpu::RenderContext* renderContext() override { return realContext->renderContext(); } - - rive::gpu::RenderTarget* renderTarget() override { return realContext->renderTarget(); } - - std::unique_ptr makeRenderer (int width, int height) override { return realContext->makeRenderer (width, height); } - - void onSizeChanged (void* nativeHandle, int width, int height, float dpiScale, uint32_t sampleCount) override - { - realContext->onSizeChanged (nativeHandle, width, height, dpiScale, sampleCount); - } - - void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override { realContext->begin (frameDesc); } - - void end (void* nativeHandle) override { realContext->end (nativeHandle); } - - std::unique_ptr createOffscreenTarget (int, int) override { return nullptr; } - - std::unique_ptr createRenderableTarget (int, int) override { return nullptr; } - - void beginOffscreen (OffscreenTarget&, const rive::gpu::RenderContext::FrameDescriptor&) override { ++beginOffscreenCalls; } - - void endOffscreen (OffscreenTarget&) override { ++endOffscreenCalls; } - - bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override { return false; } - - int beginOffscreenCalls = 0; - int endOffscreenCalls = 0; - -private: - std::unique_ptr realContext; -}; - } // namespace - -class GraphicsOffscreenTests : public ::testing::Test -{ -protected: - void SetUp() override - { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); - ASSERT_NE (context, nullptr); - } - - std::unique_ptr context; -}; - -TEST_F (GraphicsOffscreenTests, RegularConstructorIsNotOffscreen) -{ - auto renderer = context->makeRenderer (100, 100); - Graphics g (*context, *renderer); - - EXPECT_FALSE (g.isOffscreen()); -} - -TEST_F (GraphicsOffscreenTests, OffscreenConstructorIsOffscreen) -{ - // Headless context returns nullptr from createOffscreenTarget, so - // isOffscreen() will return false. This tests the headless stub path. - Image image (64, 64); - Graphics g (*context, image); - - // Headless context has no GPU, so createOffscreenTarget returns nullptr. - EXPECT_FALSE (g.isOffscreen()); -} - -TEST_F (GraphicsOffscreenTests, CommitToImageReturnsFalseForRegularGraphics) -{ - auto renderer = context->makeRenderer (100, 100); - Graphics g (*context, *renderer); - - EXPECT_FALSE (g.commitToImage()); -} - -TEST_F (GraphicsOffscreenTests, CommitToImageReturnsFalseForHeadlessOffscreen) -{ - // Headless context creates no offscreen target, so commit returns false. - Image image (64, 64); - Graphics g (*context, image); - - EXPECT_FALSE (g.commitToImage()); -} - -TEST_F (GraphicsOffscreenTests, ReadPixelsToImageReturnsFalseForRegularGraphics) -{ - auto renderer = context->makeRenderer (100, 100); - Graphics g (*context, *renderer); - - EXPECT_FALSE (g.readPixelsToImage()); -} - -TEST_F (GraphicsOffscreenTests, ReadPixelsToImageReturnsFalseForHeadlessOffscreen) -{ - // Headless context has no GPU, so readPixels returns false. - Image image (64, 64); - Graphics g (*context, image); - - EXPECT_FALSE (g.readPixelsToImage()); -} - -TEST_F (GraphicsOffscreenTests, SetGpuTextureStoresTexture) -{ - Image image (32, 32); - EXPECT_EQ (image.getGpuTexture(), nullptr); - - image.setGpuTexture (nullptr); - EXPECT_EQ (image.getGpuTexture(), nullptr); -} - -TEST_F (GraphicsOffscreenTests, GetGpuTextureReturnsNullByDefault) -{ - Image image (32, 32); - EXPECT_EQ (image.getGpuTexture(), nullptr); -} - -TEST_F (GraphicsOffscreenTests, SmallImageOffscreenConstructorDoesNotCrash) -{ - Image image (1, 1); - EXPECT_NO_THROW ({ Graphics g (*context, image); }); -} - -TEST_F (GraphicsOffscreenTests, LargeImageOffscreenConstructorDoesNotCrash) -{ - Image image (2048, 2048); - EXPECT_NO_THROW ({ Graphics g (*context, image); }); -} - -TEST_F (GraphicsOffscreenTests, CommitCalledTwiceReturnsFalseOnSecondCall) -{ - Image image (64, 64); - Graphics g (*context, image); - - const bool first = g.commitToImage(); - const bool second = g.commitToImage(); - - // Both should return false in headless (no GPU), but the contract is that - // the second call is also not a crash. - EXPECT_FALSE (second); - (void) first; -} - -TEST_F (GraphicsOffscreenTests, ReadPixelsAfterCommitDoesNotCrash) -{ - Image image (32, 32); - Graphics g (*context, image); - - g.commitToImage(); - EXPECT_NO_THROW ({ g.readPixelsToImage(); }); -} - -TEST_F (GraphicsOffscreenTests, RegularGraphicsContextRenderSize) -{ - auto renderer = context->makeRenderer (128, 64); - ASSERT_NE (renderer, nullptr); - Graphics g (*context, *renderer); - - EXPECT_FALSE (g.isOffscreen()); - EXPECT_FALSE (g.commitToImage()); -} - -TEST (GraphicsOffscreenLifecycleTests, DestroyingUncommittedGraphicsClosesFrame) -{ - TrackingGraphicsContext context; - TrackingOffscreenTarget target (64, 64); - - { - Graphics g (context, target); - EXPECT_TRUE (g.isOffscreen()); - EXPECT_EQ (1, context.beginOffscreenCalls); - EXPECT_EQ (0, context.endOffscreenCalls); - } - - EXPECT_EQ (1, context.endOffscreenCalls); -} - -TEST (GraphicsOffscreenLifecycleTests, CommittingGraphicsDoesNotCloseFrameTwice) -{ - TrackingGraphicsContext context; - TrackingOffscreenTarget target (64, 64); - - { - Graphics g (context, target); - EXPECT_TRUE (g.commitOffscreenTarget()); - EXPECT_EQ (1, context.endOffscreenCalls); - } - - EXPECT_EQ (1, context.endOffscreenCalls); -} diff --git a/tests/yup_graphics/yup_Image.cpp b/tests/yup_graphics/yup_Image.cpp index fcce68b15..9ba1e3939 100644 --- a/tests/yup_graphics/yup_Image.cpp +++ b/tests/yup_graphics/yup_Image.cpp @@ -947,7 +947,7 @@ TEST (ImageTests, SetGpuTextureWithNullDoesNotCrash) TEST (ImageTests, CreateTextureIfNotPresentOnHeadlessReturnsFalse) { - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); Image image (8, 8, PixelFormat::RGBA); @@ -958,7 +958,7 @@ TEST (ImageTests, CreateTextureIfNotPresentOnHeadlessReturnsFalse) TEST (ImageTests, CreateTextureIfNotPresentOnDefaultImageReturnsFalse) { - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); Image image; diff --git a/tests/yup_graphics/yup_SVGParser.cpp b/tests/yup_graphics/yup_SVGParser.cpp index 5250ba471..7ed282acb 100644 --- a/tests/yup_graphics/yup_SVGParser.cpp +++ b/tests/yup_graphics/yup_SVGParser.cpp @@ -38,7 +38,7 @@ auto makeHeadlessGraphics (int w = 64, int h = 64) }; HeadlessGfx g; - g.ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + g.ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); g.renderer = g.ctx->makeRenderer (w, h); g.graphics = std::make_unique (*g.ctx, *g.renderer); return g; diff --git a/tests/yup_gui/yup_ComponentEffect.cpp b/tests/yup_gui/yup_ComponentEffect.cpp index a40f261d4..1ecf0e250 100644 --- a/tests/yup_gui/yup_ComponentEffect.cpp +++ b/tests/yup_gui/yup_ComponentEffect.cpp @@ -145,7 +145,7 @@ TEST_F (ComponentEffectTest, ApplyIsCalled) auto effect = ReferenceCountedObjectPtr (new CountingEffect()); EXPECT_EQ (effect->applyCount, 0); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto renderer = ctx->makeRenderer (200, 200); ASSERT_NE (renderer, nullptr); @@ -238,7 +238,7 @@ TEST_F (ComponentEffectTest, ClearsCachedCanvasWhenEnabled) { Component comp ("test"); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto canvas = GpuCanvas::create (*ctx, 64, 64); if (canvas == nullptr) @@ -259,7 +259,7 @@ TEST_F (ComponentEffectTest, RepaintInvalidatesCache) { Component comp ("test"); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto canvas = GpuCanvas::create (*ctx, 64, 64); if (canvas == nullptr) @@ -276,7 +276,7 @@ TEST_F (ComponentEffectTest, RepaintWithRectInvalidatesCache) { Component comp ("test"); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto canvas = GpuCanvas::create (*ctx, 64, 64); if (canvas == nullptr) @@ -294,7 +294,7 @@ TEST_F (ComponentEffectTest, SetBoundsInvalidatesCache) Component comp ("test"); comp.setBounds (0, 0, 200, 200); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto canvas = GpuCanvas::create (*ctx, 64, 64); if (canvas == nullptr) @@ -388,7 +388,7 @@ TEST_F (ComponentEffectTest, ChildrenArePainted) parentMock.addAndMakeVisible (child1); parentMock.addAndMakeVisible (child2); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto renderer = ctx->makeRenderer (200, 200); ASSERT_NE (renderer, nullptr); @@ -406,7 +406,7 @@ TEST_F (ComponentEffectTest, PaintOverChildrenIsCalled) parentMock.setBounds (0, 0, 400, 300); parentMock.setVisible (true); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto renderer = ctx->makeRenderer (200, 200); ASSERT_NE (renderer, nullptr); @@ -427,7 +427,7 @@ TEST_F (ComponentEffectTest, ZeroSizedReturnsEmpty) { Component comp ("test"); // Default 0×0 size - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); EXPECT_FALSE (comp.snapshotToImage (*ctx).isValid()); } @@ -437,7 +437,7 @@ TEST_F (ComponentEffectTest, HeadlessContextReturnsEmpty) Component comp ("test"); comp.setBounds (0, 0, 200, 200); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); EXPECT_FALSE (comp.snapshotToImage (*ctx).isValid()); } @@ -447,7 +447,7 @@ TEST_F (ComponentEffectTest, IncludeEffectsFlagDoesNotCrashWithHeadless) Component comp ("test"); comp.setBounds (0, 0, 200, 200); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); // Both calls should return empty without crashing @@ -470,7 +470,7 @@ TEST_F (ComponentEffectTest, ChildrenStillPaintWhenParentIsCached) parentMock.setCachedToTexture (true); parentMock.addAndMakeVisible (child); - auto ctx = GraphicsContext::createContext (GraphicsContext::Headless, {}); + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}); ASSERT_NE (ctx, nullptr); auto renderer = ctx->makeRenderer (200, 200); ASSERT_NE (renderer, nullptr); @@ -526,7 +526,7 @@ class ComponentEffectGpuTest : public ::testing::Test static void SetUpTestSuite() { - gpuContext = GraphicsContext::createContext (GraphicsContext::Metal, {}); + gpuContext = GraphicsContext::createContext (GpuPlatform::Metal, {}); if (gpuContext == nullptr) return; diff --git a/tests/yup_gui/yup_PaintProfiler.cpp b/tests/yup_gui/yup_PaintProfiler.cpp index a1f880971..817b99da8 100644 --- a/tests/yup_gui/yup_PaintProfiler.cpp +++ b/tests/yup_gui/yup_PaintProfiler.cpp @@ -647,7 +647,7 @@ class ComponentPaintProfilingFixture : public ::testing::Test { GraphicsContext::Options opts; opts.allowHeadlessRendering = true; - context = GraphicsContext::createContext (GraphicsContext::Api::Headless, opts); + context = GraphicsContext::createContext (GpuPlatform::Headless, opts); renderer = context->makeRenderer (200, 200); profiler = &PaintProfiler::getInstance(); diff --git a/tests/yup_gui/yup_ThemeVersion1.cpp b/tests/yup_gui/yup_ThemeVersion1.cpp index fb7baad45..ae69887ce 100644 --- a/tests/yup_gui/yup_ThemeVersion1.cpp +++ b/tests/yup_gui/yup_ThemeVersion1.cpp @@ -27,7 +27,7 @@ using namespace yup; namespace yup { -extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GraphicsContext::Options); +extern std::unique_ptr yup_constructHeadlessGraphicsContext (yup::GpuDevice::Options, yup::GpuDevice::Ptr); } // namespace yup TEST (ThemeVersion1Tests, CreateReturnsNonNullTheme) @@ -80,7 +80,7 @@ TEST (ThemeVersion1Tests, ScrollBarColorsAreRegistered) TEST (ThemeVersion1Tests, PaintsCoreComponents) { - auto context = yup_constructHeadlessGraphicsContext ({}); + auto context = yup_constructHeadlessGraphicsContext ({}, {}); auto renderer = context->makeRenderer (800, 600); Graphics g (*context, *renderer); diff --git a/tests/yup_rhi.cpp b/tests/yup_rhi.cpp new file mode 100644 index 000000000..f45c0d407 --- /dev/null +++ b/tests/yup_rhi.cpp @@ -0,0 +1,29 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "mocks/rive_gpu.h" +#include "mocks/rive_ore.h" +#include "mocks/yup_rhi.h" +#include "mocks/yup_graphics.h" + +#include "yup_rhi/yup_GpuTarget.cpp" +#include "yup_rhi/yup_GpuPipeline.cpp" +#include "yup_rhi/yup_GpuPipelineMocked.cpp" diff --git a/tests/yup_graphics/yup_GpuPipeline.cpp b/tests/yup_rhi/yup_GpuPipeline.cpp similarity index 92% rename from tests/yup_graphics/yup_GpuPipeline.cpp rename to tests/yup_rhi/yup_GpuPipeline.cpp index 97df6e7ee..202abe63a 100644 --- a/tests/yup_graphics/yup_GpuPipeline.cpp +++ b/tests/yup_rhi/yup_GpuPipeline.cpp @@ -21,7 +21,7 @@ #include -#include +#include using namespace yup; @@ -46,11 +46,11 @@ class GpuPipelineTests : public ::testing::Test protected: void SetUp() override { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + context = GpuDevice::create (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); } - std::unique_ptr context; + GpuDevice::Ptr context; }; // --------------------------------------------------------------------------- @@ -195,11 +195,11 @@ class GpuPipelineCacheTests : public ::testing::Test protected: void SetUp() override { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + context = GpuDevice::create (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); } - std::unique_ptr context; + GpuDevice::Ptr context; }; TEST_F (GpuPipelineCacheTests, SameInputsProduceSameKey) @@ -207,8 +207,8 @@ TEST_F (GpuPipelineCacheTests, SameInputsProduceSameKey) ShaderBundle bundle; GpuPipelineOptions options; - const auto key1 = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::Metal); - const auto key2 = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::Metal); + const auto key1 = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::Metal); + const auto key2 = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::Metal); EXPECT_EQ (key1, key2); } @@ -217,8 +217,8 @@ TEST_F (GpuPipelineCacheTests, DifferentApiProducesDifferentKey) ShaderBundle bundle; GpuPipelineOptions options; - const auto keyMetal = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::Metal); - const auto keyD3D = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::Direct3D); + const auto keyMetal = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::Metal); + const auto keyD3D = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::Direct3D); EXPECT_NE (keyMetal, keyD3D); } @@ -230,8 +230,8 @@ TEST_F (GpuPipelineCacheTests, DifferentOptionsProduceDifferentKey) GpuPipelineOptions b; b.cullMode = GpuCullMode::back; - const auto keyA = GpuPipelineCache::generateCacheKey (bundle, a, GraphicsContext::Metal); - const auto keyB = GpuPipelineCache::generateCacheKey (bundle, b, GraphicsContext::Metal); + const auto keyA = GpuPipelineCache::generateCacheKey (bundle, a, GpuPlatform::Metal); + const auto keyB = GpuPipelineCache::generateCacheKey (bundle, b, GpuPlatform::Metal); EXPECT_NE (keyA, keyB); } @@ -286,7 +286,7 @@ TEST_F (GpuPipelineCacheTests, EmptyBundleGeneratesKeyWithNoneMarkers) ShaderBundle bundle; GpuPipelineOptions options; - const auto key = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::Metal); + const auto key = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::Metal); EXPECT_FALSE (key.isEmpty()); } @@ -295,8 +295,8 @@ TEST_F (GpuPipelineCacheTests, OpenGLES_ApisGenerateDifferentKeys) ShaderBundle bundle; GpuPipelineOptions options; - const auto keyGL = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::OpenGL); - const auto keyGLES = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::OpenGLES); + const auto keyGL = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::OpenGL); + const auto keyGLES = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::OpenGLES); EXPECT_NE (keyGL, keyGLES); } @@ -305,7 +305,7 @@ TEST_F (GpuPipelineCacheTests, WebGPU_GeneratesKey) ShaderBundle bundle; GpuPipelineOptions options; - const auto key = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::WebGPU); + const auto key = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::WebGPU); EXPECT_FALSE (key.isEmpty()); } @@ -330,7 +330,7 @@ TEST_F (GpuPipelineCacheTests, ESSL_FallbackUsesGlslWhenEsslMissing) GpuPipelineOptions options; // With ESSL API, the cache should fall back to GLSL since no ESSL variant exists. - const auto keyESSL = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::OpenGLES); + const auto keyESSL = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::OpenGLES); EXPECT_FALSE (keyESSL.isEmpty()); } @@ -361,7 +361,7 @@ TEST_F (GpuPipelineCacheTests, ESSL_FallbackUsesEsslWhenAvailable) GpuPipelineOptions options; - const auto key = GpuPipelineCache::generateCacheKey (bundle, options, GraphicsContext::OpenGLES); + const auto key = GpuPipelineCache::generateCacheKey (bundle, options, GpuPlatform::OpenGLES); EXPECT_FALSE (key.isEmpty()); } @@ -385,8 +385,8 @@ TEST_F (GpuPipelineCacheTests, KeyChangesWhenShaderSourceDiffers) GpuPipelineOptions options; - const auto keyA = GpuPipelineCache::generateCacheKey (bundleA, options, GraphicsContext::OpenGL); - const auto keyB = GpuPipelineCache::generateCacheKey (bundleB, options, GraphicsContext::OpenGL); + const auto keyA = GpuPipelineCache::generateCacheKey (bundleA, options, GpuPlatform::OpenGL); + const auto keyB = GpuPipelineCache::generateCacheKey (bundleB, options, GpuPlatform::OpenGL); EXPECT_NE (keyA, keyB); } @@ -410,8 +410,8 @@ TEST_F (GpuPipelineCacheTests, KeyChangesWhenEntryPointDiffers) GpuPipelineOptions options; - const auto keyA = GpuPipelineCache::generateCacheKey (bundleA, options, GraphicsContext::OpenGL); - const auto keyB = GpuPipelineCache::generateCacheKey (bundleB, options, GraphicsContext::OpenGL); + const auto keyA = GpuPipelineCache::generateCacheKey (bundleA, options, GpuPlatform::OpenGL); + const auto keyB = GpuPipelineCache::generateCacheKey (bundleB, options, GpuPlatform::OpenGL); EXPECT_NE (keyA, keyB); } @@ -467,7 +467,7 @@ TEST_F (GpuPipelineCacheTests, GetOrCompileWithExplicitKeyStoresCompiledPipeline ShaderBundle bundle; GpuPipelineOptions options; - const auto key = GpuPipelineCache::generateCacheKey (bundle, options, context->getApi()); + const auto key = GpuPipelineCache::generateCacheKey (bundle, options, context->getPlatform()); auto result = cache.getOrCompile (key, bundle, options); // Headless compile fails, but we verify the path doesn't crash. EXPECT_TRUE (result.failed()); @@ -478,14 +478,14 @@ TEST_F (GpuPipelineCacheTests, GetOrCompileWithExplicitKeyStoresCompiledPipeline TEST_F (GpuPipelineTests, GpuFrameHeadlessIsInvalid) { - auto frame = GpuFrame::begin (*context); + auto frame = GpuFrame::begin (context); EXPECT_FALSE (frame.isValid()); EXPECT_FALSE (frame.submit()); } TEST_F (GpuPipelineTests, GpuFrameSubmitIsIdempotentOnInvalid) { - auto frame = GpuFrame::begin (*context); + auto frame = GpuFrame::begin (context); EXPECT_FALSE (frame.submit()); EXPECT_FALSE (frame.submit()); EXPECT_NO_THROW (frame.waitForGPU()); @@ -493,10 +493,10 @@ TEST_F (GpuPipelineTests, GpuFrameSubmitIsIdempotentOnInvalid) TEST_F (GpuPipelineTests, GpuFrameMoveAssignmentMovesInvalidState) { - auto src = GpuFrame::begin (*context); + auto src = GpuFrame::begin (context); EXPECT_FALSE (src.isValid()); - auto dst = GpuFrame::begin (*context); + auto dst = GpuFrame::begin (context); dst = std::move (src); EXPECT_FALSE (dst.isValid()); @@ -505,7 +505,7 @@ TEST_F (GpuPipelineTests, GpuFrameMoveAssignmentMovesInvalidState) TEST_F (GpuPipelineTests, GpuFrameMoveConstructionFromInvalidIsInvalid) { - auto src = GpuFrame::begin (*context); + auto src = GpuFrame::begin (context); GpuFrame dst (std::move (src)); EXPECT_FALSE (dst.isValid()); @@ -515,7 +515,7 @@ TEST_F (GpuPipelineTests, GpuFrameMoveConstructionFromInvalidIsInvalid) TEST_F (GpuPipelineTests, GpuFrameDestructorDoesNotCrashOnInvalid) { { - auto frame = GpuFrame::begin (*context); + auto frame = GpuFrame::begin (context); EXPECT_FALSE (frame.isValid()); } // Destructor calls submit() which is idempotent. diff --git a/tests/yup_graphics/yup_GpuPipelineMocked.cpp b/tests/yup_rhi/yup_GpuPipelineMocked.cpp similarity index 80% rename from tests/yup_graphics/yup_GpuPipelineMocked.cpp rename to tests/yup_rhi/yup_GpuPipelineMocked.cpp index cfa7b0f46..975055f81 100644 --- a/tests/yup_graphics/yup_GpuPipelineMocked.cpp +++ b/tests/yup_rhi/yup_GpuPipelineMocked.cpp @@ -22,6 +22,7 @@ #include #include +#include using namespace yup; using ::testing::_; @@ -98,11 +99,11 @@ class GpuPipelineMockTests : public ::testing::Test void SetUp() override { mockOreCtx = std::make_unique>(); - ctx = std::make_unique (mockOreCtx.get()); + ctx = new OreInjectedGpuDevice (mockOreCtx.get()); } std::unique_ptr> mockOreCtx; - std::unique_ptr ctx; + GpuDevice::Ptr ctx; }; // -------------------------------------------------------------------------- @@ -132,7 +133,7 @@ TEST_F (GpuPipelineMockTests, CompileSucceedsWithValidShaders) auto vs = makeShaderSource ("// VS"); auto fs = makeShaderSource ("// FS"); - auto result = GpuPipeline::compile (*ctx, vs, fs); + auto result = GpuPipeline::compile (ctx, vs, fs); ASSERT_TRUE (result.wasOk()); ASSERT_NE (result.getValue(), nullptr); } @@ -145,7 +146,7 @@ TEST_F (GpuPipelineMockTests, CompileFailsWhenVertexModuleIsNull) auto vs = makeShaderSource ("// VS"); auto fs = makeShaderSource ("// FS"); - auto result = GpuPipeline::compile (*ctx, vs, fs); + auto result = GpuPipeline::compile (ctx, vs, fs); EXPECT_TRUE (result.failed()); EXPECT_FALSE (result.getErrorMessage().isEmpty()); } @@ -161,7 +162,7 @@ TEST_F (GpuPipelineMockTests, CompileFailsWhenFragmentModuleIsNull) auto vs = makeShaderSource ("// VS"); auto fs = makeShaderSource ("// FS"); - auto result = GpuPipeline::compile (*ctx, vs, fs); + auto result = GpuPipeline::compile (ctx, vs, fs); EXPECT_TRUE (result.failed()); EXPECT_FALSE (result.getErrorMessage().isEmpty()); } @@ -185,7 +186,7 @@ TEST_F (GpuPipelineMockTests, CompileFailsWhenPipelineCreationFails) auto vs = makeShaderSource ("// VS"); auto fs = makeShaderSource ("// FS"); - auto result = GpuPipeline::compile (*ctx, vs, fs); + auto result = GpuPipeline::compile (ctx, vs, fs); EXPECT_TRUE (result.failed()); EXPECT_FALSE (result.getErrorMessage().isEmpty()); } @@ -211,7 +212,7 @@ TEST_F (GpuPipelineMockTests, CompileValidatesEmptyVertexCode) auto fs = makeShaderSource ("// FS"); - auto result = GpuPipeline::compile (*ctx, vs, fs); + auto result = GpuPipeline::compile (ctx, vs, fs); EXPECT_TRUE (result.failed()); } @@ -259,7 +260,7 @@ TEST_F (GpuPipelineMockTests, CompileWithColorTargetAndDepthStencil) options.depthStencil.depthWriteEnabled = true; options.sampleCount = 4; - auto result = GpuPipeline::compile (*ctx, vs, fs, options); + auto result = GpuPipeline::compile (ctx, vs, fs, options); ASSERT_TRUE (result.wasOk()); ASSERT_NE (result.getValue(), nullptr); } @@ -289,7 +290,7 @@ TEST_F (GpuPipelineMockTests, CompileWithVertexBuffers) options.vertexBuffers = &layout; options.vertexBufferCount = 1; - auto result = GpuPipeline::compile (*ctx, vs, fs, options); + auto result = GpuPipeline::compile (ctx, vs, fs, options); ASSERT_TRUE (result.wasOk()); ASSERT_NE (result.getValue(), nullptr); } @@ -341,7 +342,7 @@ TEST_F (GpuPipelineMockTests, CompileWithMultipleBindGroups) auto fs = makeShaderSource ("// FS"); GpuPipelineOptions options; - auto result = GpuPipeline::compile (*ctx, vs, fs, options); + auto result = GpuPipeline::compile (ctx, vs, fs, options); ASSERT_TRUE (result.wasOk()); } @@ -355,11 +356,11 @@ class GpuBufferMockTests : public ::testing::Test void SetUp() override { mockOreCtx = std::make_unique>(); - ctx = std::make_unique (mockOreCtx.get()); + ctx = new OreInjectedGpuDevice (mockOreCtx.get()); } std::unique_ptr> mockOreCtx; - std::unique_ptr ctx; + GpuDevice::Ptr ctx; }; TEST_F (GpuBufferMockTests, CreateSucceedsWithValidData) @@ -370,7 +371,7 @@ TEST_F (GpuBufferMockTests, CreateSucceedsWithValidData) .WillOnce (Return (oreBuf)); const float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; - auto buf = GpuBuffer::create (*ctx, GpuBufferType::vertex, data, sizeof (data)); + auto buf = GpuBuffer::create (ctx, GpuBufferType::vertex, data, sizeof (data)); ASSERT_NE (buf, nullptr); EXPECT_EQ (buf->getType(), GpuBufferType::vertex); EXPECT_EQ (buf->getSizeInBytes(), sizeof (data)); @@ -385,7 +386,7 @@ TEST_F (GpuBufferMockTests, CreateSucceedsForIndexBuffer) .WillOnce (Return (oreBuf)); const uint16_t data[] = { 0, 1, 2, 3 }; - auto buf = GpuBuffer::create (*ctx, GpuBufferType::index, data, sizeof (data)); + auto buf = GpuBuffer::create (ctx, GpuBufferType::index, data, sizeof (data)); ASSERT_NE (buf, nullptr); EXPECT_EQ (buf->getType(), GpuBufferType::index); EXPECT_TRUE (buf->isValid()); @@ -399,7 +400,7 @@ TEST_F (GpuBufferMockTests, CreateSucceedsForUniformBuffer) .WillOnce (Return (oreBuf)); const int data[] = { 42 }; - auto buf = GpuBuffer::create (*ctx, GpuBufferType::uniform, data, sizeof (data)); + auto buf = GpuBuffer::create (ctx, GpuBufferType::uniform, data, sizeof (data)); ASSERT_NE (buf, nullptr); EXPECT_EQ (buf->getType(), GpuBufferType::uniform); EXPECT_TRUE (buf->isValid()); @@ -411,7 +412,7 @@ TEST_F (GpuBufferMockTests, CreateReturnsNullWhenMakeBufferFails) .WillOnce (ReturnNull()); const float data[] = { 1.0f }; - auto buf = GpuBuffer::create (*ctx, GpuBufferType::vertex, data, sizeof (data)); + auto buf = GpuBuffer::create (ctx, GpuBufferType::vertex, data, sizeof (data)); EXPECT_EQ (buf, nullptr); } @@ -425,18 +426,18 @@ class GpuFrameMockTests : public ::testing::Test void SetUp() override { mockOreCtx = std::make_unique>(); - ctx = std::make_unique (mockOreCtx.get()); + ctx = new OreInjectedGpuDevice (mockOreCtx.get()); } std::unique_ptr> mockOreCtx; - std::unique_ptr ctx; + GpuDevice::Ptr ctx; }; TEST_F (GpuFrameMockTests, BeginCallsOreBeginFrame) { EXPECT_CALL (*mockOreCtx, beginFrame (_)); - auto frame = GpuFrame::begin (*ctx); + auto frame = GpuFrame::begin (ctx); EXPECT_TRUE (frame.isValid()); } @@ -446,7 +447,7 @@ TEST_F (GpuFrameMockTests, SubmitCallsOreEndFrame) EXPECT_CALL (*mockOreCtx, beginFrame (_)); EXPECT_CALL (*mockOreCtx, endFrame()); - auto frame = GpuFrame::begin (*ctx); + auto frame = GpuFrame::begin (ctx); ASSERT_TRUE (frame.isValid()); EXPECT_TRUE (frame.submit()); } @@ -456,7 +457,7 @@ TEST_F (GpuFrameMockTests, SubmitIsIdempotent) EXPECT_CALL (*mockOreCtx, beginFrame (_)); EXPECT_CALL (*mockOreCtx, endFrame()); - auto frame = GpuFrame::begin (*ctx); + auto frame = GpuFrame::begin (ctx); ASSERT_TRUE (frame.isValid()); EXPECT_TRUE (frame.submit()); EXPECT_FALSE (frame.submit()); @@ -468,7 +469,7 @@ TEST_F (GpuFrameMockTests, WaitForGpuCallsOreWaitForGPU) EXPECT_CALL (*mockOreCtx, endFrame()); EXPECT_CALL (*mockOreCtx, waitForGPU()); - auto frame = GpuFrame::begin (*ctx); + auto frame = GpuFrame::begin (ctx); ASSERT_TRUE (frame.isValid()); frame.submit(); frame.waitForGPU(); @@ -480,7 +481,7 @@ TEST_F (GpuFrameMockTests, DestructorSubmitsIfNotSubmitted) EXPECT_CALL (*mockOreCtx, endFrame()); { - auto frame = GpuFrame::begin (*ctx); + auto frame = GpuFrame::begin (ctx); ASSERT_TRUE (frame.isValid()); // Not explicitly submitted — destructor does it. } @@ -491,8 +492,8 @@ TEST_F (GpuFrameMockTests, MoveAssignmentSubmitsExisting) EXPECT_CALL (*mockOreCtx, beginFrame (_)).Times (2); EXPECT_CALL (*mockOreCtx, endFrame()).Times (2); - auto src = GpuFrame::begin (*ctx); - auto dst = GpuFrame::begin (*ctx); + auto src = GpuFrame::begin (ctx); + auto dst = GpuFrame::begin (ctx); dst = std::move (src); } @@ -507,11 +508,11 @@ class GpuPipelineBundleMockTests : public ::testing::Test void SetUp() override { mockOreCtx = std::make_unique>(); - ctx = std::make_unique (mockOreCtx.get()); + ctx = new OreInjectedGpuDevice (mockOreCtx.get()); } std::unique_ptr> mockOreCtx; - std::unique_ptr ctx; + GpuDevice::Ptr ctx; }; TEST_F (GpuPipelineBundleMockTests, CompileFromBundleSucceeds) @@ -554,7 +555,7 @@ TEST_F (GpuPipelineBundleMockTests, CompileFromBundleSucceeds) fs.reflection.uniformBuffers.push_back (ub); bundle.addShader (fs); - auto result = GpuPipeline::compileFromBundle (*ctx, bundle); + auto result = GpuPipeline::compileFromBundle (ctx, bundle); ASSERT_TRUE (result.wasOk()); ASSERT_NE (result.getValue(), nullptr); } @@ -570,7 +571,7 @@ TEST_F (GpuPipelineBundleMockTests, CompileFromBundleFailsWhenNoVertexShader) fs.source = "// fragment shader"; bundle.addShader (fs); - auto result = GpuPipeline::compileFromBundle (*ctx, bundle); + auto result = GpuPipeline::compileFromBundle (ctx, bundle); EXPECT_TRUE (result.failed()); } @@ -585,79 +586,10 @@ TEST_F (GpuPipelineBundleMockTests, CompileFromBundleFailsWhenNoFragmentShader) vs.source = "// vertex shader"; bundle.addShader (vs); - auto result = GpuPipeline::compileFromBundle (*ctx, bundle); + auto result = GpuPipeline::compileFromBundle (ctx, bundle); EXPECT_TRUE (result.failed()); } -// ============================================================================== -// GpuCanvas / GpuTexture / GpuRenderPass — mock-based tests -// ============================================================================== - -class GpuCanvasMockTests : public ::testing::Test -{ -protected: - void SetUp() override - { - mockOreCtx = std::make_unique>(); - ctx = std::make_unique (mockOreCtx.get(), - MockOffscreenTarget::withGpuTexture (64, 48)); - } - - std::unique_ptr> mockOreCtx; - std::unique_ptr ctx; -}; - -TEST_F (GpuCanvasMockTests, CreateReturnsValidCanvas) -{ - auto canvas = GpuCanvas::create (*ctx, 64, 48); - ASSERT_NE (canvas, nullptr); - EXPECT_EQ (canvas->getWidth(), 64); - EXPECT_EQ (canvas->getHeight(), 48); -} - -TEST_F (GpuCanvasMockTests, CreateWithZeroWidthReturnsNull) -{ - EXPECT_EQ (GpuCanvas::create (*ctx, 0, 64), nullptr); -} - -TEST_F (GpuCanvasMockTests, CreateWithZeroHeightReturnsNull) -{ - EXPECT_EQ (GpuCanvas::create (*ctx, 64, 0), nullptr); -} - -TEST_F (GpuCanvasMockTests, AsTextureReturnsValidTexture) -{ - auto canvas = GpuCanvas::create (*ctx, 64, 48); - ASSERT_NE (canvas, nullptr); - - // asTexture() works immediately when the canvas wraps a GPU texture, - // not only after 2D commit(). - auto tex = canvas->asTexture(); - ASSERT_NE (tex, nullptr); - EXPECT_EQ (tex->getWidth(), 64); - EXPECT_EQ (tex->getHeight(), 48); - EXPECT_TRUE (tex->isValid()); - EXPECT_FALSE (tex->isRenderTarget()); // fromGpuTexture() sets renderTarget=false -} - -TEST_F (GpuCanvasMockTests, CommitReturnsFalseWhenNoFrameOpen) -{ - auto canvas = GpuCanvas::create (*ctx, 64, 48); - ASSERT_NE (canvas, nullptr); - - // No 2D frame was opened via getGraphics, so commit() returns false. - EXPECT_FALSE (canvas->commit()); -} - -TEST_F (GpuCanvasMockTests, ReadPixelsReturnsFalseBeforeCommit) -{ - auto canvas = GpuCanvas::create (*ctx, 64, 48); - ASSERT_NE (canvas, nullptr); - - std::vector buf (64 * 48 * 4, 0); - EXPECT_FALSE (canvas->readPixels (buf.data(), buf.size())); -} - // ============================================================================== // GpuRenderPass — mock-based tests // ============================================================================== @@ -668,33 +600,33 @@ class GpuRenderPassMockTests : public ::testing::Test void SetUp() override { mockOreCtx = std::make_unique>(); - ctx = std::make_unique (mockOreCtx.get(), MockOffscreenTarget::withGpuTexture (256, 128)); - headlessCtx = yup::GraphicsContext::createContext (yup::GraphicsContext::Headless, {}); + ctx = new OreAndTargetGpuDevice (mockOreCtx.get(), MockOffscreenTarget::withGpuTexture (256, 128)); + headlessCtx = yup::GpuDevice::create (yup::GpuPlatform::Headless, {}); } GpuFrame makeValidFrame() { EXPECT_CALL (*mockOreCtx, beginFrame (_)); - return GpuFrame::begin (*ctx); + return GpuFrame::begin (ctx); } GpuFrame makeInvalidFrame() { - return GpuFrame::begin (*headlessCtx); + return GpuFrame::begin (headlessCtx); } std::unique_ptr> mockOreCtx; - std::unique_ptr ctx; - std::unique_ptr headlessCtx; + GpuDevice::Ptr ctx; + yup::GpuDevice::Ptr headlessCtx; }; TEST_F (GpuRenderPassMockTests, BeginRenderPassWithInvalidFrameReturnsInvalidPass) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); auto invalid = makeInvalidFrame(); - auto pass = canvas->beginRenderPass (invalid); + auto pass = target->beginRenderPass (invalid); EXPECT_FALSE (pass.isValid()); EXPECT_FALSE (pass.draw (3)); EXPECT_FALSE (pass.drawIndexed (3)); @@ -703,11 +635,11 @@ TEST_F (GpuRenderPassMockTests, BeginRenderPassWithInvalidFrameReturnsInvalidPas TEST_F (GpuRenderPassMockTests, BeginRenderPassWithValidFrameReturnsValidPass) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); auto valid = makeValidFrame(); - auto pass = canvas->beginRenderPass (valid); + auto pass = target->beginRenderPass (valid); EXPECT_TRUE (pass.isValid()); pass.finish(); @@ -716,8 +648,8 @@ TEST_F (GpuRenderPassMockTests, BeginRenderPassWithValidFrameReturnsValidPass) TEST_F (GpuRenderPassMockTests, SetPipelineOnValidPassDoesNotCrash) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); // Compile a real pipeline via mocks for the setPipeline test. auto vsModule = makeShaderModuleWithBindingMap(); @@ -733,12 +665,12 @@ TEST_F (GpuRenderPassMockTests, SetPipelineOnValidPassDoesNotCrash) EXPECT_CALL (*mockOreCtx, makePipeline (_, _)) .WillOnce (Return (pipeline)); - auto compileResult = GpuPipeline::compile (*ctx, makeShaderSource ("// VS"), makeShaderSource ("// FS")); + auto compileResult = GpuPipeline::compile (ctx, makeShaderSource ("// VS"), makeShaderSource ("// FS")); ASSERT_TRUE (compileResult.wasOk()); auto* compiledPipeline = compileResult.getValue().get(); auto valid = makeValidFrame(); - auto pass = canvas->beginRenderPass (valid); + auto pass = target->beginRenderPass (valid); EXPECT_TRUE (pass.isValid()); EXPECT_NO_THROW (pass.setPipeline (*compiledPipeline)); @@ -755,11 +687,11 @@ TEST_F (GpuRenderPassMockTests, SetPipelineOnValidPassDoesNotCrash) TEST_F (GpuRenderPassMockTests, SetPipelineOnInvalidPassDoesNotCrash) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); auto invalid = makeInvalidFrame(); - auto pass = canvas->beginRenderPass (invalid); + auto pass = target->beginRenderPass (invalid); EXPECT_FALSE (pass.isValid()); int dummy = 0; @@ -771,11 +703,11 @@ TEST_F (GpuRenderPassMockTests, SetPipelineOnInvalidPassDoesNotCrash) TEST_F (GpuRenderPassMockTests, SetTextureOnInvalidPassDoesNotCrash) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); auto invalid = makeInvalidFrame(); - auto pass = canvas->beginRenderPass (invalid); + auto pass = target->beginRenderPass (invalid); EXPECT_FALSE (pass.isValid()); int dummy = 0; @@ -789,11 +721,11 @@ TEST_F (GpuRenderPassMockTests, SetTextureOnInvalidPassDoesNotCrash) TEST_F (GpuRenderPassMockTests, MoveConstructionPreservesInvalidState) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); auto invalid = makeInvalidFrame(); - auto src = canvas->beginRenderPass (invalid); + auto src = target->beginRenderPass (invalid); EXPECT_FALSE (src.isValid()); GpuRenderPass dst (std::move (src)); @@ -803,12 +735,12 @@ TEST_F (GpuRenderPassMockTests, MoveConstructionPreservesInvalidState) TEST_F (GpuRenderPassMockTests, MoveAssignmentPreservesInvalidState) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); auto invalid = makeInvalidFrame(); - auto src = canvas->beginRenderPass (invalid); - auto dst = canvas->beginRenderPass (invalid); + auto src = target->beginRenderPass (invalid); + auto dst = target->beginRenderPass (invalid); dst = std::move (src); EXPECT_FALSE (dst.isValid()); @@ -816,23 +748,23 @@ TEST_F (GpuRenderPassMockTests, MoveAssignmentPreservesInvalidState) TEST_F (GpuRenderPassMockTests, FinishIsIdempotentOnInvalidPass) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); auto invalid = makeInvalidFrame(); - auto pass = canvas->beginRenderPass (invalid); + auto pass = target->beginRenderPass (invalid); EXPECT_FALSE (pass.finish()); EXPECT_FALSE (pass.finish()); } TEST_F (GpuRenderPassMockTests, DestructorDoesNotCrashOnInvalidPass) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); { auto invalid = makeInvalidFrame(); - auto pass = canvas->beginRenderPass (invalid); + auto pass = target->beginRenderPass (invalid); EXPECT_FALSE (pass.isValid()); } EXPECT_TRUE (true); @@ -840,8 +772,8 @@ TEST_F (GpuRenderPassMockTests, DestructorDoesNotCrashOnInvalidPass) TEST_F (GpuRenderPassMockTests, DrawEndToEndWithValidPipeline) { - auto canvas = GpuCanvas::create (*ctx, 256, 128); - ASSERT_NE (canvas, nullptr); + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); // Compile a pipeline auto vsModule = makeShaderModuleWithBindingMap(); @@ -857,7 +789,7 @@ TEST_F (GpuRenderPassMockTests, DrawEndToEndWithValidPipeline) EXPECT_CALL (*mockOreCtx, makePipeline (_, _)) .WillOnce (Return (pipeline)); - auto compileResult = GpuPipeline::compile (*ctx, makeShaderSource ("// VS"), makeShaderSource ("// FS")); + auto compileResult = GpuPipeline::compile (ctx, makeShaderSource ("// VS"), makeShaderSource ("// FS")); ASSERT_TRUE (compileResult.wasOk()); auto* compiledPipeline = compileResult.getValue().get(); @@ -876,7 +808,7 @@ TEST_F (GpuRenderPassMockTests, DrawEndToEndWithValidPipeline) .WillOnce (Return (std::move (mockRenderPass))); auto valid = makeValidFrame(); - auto pass = canvas->beginRenderPass (valid); + auto pass = target->beginRenderPass (valid); ASSERT_TRUE (pass.isValid()); pass.setPipeline (*compiledPipeline); @@ -917,7 +849,7 @@ TEST_F (GpuPipelineMockTests, CompileFromGlslSucceeds) "layout(location = 0) out vec4 fragColor;\n" "void main() { fragColor = vec4(1.0, 0.0, 0.0, 1.0); }\n"; - auto result = GpuPipeline::compileFromGlsl (*ctx, vertexGlsl, fragmentGlsl); + auto result = GpuPipeline::compileFromGlsl (ctx, vertexGlsl, fragmentGlsl); EXPECT_TRUE (result.wasOk()); ASSERT_NE (result.getValue(), nullptr); } @@ -948,7 +880,7 @@ TEST_F (GpuPipelineMockTests, CompileFromGlslWithOptions) "layout(location = 0) out vec4 c;\n" "void main() { c = vec4(1); }\n"; - auto result = GpuPipeline::compileFromGlsl (*ctx, vs, fs, options); + auto result = GpuPipeline::compileFromGlsl (ctx, vs, fs, options); EXPECT_TRUE (result.wasOk()); ASSERT_NE (result.getValue(), nullptr); } diff --git a/tests/yup_graphics/yup_GpuTarget.cpp b/tests/yup_rhi/yup_GpuTarget.cpp similarity index 89% rename from tests/yup_graphics/yup_GpuTarget.cpp rename to tests/yup_rhi/yup_GpuTarget.cpp index aa9c1336e..3da3ab919 100644 --- a/tests/yup_graphics/yup_GpuTarget.cpp +++ b/tests/yup_rhi/yup_GpuTarget.cpp @@ -21,7 +21,7 @@ #include -#include +#include using namespace yup; @@ -30,11 +30,11 @@ class GpuTargetTests : public ::testing::Test protected: void SetUp() override { - context = GraphicsContext::createContext (GraphicsContext::Headless, {}); + context = GpuDevice::create (GpuPlatform::Headless, {}); ASSERT_NE (context, nullptr); } - std::unique_ptr context; + GpuDevice::Ptr context; }; // --------------------------------------------------------------------------- @@ -74,15 +74,6 @@ TEST_F (GpuTargetTests, AsTextureReturnsNull) EXPECT_EQ (target->asTexture(), nullptr); } -TEST_F (GpuTargetTests, AsImageReturnsEmptyImage) -{ - auto target = GpuTarget::create (*context, 64, 64); - if (target == nullptr) - return; - - EXPECT_FALSE (target->asImage().isValid()); -} - TEST_F (GpuTargetTests, ReadPixelsReturnsFalseOrSucceeds) { auto target = GpuTarget::create (*context, 64, 64); @@ -111,7 +102,7 @@ TEST_F (GpuTargetTests, GetWidthAndHeightAreNonNegative) TEST_F (GpuTargetTests, BeginRenderPassWithHeadlessReturnsInvalidPass) { - auto frame = GpuFrame::begin (*context); + auto frame = GpuFrame::begin (context); if (! frame.isValid()) return;