diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index a5bf6fceb..efe72c2d2 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -32,7 +32,9 @@ env: libasound2-dev libjack-jackd2-dev ladspa-sdk libcurl4-openssl-dev libfreetype6-dev libx11-dev libxcomposite-dev libxcursor-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxrandr-dev libxrender-dev libxfixes-dev libxss-dev libxtst-dev libxkbcommon-dev - libglu1-mesa-dev libegl1-mesa-dev mesa-common-dev + libglu1-mesa-dev libegl1-mesa-dev mesa-common-dev libgl1-mesa-dri mesa-utils xvfb + LIBGL_ALWAYS_SOFTWARE: "1" + GALLIUM_DRIVER: "llvmpipe" jobs: configure: @@ -43,6 +45,8 @@ jobs: fetch-depth: 0 - name: Install Dependencies run: sudo apt-get update && sudo apt-get install -y ${INSTALL_DEPS} + - name: Test OpenGL Version + run: xvfb-run glxinfo | grep "OpenGL version" - name: Configure run: cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_ENABLE_TESTS=ON -DYUP_ENABLE_EXAMPLES=ON - name: Build SDL @@ -75,7 +79,7 @@ jobs: run: cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_ENABLE_TESTS=ON - run: cmake --build ${{ runner.workspace }}/build --config Debug --target yup_tests - working-directory: ${{ runner.workspace }}/build/tests/Debug - run: ./yup_tests + run: xvfb-run -a ./yup_tests build_tests_release: runs-on: ubuntu-latest @@ -96,7 +100,7 @@ jobs: run: cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_ENABLE_TESTS=ON - run: cmake --build ${{ runner.workspace }}/build --config Release --target yup_tests - working-directory: ${{ runner.workspace }}/build/tests/Release - run: ./yup_tests + run: xvfb-run -a ./yup_tests build_console: runs-on: ubuntu-latest diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 79eca136e..85dd1512e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -64,7 +64,9 @@ env: libasound2-dev libjack-jackd2-dev ladspa-sdk libcurl4-openssl-dev libfreetype6-dev libx11-dev libxcomposite-dev libxcursor-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxrandr-dev libxrender-dev libxfixes-dev libxss-dev libxtst-dev libxkbcommon-dev - libglu1-mesa-dev libegl1-mesa-dev mesa-common-dev lcov + libglu1-mesa-dev libegl1-mesa-dev mesa-common-dev libgl1-mesa-dri xvfb lcov + LIBGL_ALWAYS_SOFTWARE: "1" + GALLIUM_DRIVER: "llvmpipe" IGNORE_ERRORS: "mismatch,gcov,source,negative,unused,empty,format,corrupt" jobs: @@ -94,7 +96,7 @@ jobs: run: cmake --build . --target coverage_clean - name: Run C++ Tests working-directory: ${{ runner.workspace }}/build/tests/Debug - run: SDL_VIDEODRIVER=dummy ./yup_tests --gtest_output=xml:test_results.xml + run: xvfb-run -a ./yup_tests - name: Generate C++ Coverage Report working-directory: ${{ runner.workspace }}/build run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e068bb3..55a374171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Added a native WebGPU `GraphicsContext` backend for Emscripten via the Emdawnwebgpu port (`RIVE_WEBGPU=2` + `--use-port=emdawnwebgpu`, enabled with the `ENABLE_EMSCRIPTEN_WEBGPU` parameter of `yup_standalone_app`), rendering Rive content through the browser's WebGPU API without Dawn - Fixed `GpuFrame::begin()` aborting on the Emscripten WebGPU backend: the WGPU context now creates and submits its own command encoder when no external one is provided, matching the Metal/GL/D3D11 self-managed frame model +- Fixed a crash on Windows when creating any native window: the D3D11 `GpuDevice` was built with an already moved-from `ID3D11Device`, and the Direct3D `GraphicsContext` created a second device whose swapchain textures could not be used by the render context. Both now share a single `ID3D11Device` +- Fixed the Emscripten WebGPU `GraphicsContext` never storing its surface size, leaving the offscreen copy at 0x0 +- Fixed `GpuDevice::updateBuffer()` failing for every vertex, index and uniform buffer on the WebGPU, Dawn and D3D11 backends: those overrides handled native storage buffers only and returned false instead of delegating ore-backed buffers to the base class, the way the Metal and OpenGL overrides do +- Implemented `GpuDevice::readBuffer()` for D3D11, which previously reported `isComputeAvailable()` but had no override, so every storage buffer readback silently failed through the base class. It copies into a cached `D3D11_USAGE_STAGING` buffer on the immediate context (ordered after the dispatch) and maps it for reading +- `GpuComputePass` on D3D11 now unbinds the UAV slots it bound when the pass finishes, so a storage buffer is no longer left bound for writing while a later readback or draw reads it +- Fixed `GpuDevice::readBuffer()` never succeeding on the Emscripten WebGPU backend: it mapped its staging buffer with `WGPUCallbackMode_AllowProcessEvents` and then tested the result in the same call, but WebGPU buffer mapping only resolves through the JavaScript event loop, so the callback could not have run. The WGPU backend now pipelines the readback over a ring of three staging buffers using `WGPUCallbackMode_AllowSpontaneous`, which completes on its own between main-loop ticks — no ASYNCIFY needed +- `GpuDevice::readBuffer()` is no longer documented as unconditionally blocking. Whether it blocks is a property of the backend: Metal, D3D11 and OpenGL read back in lockstep and fill the destination every call, while WebGPU cannot map synchronously and so trails the GPU by a frame or two. Callers must now own the destination across calls and treat a false return as "no new data yet" rather than an error — the previous contents stay valid +- `ComputeParticlesDemo`: keeps drawing the last particle snapshot on frames where no new one has landed, so it renders on the Emscripten WebGPU backend instead of showing nothing. The status label reports the landed-snapshot count alongside the frame count +- `Component`'s effect path now reuses its offscreen `GpuCanvas` across frames while the component size is unchanged, instead of allocating (and freeing) a full-size render target every frame. On a size change the outgoing canvas is released before the replacement is created, so its `RenderContext` lease returns to the pool rather than forcing a second context to be reserved permanently +- `ComponentEffectsDemo`: shader effects now share a common base that compiles the pipeline at most once instead of retrying a failed compile on every frame, reports the compile error in the status label and on the console, and shows the CPU time spent applying the effect next to the paint time #### Rive Runtime Bump @@ -40,6 +50,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **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). +#### Compute Shaders & GPU Audio + +- New `GpuComputePipeline` class (`rhi/yup_GpuComputePipeline.h`): an immutable compiled compute pipeline that bypasses ore to go directly to the backend-native API (Metal `MTLComputePipelineState`, D3D11 `ID3D11ComputeShader`, WebGPU/Dawn `wgpu::ComputePipeline`, OpenGL `GL_COMPUTE_SHADER`). `compile(ctx, source, GpuWorkgroupSize)`, `compileFromBundle(ctx, ShaderBundle)`, and `compileFromGlsl(ctx, glsl)` (when `YUP_ENABLE_SHADER_TRANSPILER = 1`) all return `ResultValue`. +- New `GpuComputePass` class (`rhi/yup_GpuComputePass.h`): move-only RAII compute dispatch encoder (`GpuComputePass::begin(device)`). Binds a `GpuComputePipeline`, storage buffers (`setStorageBuffer`), uniform buffers (`setUniformBuffer`), and textures (`setTexture`), then dispatches workgroups via `dispatch(gx, gy, gz)`. +- `GpuBuffer` extended with `GpuBufferType::storage`: native storage buffer creation for each backend (Metal `MTLBuffer`, D3D11 structured buffer + UAV, WebGPU `Storage` buffer, OpenGL `GL_SHADER_STORAGE_BUFFER`). Storage buffers are bound to `GpuComputePass::setStorageBuffer()`. +- `GpuDevice` backends expose native compute handles: `getDevice()`/`getCommandQueue()` (Metal), `getD3DDevice()`/`getD3DDeviceContext()` (D3D11), `getWgpuDevice()`/`getWgpuQueue()` (WebGPU/Emscripten), `getBackendDevice()`/`getDevice()`/`getQueue()` (Dawn). +- `GpuAudioProcessingDemo` example: real-time GPU-accelerated audio effect (gain + soft clipper) using compute shaders. Captures live audio via `AudioIODeviceCallback`, uploads to GPU storage buffers, dispatches a compute shader, and reads back processed audio — all on the audio I/O thread. +- New `GpuDevice::updateBuffer()`: writes new data into an existing storage buffer without reallocating it (Metal `contents` memcpy, D3D11 `UpdateSubresource`, WebGPU/Dawn `WriteBuffer`, GL `glBufferSubData`). Fixes `GpuAudioProcessingDemo` reallocating its input storage buffer every audio callback, which caused audible stutter. The gain/mix parameters remain a uniform buffer (as before) — that path is unaffected and its small per-dispatch allocation is negligible next to the audio-block-sized buffer this fix removes. +- Fixed `ShaderTranspiler`'s MSL backend assigning storage/uniform buffer indices via spirv-cross's own auto-incrementing scheme instead of the shader's declared `layout(binding=N)`: added `CompilerMSL::Options::enable_decoration_binding = true` so the compiled `[[buffer(N)]]` index always matches the declared binding, matching what `GpuComputePass`'s native dispatch (which binds slots as `group*16+binding` with no reflection indirection) requires. This was silently producing zero output from any Metal compute shader with more than one storage/uniform buffer, including `GpuAudioProcessingDemo`. +- Metal `GpuDevice`/`GpuComputePass` calls now wrap their Objective-C work in `@autoreleasepool` blocks — without one, real-time callers (e.g. an audio thread with no ambient pool) accumulated command buffers/encoders indefinitely. + #### 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. @@ -61,6 +82,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - New `GpuBuffer` class (`rhi/yup_GpuBuffer.h`): reference-counted GPU buffer handle wrapping a backend-native GPU buffer. `GpuBuffer::create(ctx, GpuBufferType, data, byteSize)` uploads immutable vertex/index/uniform data for use with `GpuRenderPass`. - `Image::fromTexture(GpuTexture::Ptr)`: creates an `Image` wrapping an existing GPU texture (no CPU round-trip). Suitable for `Graphics::drawImage()`. - `Graphics::drawTexture(GpuTexture::Ptr, Rectangle)`: draws a GPU texture directly without materialising an `Image`, avoiding CPU-side ImagePixelData allocation. +- `GpuRenderPass` no longer creates a sampler and a uniform buffer per draw. The linear/clamp-to-edge samplers that fill a layout's sampler bindings are created once when the `GpuPipeline` is compiled, and uniform buffers come from a size-bucketed pool on the `GpuDevice` that recycles them when a frame reports GPU completion — so a steady-state workload stops allocating GPU objects after its first frames. `GpuFrame` stays stack RAII; nothing changes for callers. +- Fixed the GLSL→WGSL transpiler rejecting comma-separated members in a struct or interface block (`uniform Params { float s, r, rx, ry; }`), which failed with `Expected ';'`. Each declarator now becomes its own member and binds its own array specifiers. #### Shader Compiler (#126 and #130) @@ -129,7 +152,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Bug Fixes - iOS applications now use the `UIScene` lifecycle, removing UIKit's legacy lifecycle warning and ensuring SDL windows are created for the connected scene. -- Offscreen GPU rendering now supports recursive targets on Metal, OpenGL/GLES, and D3D11. Render contexts are reserved only while a target frame is active, so Lottie alpha/luma mattes, isolated-opacity layers, and cached precomps retain GPU compositing when rendered into an `Image` or `GpuCanvas` without allocating a context per sequential target. Repeated Lottie matte and precomp renders now reuse their canvases rather than allocating GPU textures each frame. Metal child targets allocate only their Rive render-canvas output texture; the CPU readback staging texture is created only when pixels are requested. +- Offscreen GPU rendering now supports recursive targets on Metal, OpenGL/GLES, and D3D11, so Lottie alpha/luma mattes, isolated-opacity layers, and cached precomps retain GPU compositing when rendered into an `Image` or `GpuCanvas`. Each `RenderableTarget` leases a Rive render context exclusively for its lifetime and returns it to the pool when destroyed. Repeated Lottie matte and precomp renders now reuse their canvases rather than allocating GPU textures each frame. Metal child targets allocate only their Rive render-canvas output texture; the CPU readback staging texture is created only when pixels are requested. +- Fixed undefined offscreen contents when nesting pooled render targets on all GPU backends. Render context slots were recycled whenever no frame was currently active, so two long-lived targets could share one slot; once their frames nested — which happens as Lottie matte and precomp layers cross their in/out points and the nesting order changes between frames — the inner target skipped `beginFrame` and was then flushed against the outer target's frame descriptor. +- Lottie: a matte layer no longer paints another matte layer's content. Drawing a matte result only queues a reference to its canvas texture, which the enclosing frame resolves at flush time, but the canvas lease was released as soon as the layer finished. Since every matte in a composition is sized to the same fitted rectangle, the pool handed the same canvas triple to the next matte layer, which overwrote the pixels already queued and left only the last matte visible (e.g. `world_locations.json`'s four matted dots collapsed to one and its continent outlines disappeared; `insta_camera.json` lost its animated circles). Leases are now held until the composition render completes. +- `GpuFrame` now waits for the GPU before releasing the texture views, uniform buffers and samplers it keeps alive for its encoded render passes. Those passes reference them by raw pointer, and `submit()` does not block, so letting a frame go out of scope freed them while the GPU was still reading — corrupting the pass output progressively, as the freed memory only starts being handed back out after the allocator has churned for a while (the growing magenta flashes in `bell.json`). `waitForGPU()` is now only needed explicitly when results are required before the end of the frame's scope, and is idempotent so waiting explicitly costs no more than one stall. Move-assignment drains the frame it replaces for the same reason. +- `AffineTransform::getScaleFactor()` is now independent of rotation. It averaged the absolute values of the matrix diagonal and ignored the shear terms, so a rotated transform reported `scale * cos(angle)` — falling to zero at 90 degrees. It now measures the lengths of the transformed basis vectors. Lottie precomposition and matte canvases are sized from this value, so a layer under an animated rotation (e.g. `bell.json`, whose precomposition is parented to a rotating null) requested a different pixel size on every frame, reallocating its canvases mid-frame and flashing while a queued draw still referenced the previous ones. +- Lottie: the matte canvas pool now replaces an idle slot of a different size instead of appending a new one. Nothing removed slots, so a layer whose on-screen size changed every frame added three canvases — each leasing a Rive render context — per frame, without bound. +- `GpuCanvas::create()` takes a `std::optional clearColor`, defaulting to transparent black, and fills the new canvas with it so it is safe to sample before anything is drawn into it (pass `std::nullopt` to leave the contents undefined). The backing texture is allocated uninitialized and a 2D frame whose draw list ends up empty is not guaranteed to honour its `loadAction=clear`, so a canvas could previously be composited while still holding undefined GPU memory (the magenta flashes in `bell.json`, whose only content is one matted precomposition). The clear is issued through the new `GpuDevice::clearOffscreen()`, which encodes it with the backend's native API — a clear binds no pipeline, buffers or samplers, so it needs neither a render pass nor a submit/wait cycle. +- `GpuCanvas::beginDraw()` now drops the target's cached `GpuTexture` wrap, as its documentation already claimed. The wrap memoizes the Rive texture handle it resolved, so a pooled canvas reused across frames kept handing out the handle resolved on the frame it was first sampled. +- Lottie: a failed matte composite no longer blits undefined GPU memory over the matted layer. The result canvas is written only by the composite render pass — nothing else clears it, and its backing texture is allocated uninitialized — but the pass result was ignored and the texture composited regardless, flashing an arbitrary color. The renderer now falls back to the geometric-clip matte path when the composite fails. +- Lottie: a paint-less nested group now contributes its geometry to the enclosing group's paints with its own modifiers applied. The geometry was rebuilt from raw shapes, dropping the nested group's trim, repeater, merge-paths and rounded-corner modifiers, which is what defines the outline: RubberHose rigs draw a limb as a 4-point star trimmed to a quarter, so the parent stroke painted the whole star instead of an arc (the stray stars in `mughead.json` and `pumped_up.json`). - Lottie: track mattes (alpha, alpha-inverted, luma, luma-inverted) now composite the matte source's *rendered alpha* — including its fill opacity, gradients, and anti-aliased edges — instead of hard-clipping the target to the source silhouette. The matte source and target are rendered into offscreen GPU buffers (sized to the fitted on-screen resolution) and multiplied by a fullscreen matte-composite shader. A partially transparent matte source now shows through correctly (e.g. `matte_two_item_with_lowerlayer.json`, whose 65%-opacity source blends the white matted ellipse to pink over the red layer beneath). Falls back to the previous geometric-clip behaviour when no GPU is available (e.g. headless rendering). - Lottie: `EllipseShape` paths now start at the top (12 o'clock) and follow the shape direction (clockwise for `d == 1`, counter-clockwise for `d == 3`), matching Lottie's convention. Previously they started at the right (3 o'clock) going counter-clockwise, which placed trimmed arcs at the wrong position (e.g. the expanding rings in `world_locations.json` were cut short on the right). - `Path::withRoundedCorners()` left one corner sharp on closed subpaths whose geometry ended with an explicit segment back to the start vertex (as produced by Lottie bezier `toPath()`). The duplicated start/end point formed a zero-length edge that made that corner degenerate. The trailing duplicate is now dropped, and corners are rounded with a cubic arc (circle kappa) instead of a single quadratic through the vertex, so a square with a full Round Corners modifier becomes a proper circle (e.g. the morphing loader shape in `loader.json`). diff --git a/codecov.yml b/codecov.yml index 4f79ea66e..8a691dd12 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,6 +10,7 @@ coverage: project: default: informational: true + if_ci_failed: error target: 80% threshold: 5% base: auto @@ -30,11 +31,13 @@ coverage: - yup_graphics - yup_gui - yup_python + - yup_rhi - yup_shading - yup_simd patch: default: informational: true + if_ci_failed: error target: 80% threshold: 5% @@ -55,6 +58,7 @@ flags: yup_graphics: { paths: [modules/yup_graphics/], carryforward: true } yup_gui: { paths: [modules/yup_gui/], carryforward: true } yup_python: { paths: [modules/yup_python/], carryforward: true } + yup_rhi: { paths: [modules/yup_rhi/], carryforward: true } yup_shading: { paths: [modules/yup_shading/], carryforward: true } yup_simd: { paths: [modules/yup_simd/], carryforward: true } diff --git a/docs/graphics/rhi/compute-shaders.md b/docs/graphics/rhi/compute-shaders.md new file mode 100644 index 000000000..450e1884e --- /dev/null +++ b/docs/graphics/rhi/compute-shaders.md @@ -0,0 +1,167 @@ +# Compute Shaders + +The `yup_rhi` module supports GPU compute shaders through `GpuComputePipeline` +and `GpuComputePass`. Compute shaders run general-purpose GPU work — audio DSP, +physics simulation, particle systems, image processing — without any window, +framebuffer, or graphics pipeline. + +## Availability + +Compute shaders are available on backends that expose +`GpuDevice::isComputeAvailable() == true`: **Metal**, **Direct3D 11**, +**WebGPU** (Dawn and Emscripten), and **OpenGL 4.3+** / **OpenGL ES 3.1+**. + +Compute is **not** available on the Headless backend. + +## Architecture + +Compute shaders bypass Rive's ore layer (`rive::ore`) entirely. The ore layer +does not yet expose compute dispatch, so `GpuComputePipeline` and +`GpuComputePass` go directly to the backend-native API: + +| Backend | Pipeline compilation | Dispatch | +| ------------ | -------------------------------------- | ---------------------------------- | +| Metal | `MTLComputePipelineState` | `dispatchThreadgroups:` | +| Direct3D 11 | `ID3D11ComputeShader` | `ID3D11DeviceContext::Dispatch()` | +| WebGPU | `wgpu::ComputePipeline` | `DispatchWorkgroups()` | +| OpenGL | `GL_COMPUTE_SHADER` + program link | `glDispatchCompute()` | + +## Compiling a compute pipeline + +### From GLSL (online, requires `YUP_ENABLE_SHADER_TRANSPILER`) + +```cpp +auto result = GpuComputePipeline::compileFromGlsl (device, glslSource); +if (result.wasOk()) + auto pipeline = result.getValue(); +``` + +The transpiler compiles GLSL → SPIR-V, reflects the workgroup size from +`layout(local_size_x=...)`, transpiles to the backend-native language (MSL, +HLSL, WGSL), and compiles the final pipeline. + +### From a `.ysl` shader bundle (offline) + +```cpp +auto bundle = ShaderBundle::loadFromFile (File ("audio_effect.ysl")); +if (bundle.wasOk()) +{ + auto result = GpuComputePipeline::compileFromBundle (device, bundle.getReference()); + if (result.wasOk()) + auto pipeline = result.getValue(); +} +``` + +### From raw native source (any backend) + +```cpp +GpuShaderSource source; +source.language = GpuShaderLanguage::msl; // or hlsl, wgsl, glsl +source.code = mslSource; +source.codeSize = mslLength; + +auto result = GpuComputePipeline::compile (device, source, { 256, 1, 1 }); +``` + +## Dispatching compute work + +```cpp +auto pass = GpuComputePass::begin (device); + +pass.setPipeline (pipeline); +pass.setStorageBuffer (0, 0, inputBuffer); // SSBO binding (set=0, binding=0) +pass.setStorageBuffer (0, 1, outputBuffer); // SSBO binding (set=0, binding=1) +pass.setUniformBuffer (0, 2, ¶ms, sizeof params); + +uint32_t groupsX = (numElements + 255) / 256; // workgroupSize.x = 256 +pass.dispatch (groupsX, 1, 1); +pass.finish(); // commits work to the GPU +``` + +## Storage buffers + +Storage buffers (`GpuBufferType::storage`) are read-write GPU buffers for compute +shaders. Create them with `GpuBuffer::create()`: + +```cpp +std::vector data (numSamples, 0.0f); +auto buf = GpuBuffer::create (device, + GpuBufferType::storage, + data.data(), + data.size() * sizeof (float)); +``` + +Unlike vertex/index/uniform buffers that go through ore, storage buffers are +allocated directly on the native API (Metal `MTLBuffer`, D3D11 structured +buffer + UAV, WebGPU `Storage` buffer, GL `GL_SHADER_STORAGE_BUFFER`). + +### Updating a storage buffer in place + +`GpuBuffer` is immutable once created — `GpuDevice::createBuffer()` always +allocates a new native resource. For code that feeds a storage buffer new +data every frame or audio callback (a compute effect processing a live +stream, for instance), reallocating on every iteration is expensive and, on +a real-time thread such as an audio callback, unsafe: buffer allocation has +unbounded, driver-dependent latency and can cause audible dropouts. + +`GpuDevice::updateBuffer()` writes new data into an *existing* storage buffer +without reallocating it: + +```cpp +// Once, outside the hot loop: +auto buf = GpuBuffer::create (device, GpuBufferType::storage, initialData, byteSize); + +// Every frame / audio callback — no allocation: +device->updateBuffer (buf, newData, byteSize); +``` + +`byteSize` must not exceed the buffer's original size. Supported on all +compute-capable backends (Metal, D3D11, WebGPU/Dawn, OpenGL). + +### Buffer binding indices on Metal + +`GpuComputePass`'s native dispatch binds Metal buffer arguments directly as +`group*16 + binding` (see `native/yup_GpuComputePass_metal.cpp`) — there is no +reflection layer translating GLSL `(set, binding)` pairs to the compiled +function's actual `[[buffer(N)]]` indices, unlike the render pipeline path. +This requires the transpiled MSL's argument indices to exactly match the +GLSL/SPIR-V declared `binding=N` values. The transpiler enforces this by +setting `CompilerMSL::Options::enable_decoration_binding = true`; without it, +spirv-cross assigns MSL buffer indices via its own auto-incrementing scheme, +which can silently diverge from the declared bindings for any shader with +more than one buffer resource, producing a pipeline that compiles and +dispatches without error but never actually reads/writes the intended data. + +## Example: GPU audio effect + +The `GpuAudioProcessingDemo` example demonstrates real-time audio processing on +the GPU: + +1. `AudioIODeviceCallback` captures live audio input +2. Audio samples are written into a preallocated `GpuBuffer` storage buffer via + `updateBuffer()` — no GPU allocation happens on the audio thread +3. A compute shader applies gain + soft clipping +4. Processed samples are read back from the GPU +5. Results are routed to the audio output + +The compute shader runs on the audio I/O thread, using a dedicated `GpuDevice` +that does not share state with the render thread. The tiny per-block +parameters (gain, mix) stay a uniform buffer bound via `setUniformBuffer()` — +`dispatch()` allocates a small temporary buffer for it on every call, but at +16 bytes that's negligible next to the audio-block-sized input buffer that +`updateBuffer()` now avoids reallocating. + +```glsl +#version 450 +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout(std430, set = 0, binding = 0) buffer InputBuf { float inData[]; }; +layout(std430, set = 0, binding = 1) buffer OutputBuf { float outData[]; }; +layout(std140, set = 0, binding = 2) uniform Params { float gain; float mix; }; + +void main() { + uint i = gl_GlobalInvocationID.x; + float s = inData[i] * gain; + outData[i] = tanh(s) * mix + inData[i] * (1.0 - mix); +} +``` diff --git a/docs/graphics/rhi/index.md b/docs/graphics/rhi/index.md index 28ef357fb..6c672a631 100644 --- a/docs/graphics/rhi/index.md +++ b/docs/graphics/rhi/index.md @@ -7,8 +7,8 @@ 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. For GPU compute without any window or +express - 3D geometry, post-process effects, compute passes for DSP or simulation, +or 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. @@ -19,8 +19,9 @@ graphics (e.g. audio DSP on the GPU), use `GpuDevice` directly — no | Draw 2D vector content (paths, text, images) | `Graphics` (not the RHI) | | Render custom geometry with your own shaders | `GpuPipeline` + `GpuRenderPass` | | Apply a fullscreen post-process effect | `GpuPipeline` (fullscreen) | +| Run GPU compute (DSP, simulation) | `GpuComputePipeline` + `GpuComputePass` | | Render offscreen and sample the result as a texture | `GpuTarget` or `GpuCanvas` | -| Mix 2D drawing *and* custom passes on one surface | `GpuCanvas` | +| Mix 2D drawing *and* custom passes on one surface | `GpuCanvas` | ## Classes at a glance @@ -30,10 +31,14 @@ graphics (e.g. audio DSP on the GPU), use `GpuDevice` directly — no submit. - **`GpuRenderPass`** - records draw commands (pipeline, bindings, draws) into a render target within a frame. +- **`GpuComputePass`** - records compute dispatch commands (pipeline, storage + buffers, uniforms) directly against the backend-native API. - **`GpuPipeline`** - an immutable, compiled vertex + fragment pipeline plus fixed state. +- **`GpuComputePipeline`** - an immutable, compiled compute pipeline (single + compute stage, native backend API, no ore dependency). - **`GpuPipelineCache`** - thread-safe compile-or-fetch cache for pipelines. -- **`GpuBuffer`** - an immutable vertex, index, or uniform buffer. +- **`GpuBuffer`** - an immutable vertex, index, uniform, or storage buffer. - **`GpuTexture`** - an opaque GPU texture, the currency between passes, `Image`, and `Graphics::drawTexture`. - **`GpuTarget`** - a minimal offscreen render surface for render-pass-only work. diff --git a/docs/index.md b/docs/index.md index d5bbbdbca..93b2ae059 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,6 +27,7 @@ concept guides, walkthroughs, and reference material. - [Imaging](imaging/index.md) - bitmap images: pixels, loading, saving, and drawing. - [UI](ui/index.md) - components, windowing, events, layout, and widgets. - [Audio](audio/index.md) - audio devices, formats, DSP, the audio graph, processors, and plugin hosting/client wrappers. +- [AI](ai/index.md) - the AI and LLM infrastructure. - [Scripting](scripting/index.md) - the Python bindings layer. ## Quick links diff --git a/examples/graphics/source/examples/ComponentEffectsDemo.h b/examples/graphics/source/examples/ComponentEffectsDemo.h index 07d96b296..0269e0ace 100644 --- a/examples/graphics/source/examples/ComponentEffectsDemo.h +++ b/examples/graphics/source/examples/ComponentEffectsDemo.h @@ -76,7 +76,8 @@ class ComponentEffectsDemo : public yup::Component paramSlider->onValueChanged = [this] (double v) { effectParam = (float) v; - onEffectParamChanged (effectParam); + if (shaderEffect != nullptr) + shaderEffect->setEffectParameter (effectParam); background->repaint(); }; addAndMakeVisible (paramSlider.get()); @@ -171,14 +172,29 @@ class ComponentEffectsDemo : public yup::Component void componentPaintCompleted (yup::Component& component, const yup::ComponentPaintMetrics& metrics) override { - if (&component == background.get()) + if (&component != background.get()) + return; + + // The effect runs after this callback, so its timing is one frame behind. + // Comparing the two numbers against the observed frame rate localises a + // slow frame: if both stay small, the time is going to the GPU. + const auto paintMicros = ticksToMicroseconds (metrics.totalTicks); + const auto effectMicros = shaderEffect != nullptr ? ticksToMicroseconds (shaderEffect->getLastApplyTicks()) : 0; + + yup::MessageManager::callAsync ([this, paintMicros, effectMicros] + { + paintTimeLabel->setText ("Paint: " + yup::String (paintMicros) + + " us, effect: " + yup::String (effectMicros) + " us", + yup::dontSendNotification); + }); + + if (! compileErrorReported && shaderEffect != nullptr && shaderEffect->getCompileError().isNotEmpty()) { - const auto us = static_cast (metrics.totalTicks * 1000000.0 - / yup::Time::getHighResolutionTicksPerSecond()); - yup::MessageManager::callAsync ([this, us] + compileErrorReported = true; + + yup::MessageManager::callAsync ([this, error = shaderEffect->getCompileError()] { - paintTimeLabel->setText ("Paint: " + yup::String (us) + " us", - yup::dontSendNotification); + statusLabel->setText ("Shader compile FAILED: " + error, yup::dontSendNotification); }); } } @@ -201,25 +217,97 @@ void main() { }; //============================================================================== - /** Two-pass separable Gaussian blur. Parameter = sigma (0..64). */ - class BlurEffect : public yup::ComponentEffect + /** Base for the demo's shader effects. + + Owns the pipeline and compiles it at most once. Compiling GLSL runs the + shader transpiler - and, on WebGPU, the GLSL→WGSL lowering on top of that - + which costs tens of milliseconds, so a failed compile is remembered + instead of retried: retrying every frame would silently cap the frame rate + rather than just falling back to an unfiltered draw. + + Also records the CPU time spent inside apply(), so the demo can show + whether a slow frame is spent on the CPU or waiting on the GPU. + */ + class ShaderEffect : public yup::ComponentEffect { public: - void setEffectParameter (float s) { sigma = s; } + void setEffectParameter (float v) { param = v; } + + /** CPU ticks spent inside the most recent apply() call. */ + yup::int64 getLastApplyTicks() const noexcept { return applyTicks; } - void apply (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) override + /** The error from the single pipeline compile attempt, empty while it succeeded. */ + const yup::String& getCompileError() const noexcept { return compileError; } + + void apply (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) final { - if (! input || ! input->isValid()) + if (input == nullptr || ! input->isValid()) return; - auto& ctx = g.getGraphicsContext(); - const int w = input->getWidth(), h = input->getHeight(); - const float blurR = std::ceil (sigma * 3.0f); - if (! ensurePipeline (ctx)) - { + const auto startTicks = yup::Time::getHighResolutionTicks(); + + if (ensurePipeline (g.getGraphicsContext())) + applyEffect (g, input, bounds); + else g.drawTexture (input, bounds); - return; + + applyTicks = yup::Time::getHighResolutionTicks() - startTicks; + } + + protected: + /** Returns this effect's fragment shader source. */ + virtual const char* getFragmentSource() const = 0; + + /** Renders the effect. Only called once the pipeline compiled successfully. */ + virtual void applyEffect (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) = 0; + + float param = 8.0f; + yup::GpuPipeline::Ptr pipeline; + + private: + bool ensurePipeline (yup::GraphicsContext& ctx) + { + if (pipeline != nullptr) + return true; + + if (compileAttempted) + return false; + + compileAttempted = true; + + auto result = yup::GpuPipeline::compileFromGlsl (ctx.getGpuDevice(), + kVertSource, + yup::String::fromUTF8 (getFragmentSource()), + {}); + if (result.wasOk()) + { + pipeline = result.getValue(); + return true; } + + compileError = result.getErrorMessage(); + yup::Logger::outputDebugString ("ComponentEffectsDemo: shader compile failed: " + compileError); + return false; + } + + bool compileAttempted = false; + yup::String compileError; + yup::int64 applyTicks = 0; + }; + + //============================================================================== + /** Two-pass separable Gaussian blur. Parameter = sigma (0..64). */ + class BlurEffect : public ShaderEffect + { + protected: + const char* getFragmentSource() const override { return kBlurFrag; } + + void applyEffect (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) override + { + auto& ctx = g.getGraphicsContext(); + const int w = input->getWidth(), h = input->getHeight(); + const float blurR = std::ceil (param * 3.0f); + if (! ensureTargets (ctx, w, h)) { g.drawTexture (input, bounds); @@ -229,7 +317,7 @@ void main() { 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 }; + EffectParams p { param, blurR, (float) w, (float) h, dx, dy, 0, 0 }; auto rp = t.beginRenderPass (frame, { true, yup::Colors::transparentBlack }); rp.setPipeline (pipeline); rp.setTexture (0, 0, in); @@ -246,20 +334,8 @@ void main() { } private: - float sigma = 4.0f; - yup::GpuPipeline::Ptr pipeline; yup::GpuTarget::Ptr targetA, targetB; - bool ensurePipeline (yup::GraphicsContext& ctx) - { - if (pipeline) - return true; - auto r = yup::GpuPipeline::compileFromGlsl (ctx.getGpuDevice(), kVertSource, kBlurFrag, {}); - if (r.wasOk()) - pipeline = r.getValue(); - return pipeline != nullptr; - } - bool ensureTargets (yup::GraphicsContext& ctx, int w, int h) { if (! targetA || targetA->getWidth() != w || targetA->getHeight() != h) @@ -296,43 +372,23 @@ void main() { //============================================================================== /** Single-pass fullscreen effect base. Shared by Pixelate, Edge, Wave, Sharpen, CRT. */ - class SinglePassEffect : public yup::ComponentEffect + class SinglePassEffect : public ShaderEffect { - public: - void setEffectParameter (float v) { param = v; } - protected: - float param = 8.0f; - yup::GpuPipeline::Ptr pipeline; - yup::GpuTarget::Ptr target; - - bool ensurePipeline (yup::GraphicsContext& ctx, const char* fragSource) - { - if (pipeline) - return true; - auto r = yup::GpuPipeline::compileFromGlsl (ctx.getGpuDevice(), kVertSource, yup::String::fromUTF8 (fragSource), {}); - if (r.wasOk()) - pipeline = r.getValue(); - return pipeline != nullptr; - } - - bool ensureTarget (yup::GraphicsContext& ctx, int w, int h) - { - if (! target || target->getWidth() != w || target->getHeight() != h) - target = yup::GpuTarget::create (ctx.getGpuDevice(), w, h); - return target != nullptr; - } + /** Fills the uniform block for this effect from the input dimensions. */ + virtual EffectParams getEffectParams (const yup::GpuTexture& input) const = 0; - void drawPass (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds, const EffectParams& p) + void applyEffect (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) override { auto& ctx = g.getGraphicsContext(); - const int w = input->getWidth(), h = input->getHeight(); - if (! ensureTarget (ctx, w, h)) + if (! ensureTarget (ctx, input->getWidth(), input->getHeight())) { g.drawTexture (input, bounds); return; } + const auto p = getEffectParams (*input); + auto frame = yup::GpuFrame::begin (ctx.getGpuDevice()); { auto rp = target->beginRenderPass (frame, { true, yup::Colors::transparentBlack }); @@ -345,25 +401,28 @@ void main() { frame.submit(); g.drawTexture (target->asTexture(), bounds); } + + private: + yup::GpuTarget::Ptr target; + + bool ensureTarget (yup::GraphicsContext& ctx, int w, int h) + { + if (! target || target->getWidth() != w || target->getHeight() != h) + target = yup::GpuTarget::create (ctx.getGpuDevice(), w, h); + return target != nullptr; + } }; //============================================================================== /** Pixelate: downsamples into blocks. Parameter = block size (1..64). */ class PixelateEffect : public SinglePassEffect { - public: - void apply (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) override + protected: + const char* getFragmentSource() const override { return kPixelateFrag; } + + EffectParams getEffectParams (const yup::GpuTexture& input) const override { - if (! input || ! input->isValid()) - return; - auto& ctx = g.getGraphicsContext(); - if (! ensurePipeline (ctx, kPixelateFrag)) - { - g.drawTexture (input, bounds); - return; - } - EffectParams p { param, (float) input->getWidth(), (float) input->getHeight(), 0, 0, 0, 0, 0 }; - drawPass (g, input, bounds, p); + return { param, (float) input.getWidth(), (float) input.getHeight(), 0, 0, 0, 0, 0 }; } private: @@ -386,19 +445,12 @@ void main() { /** Sobel edge detection. Parameter = threshold (0..64). */ class EdgeEffect : public SinglePassEffect { - public: - void apply (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) override + protected: + const char* getFragmentSource() const override { return kEdgeFrag; } + + EffectParams getEffectParams (const yup::GpuTexture& input) const override { - if (! input || ! input->isValid()) - return; - auto& ctx = g.getGraphicsContext(); - if (! ensurePipeline (ctx, kEdgeFrag)) - { - g.drawTexture (input, bounds); - return; - } - EffectParams p { param * 0.05f, (float) input->getWidth(), (float) input->getHeight(), 0, 0, 0, 0, 0 }; - drawPass (g, input, bounds, p); + return { param * 0.05f, (float) input.getWidth(), (float) input.getHeight(), 0, 0, 0, 0, 0 }; } private: @@ -433,18 +485,12 @@ void main() { public: WaveEffect() { param = 12.0f; } - void apply (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) override + protected: + const char* getFragmentSource() const override { return kWaveFrag; } + + EffectParams getEffectParams (const yup::GpuTexture& input) const override { - if (! input || ! input->isValid()) - return; - auto& ctx = g.getGraphicsContext(); - if (! ensurePipeline (ctx, kWaveFrag)) - { - g.drawTexture (input, bounds); - return; - } - EffectParams p { param, 12.0f, (float) yup::Time::getMillisecondCounterHiRes() * 0.002f, (float) input->getWidth(), (float) input->getHeight(), 0, 0, 0 }; - drawPass (g, input, bounds, p); + return { param, 12.0f, (float) yup::Time::getMillisecondCounterHiRes() * 0.002f, (float) input.getWidth(), (float) input.getHeight(), 0, 0, 0 }; } private: @@ -472,18 +518,12 @@ void main() { public: SharpenEffect() { param = 4.0f; } - void apply (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) override + protected: + const char* getFragmentSource() const override { return kSharpenFrag; } + + EffectParams getEffectParams (const yup::GpuTexture& input) const override { - if (! input || ! input->isValid()) - return; - auto& ctx = g.getGraphicsContext(); - if (! ensurePipeline (ctx, kSharpenFrag)) - { - g.drawTexture (input, bounds); - return; - } - EffectParams p { param * 0.1f, (float) input->getWidth(), (float) input->getHeight(), 0, 0, 0, 0, 0 }; - drawPass (g, input, bounds, p); + return { param * 0.1f, (float) input.getWidth(), (float) input.getHeight(), 0, 0, 0, 0, 0 }; } private: @@ -517,18 +557,12 @@ void main() { public: CRTScanEffect() { param = 12.0f; } - void apply (yup::Graphics& g, yup::GpuTexture::Ptr input, yup::Rectangle bounds) override + protected: + const char* getFragmentSource() const override { return kCRTFrag; } + + EffectParams getEffectParams (const yup::GpuTexture& input) const override { - if (! input || ! input->isValid()) - return; - auto& ctx = g.getGraphicsContext(); - if (! ensurePipeline (ctx, kCRTFrag)) - { - g.drawTexture (input, bounds); - return; - } - EffectParams p { param * 0.02f, (float) input->getWidth(), (float) input->getHeight(), 0, 0, 0, 0, 0 }; - drawPass (g, input, bounds, p); + return { param * 0.02f, (float) input.getWidth(), (float) input.getHeight(), 0, 0, 0, 0, 0 }; } private: @@ -700,88 +734,45 @@ void main() { //============================================================================== void selectEffect() { - const auto id = effectCombo->getSelectedId(); background->setComponentEffect (nullptr); activeEffect = nullptr; + shaderEffect = nullptr; + compileErrorReported = false; - float defaultParam = 8.0f; + struct Choice + { + ShaderEffect* effect; + float defaultParam; + }; - switch (id) + const auto choice = [id = effectCombo->getSelectedId()]() -> Choice { - case 1: + switch (id) { - auto effect = new BlurEffect(); - activeEffect = yup::ReferenceCountedObjectPtr (effect); - defaultParam = 8.0f; - onEffectParamChanged = [effect] (float v) - { - effect->setEffectParameter (v); - }; - break; + case 1: + return { new BlurEffect(), 8.0f }; + case 2: + return { new PixelateEffect(), 8.0f }; + case 3: + return { new EdgeEffect(), 16.0f }; + case 4: + return { new WaveEffect(), 16.0f }; + case 5: + return { new SharpenEffect(), 8.0f }; + case 6: + return { new CRTScanEffect(), 12.0f }; + default: + return { nullptr, 8.0f }; } - case 2: - { - auto effect = new PixelateEffect(); - activeEffect = yup::ReferenceCountedObjectPtr (effect); - defaultParam = 8.0f; - onEffectParamChanged = [effect] (float v) - { - effect->setEffectParameter (v); - }; - break; - } - case 3: - { - auto effect = new EdgeEffect(); - activeEffect = yup::ReferenceCountedObjectPtr (effect); - defaultParam = 16.0f; - onEffectParamChanged = [effect] (float v) - { - effect->setEffectParameter (v); - }; - break; - } - case 4: - { - auto effect = new WaveEffect(); - activeEffect = yup::ReferenceCountedObjectPtr (effect); - defaultParam = 16.0f; - onEffectParamChanged = [effect] (float v) - { - effect->setEffectParameter (v); - }; - break; - } - case 5: - { - auto effect = new SharpenEffect(); - activeEffect = yup::ReferenceCountedObjectPtr (effect); - defaultParam = 8.0f; - onEffectParamChanged = [effect] (float v) - { - effect->setEffectParameter (v); - }; - break; - } - case 6: - { - auto effect = new CRTScanEffect(); - activeEffect = yup::ReferenceCountedObjectPtr (effect); - defaultParam = 12.0f; - onEffectParamChanged = [effect] (float v) - { - effect->setEffectParameter (v); - }; - break; - } - default: - break; - } + }(); - if (activeEffect != nullptr) + if (choice.effect != nullptr) { - effectParam = defaultParam; - onEffectParamChanged (effectParam); + shaderEffect = choice.effect; + activeEffect = yup::ComponentEffect::Ptr (choice.effect); + + effectParam = choice.defaultParam; + shaderEffect->setEffectParameter (effectParam); paramSlider->setValue ((double) effectParam, yup::dontSendNotification); background->setComponentEffect (activeEffect); } @@ -819,6 +810,11 @@ void main() { statusLabel->setText (text, yup::dontSendNotification); } + static int ticksToMicroseconds (yup::int64 ticks) + { + return static_cast (ticks * 1000000.0 / yup::Time::getHighResolutionTicksPerSecond()); + } + //============================================================================== yup::GraphicsContext* capturedContext = nullptr; @@ -834,8 +830,9 @@ void main() { std::unique_ptr spinner; std::unique_ptr snapshotPreview; - yup::ReferenceCountedObjectPtr activeEffect; - std::function onEffectParamChanged = [] (float) {}; + yup::ComponentEffect::Ptr activeEffect; + ShaderEffect* shaderEffect = nullptr; // Same object as activeEffect, kept for its stats. + bool compileErrorReported = false; float effectParam = 8.0f; YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentEffectsDemo) diff --git a/examples/graphics/source/examples/ComputeParticlesDemo.h b/examples/graphics/source/examples/ComputeParticlesDemo.h new file mode 100644 index 000000000..83232af22 --- /dev/null +++ b/examples/graphics/source/examples/ComputeParticlesDemo.h @@ -0,0 +1,674 @@ +/* + ============================================================================== + + 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 + +//============================================================================== + +/** + Demonstrates GPU compute-shader particle simulation rendered via GpuPipeline. + + A compute shader simulates 8192 particles on the GPU: particles explode + outward from the center, fall under gravity, bounce off the ground and + walls, and respawn when their lifetime expires. Each frame the particle + positions and colors are read back from the compute SSBO, assembled into + quad geometry on the CPU, and drawn with a soft-circle fragment shader + using additive blending. + + Requirements: + - A GpuDevice with compute shader support (Metal, D3D11, WebGPU, GL 4.3+) + - YUP_ENABLE_SHADER_TRANSPILER for online GLSL → native compilation + + @see GpuComputePipeline, GpuComputePass, GpuPipeline, GpuRenderPass +*/ +class ComputeParticlesDemo : public yup::Component +{ +public: + //============================================================================== + ComputeParticlesDemo() + : yup::Component ("ComputeParticlesDemo") + { + statusLabel = std::make_unique ("statusLabel"); + statusLabel->setText ("Initializing GPU compute...", yup::dontSendNotification); + addAndMakeVisible (statusLabel.get()); + + gravitySlider = std::make_unique (yup::Slider::LinearHorizontal); + gravitySlider->setRange (0.5, 8.0); + gravitySlider->setValue (3.5); + gravitySlider->onValueChanged = [this] (double v) + { + simGravity = (float) v; + gravityLabel->setText ("Gravity: " + yup::String (simGravity, 2), yup::dontSendNotification); + }; + addAndMakeVisible (gravitySlider.get()); + + gravityLabel = std::make_unique ("gravityLabel"); + gravityLabel->setText ("Gravity: 3.50", yup::dontSendNotification); + addAndMakeVisible (gravityLabel.get()); + } + + ~ComputeParticlesDemo() override + { + } + + //============================================================================== + void paint (yup::Graphics& g) override + { + g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::darkslategray)); + g.fillAll(); + + if (capturedContext == nullptr) + { + capturedContext = &g.getGraphicsContext(); + initGpu(); + } + + if (! gpuReady) + return; + + auto bounds = getLocalBounds().to().reduced (10.0f); + auto particleBounds = bounds; + particleBounds.removeFromBottom (60.0f); + + const int w = yup::roundToInt (particleBounds.getWidth()); + const int h = yup::roundToInt (particleBounds.getHeight()); + + if (w < 2 || h < 2) + return; + + yup::GpuTexture::Ptr outputTex = simulateAndRender (w, h); + + if (outputTex != nullptr) + g.drawTexture (outputTex, particleBounds); + } + + //============================================================================== + void resized() override + { + auto bounds = getLocalBounds().to().reduced (10.0f); + + statusLabel->setBounds (bounds.removeFromBottom (25.0f)); + + auto sliderBounds = bounds.removeFromBottom (30.0f); + gravityLabel->setBounds (sliderBounds.removeFromLeft (80.0f)); + gravitySlider->setBounds (sliderBounds); + } + + //============================================================================== + void visibilityChanged() override + { + if (isVisible()) + { + capturedContext = nullptr; + computePipeline = nullptr; + renderPipeline = nullptr; + renderTarget = nullptr; + particleSSBO = nullptr; + gpuReady = false; + } + } + + void refreshDisplay (double /*lastFrameTimeSeconds*/) override + { + if (gpuReady) + repaint(); + } + +private: + //============================================================================== + /** One particle as laid out in the GPU SSBO (std430). + + Layout (48 bytes total, 16-byte aligned): + offset 0: vec2 position (8 bytes) + offset 8: vec2 velocity (8 bytes) + offset 16: vec4 color (16 bytes) + offset 32: float lifetime (4 bytes) + offset 36: float age (4 bytes) + -- 8 bytes implicit tail padding to align struct size to 16 -- + */ + static constexpr int kParticleGpuStrideFloats = 12; // 48 bytes / 4 + + /** One vertex for the render pipeline (quad corner). + + Layout (40 bytes): + offset 0: vec2 center (location 0, float2) + offset 8: vec2 offset (location 1, float2) + offset 16: vec4 color (location 2, float4) + offset 32: vec2 size (location 3, float2) + */ + static constexpr int kVertexStrideFloats = 10; // 40 bytes / 4 + static constexpr int kVerticesPerParticle = 6; // 2 triangles + + static constexpr int kParticleCount = 8192; + static constexpr int kWorkgroupSize = 256; + static constexpr int kWorkgroupCount = kParticleCount / kWorkgroupSize; // 32 + + // Offsets for extracting particle data from the raw GPU readback array. + static constexpr int kGpuPosX = 0; + static constexpr int kGpuPosY = 1; + static constexpr int kGpuColR = 4; + static constexpr int kGpuColG = 5; + static constexpr int kGpuColB = 6; + static constexpr int kGpuColA = 7; + + // Quad corner offsets centred at the origin (in particle-local space). + static constexpr float kQuadOffsets[kVerticesPerParticle * 2] = { + -0.5f, + -0.5f, + 0.5f, + -0.5f, + 0.5f, + 0.5f, + -0.5f, + -0.5f, + 0.5f, + 0.5f, + -0.5f, + 0.5f + }; + + //============================================================================== + /** GLSL 450 compute shader: particle physics simulation. + + Particle data is stored as a flat float array in an SSBO to avoid + struct-based layouts that can confuse the Metal transpiler's + binding reflection. Each particle occupies 12 floats (48 bytes): + offset 0: posX, posY + offset 2: velX, velY + offset 4: colR, colG, colB, colA + offset 8: lifetime + offset 9: age + offset 10-11: padding (unused) + */ + /** GLSL 450 compute shader: particle physics simulation. + + Each particle occupies 12 floats (48 bytes in std430): + offset 0: posX, posY + offset 2: velX, velY + offset 4: colR, colG, colB, colA + offset 8: lifetime + offset 9: age + offset 10-11: padding (unused) + + Bindings: UBO at (0,0), SSBO at (0,1) — matching Metal's + declaration-order buffer indexing. + */ + static constexpr const char kComputeSource[] = R"glsl(#version 450 +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout(std140, set = 0, binding = 0) uniform Params { + float deltaTime; + float gravity; + float restitution; + float particleCountF; + float simLeft; + float simRight; + float simBottom; + float simTop; +} params; + +layout(std430, set = 0, binding = 1) buffer ParticleBuffer { + float data[]; +}; + +// Simple hash function for pseudo-random numbers per particle. +uint wangHash(uint seed) { + seed = (seed ^ 61u) ^ (seed >> 16u); + seed *= 9u; + seed = seed ^ (seed >> 4u); + seed *= 0x27d4eb2du; + seed = seed ^ (seed >> 15u); + return seed; +} + +void main() { + uint idx = gl_GlobalInvocationID.x; + uint particleCount = uint(params.particleCountF); + if (idx >= particleCount) + return; + + uint base = idx * 12u; + + // Read particle state from the flat array. + vec2 pos = vec2(data[base + 0u], data[base + 1u]); + vec2 vel = vec2(data[base + 2u], data[base + 3u]); + vec4 col = vec4(data[base + 4u], data[base + 5u], data[base + 6u], data[base + 7u]); + float lifetime = data[base + 8u]; + float age = data[base + 9u]; + + // Age the particle. + age += params.deltaTime; + + // Respawn when lifetime expires — explode outward from the centre. + if (age >= lifetime) { + float angle = float(wangHash(idx * 2u + uint(age * 1000.0))) / float(0xFFFFFFFFu) * 6.283185307; + float speed = float(wangHash(idx * 3u)) / float(0xFFFFFFFFu) * 1.8 + 0.2; + + pos = vec2(0.0, params.simBottom + 1.4); + vel = vec2(cos(angle), sin(angle)) * speed; + age = 0.0; + lifetime = float(wangHash(idx * 5u)) / float(0xFFFFFFFFu) * 1.5 + 0.3; + + // Random saturated color. + uint cr = wangHash(idx * 7u); + uint cg = wangHash(idx * 11u); + uint cb = wangHash(idx * 13u); + col = vec4( + float(cr & 0xFFu) / 255.0, + float(cg & 0xFFu) / 255.0, + float(cb & 0xFFu) / 255.0, + 1.0 + ); + } + + // Apply gravity. + vel.y -= params.gravity * params.deltaTime; + + // Integrate position. + pos += vel * params.deltaTime; + + // Bounce off ground. + if (pos.y < params.simBottom) { + pos.y = params.simBottom; + vel.y = abs(vel.y) * params.restitution; + vel.x *= 0.92; + } + + // Bounce off side walls (viewport edges). + if (pos.x < params.simLeft) { + vel.x = abs(vel.x) * 0.7; + pos.x = params.simLeft; + } + if (pos.x > params.simRight) { + vel.x = -abs(vel.x) * 0.7; + pos.x = params.simRight; + } + + // Write back to the flat array. + data[base + 0u] = pos.x; + data[base + 1u] = pos.y; + data[base + 2u] = vel.x; + data[base + 3u] = vel.y; + data[base + 4u] = col.r; + data[base + 5u] = col.g; + data[base + 6u] = col.b; + data[base + 7u] = col.a; + data[base + 8u] = lifetime; + data[base + 9u] = age; +} +)glsl"; + + //============================================================================== + /** GLSL 450 vertex shader: expands each vertex into a clip-space quad corner. */ + static constexpr const char kRenderVertSource[] = R"glsl(#version 450 +layout(location = 0) in vec2 aCenter; +layout(location = 1) in vec2 aOffset; +layout(location = 2) in vec4 aColor; +layout(location = 3) in vec2 aSize; + +layout(location = 0) out vec4 vColor; +layout(location = 1) out vec2 vOffset; + +void main() { + gl_Position = vec4(aCenter + aOffset * aSize, 0.0, 1.0); + vColor = aColor; + vOffset = aOffset; +} +)glsl"; + + //============================================================================== + /** GLSL 450 fragment shader: hard opaque circle, no blending. + + vOffset is the raw quad corner offset in [-0.5, 0.5]. + Multiply by 2 to normalise to [-1, 1] for a correct + unit-circle distance test that works with any viewport aspect. */ + static constexpr const char kRenderFragSource[] = R"glsl(#version 450 +layout(location = 0) in vec4 vColor; +layout(location = 1) in vec2 vOffset; + +layout(location = 0) out vec4 outColor; + +void main() { + // Raw offset is always [-0.5, 0.5] on both axes regardless of + // viewport aspect compensation. Normalise to [-1, 1] for a true circle. + float dist = length(vOffset * 2.0); + if (dist > 1.0) + discard; + outColor = vColor; +} +)glsl"; + + //============================================================================== + void initGpu() + { + if (capturedContext == nullptr) + return; + + auto device = capturedContext->getGpuDevice(); + + if (! device->isComputeAvailable()) + { + statusLabel->setText ("Compute shaders not available on this GPU backend.", yup::dontSendNotification); + YUP_DBG ("Compute shaders not available on this GPU backend."); + return; + } + + // Compile the compute pipeline from GLSL. + yup::String glslSource = yup::String::fromUTF8 (kComputeSource, sizeof (kComputeSource) - 1); + +#if YUP_ENABLE_SHADER_TRANSPILER + yup::GpuWorkgroupSize wgs { (uint32_t) kWorkgroupSize, 1, 1 }; + auto computeResult = yup::GpuComputePipeline::compileFromGlsl (device, glslSource, wgs); + + if (computeResult.failed()) + { + statusLabel->setText ("Compute shader compile failed: " + computeResult.getErrorMessage().substring (0, 60), + yup::dontSendNotification); + YUP_DBG ("Compute shader compile failed: " << computeResult.getErrorMessage()); + return; + } + + computePipeline = computeResult.getValue(); +#else + statusLabel->setText ("Shader transpiler not available (YUP_ENABLE_SHADER_TRANSPILER).", yup::dontSendNotification); + YUP_DBG ("Shader transpiler not available (YUP_ENABLE_SHADER_TRANSPILER)."); + return; +#endif + + // Compile the render pipeline. + yup::String vertSource = yup::String::fromUTF8 (kRenderVertSource, sizeof (kRenderVertSource) - 1); + yup::String fragSource = yup::String::fromUTF8 (kRenderFragSource, sizeof (kRenderFragSource) - 1); + + // Vertex buffer layout: 4 attributes, 40-byte stride. + static const yup::GpuVertexAttribute kVertexAttrs[] = { + { yup::GpuVertexFormat::float2, 0, 0 }, // center + { yup::GpuVertexFormat::float2, 8, 1 }, // offset + { yup::GpuVertexFormat::float4, 16, 2 }, // color + { yup::GpuVertexFormat::float2, 32, 3 }, // size (x,y) + }; + + static const yup::GpuVertexBufferLayout kVertexLayout = { + kVertexStrideFloats * (uint32_t) sizeof (float), + yup::GpuVertexStepMode::vertex, + kVertexAttrs, + (uint32_t) yup::numElementsInArray (kVertexAttrs) + }; + + yup::GpuPipelineOptions pipelineOpts; + pipelineOpts.vertexBuffers = &kVertexLayout; + pipelineOpts.vertexBufferCount = 1; + pipelineOpts.topology = yup::GpuPrimitiveTopology::triangleList; + pipelineOpts.cullMode = yup::GpuCullMode::none; + pipelineOpts.colorTargets[0].blendEnabled = false; + pipelineOpts.colorTargetCount = 1; + + auto renderResult = yup::GpuPipeline::compileFromGlsl (device, vertSource, fragSource, pipelineOpts); + if (renderResult.failed()) + { + statusLabel->setText ("Render shader compile failed: " + renderResult.getErrorMessage().substring (0, 60), + yup::dontSendNotification); + YUP_DBG ("Render shader compile failed: " << renderResult.getErrorMessage()); + return; + } + + renderPipeline = renderResult.getValue(); + + // Allocate CPU-side readback and vertex buffers. + const size_t readbackSize = (size_t) kParticleCount * (size_t) kParticleGpuStrideFloats; + cpuParticleData.resize (readbackSize, 0.0f); + + // Pre-seed particle data. + yup::Random rng; + for (int i = 0; i < kParticleCount; ++i) + { + const size_t base = (size_t) i * (size_t) kParticleGpuStrideFloats; + // Position: random within sim space, biased toward centre. + cpuParticleData[base + kGpuPosX] = rng.nextFloat() * 2.0f - 1.0f; + cpuParticleData[base + kGpuPosY] = rng.nextFloat() * 1.5f - 0.5f; + // Velocity: zero (compute shader will set this on respawn). + cpuParticleData[base + 2] = 0.0f; + cpuParticleData[base + 3] = 0.0f; + // Color: bright, fully opaque. + cpuParticleData[base + kGpuColR] = rng.nextFloat(); + cpuParticleData[base + kGpuColG] = rng.nextFloat(); + cpuParticleData[base + kGpuColB] = rng.nextFloat(); + cpuParticleData[base + kGpuColA] = 1.0f; + // Lifetime = 0, age = 0 → triggers immediate respawn in compute. + cpuParticleData[base + 8] = 0.0f; + cpuParticleData[base + 9] = 0.0f; + } + + // Create a persistent SSBO with initial particle data. + const size_t readbackBytes = cpuParticleData.size() * sizeof (float); + particleSSBO = device->createBuffer (yup::GpuBufferType::storage, + cpuParticleData.data(), + readbackBytes); + + // Pre-allocate vertex buffer at max capacity. + const size_t vertexDataSize = (size_t) kParticleCount * (size_t) kVerticesPerParticle * (size_t) kVertexStrideFloats; + cpuVertexData.resize (vertexDataSize, 0.0f); + + const size_t vertexBytes = vertexDataSize * sizeof (float); + particleVBO = yup::GpuBuffer::create (device, yup::GpuBufferType::vertex, cpuVertexData.data(), vertexBytes); + + statusLabel->setText (yup::String::formatted ("GPU compute particles | %d particles | %d workgroups", + kParticleCount, + kWorkgroupCount), + yup::dontSendNotification); + + lastFrameStamp = yup::Time::getHighResolutionTicks(); + frameCount = 0; + snapshotCount = 0; + fpsUpdateAccum = 0.0; + gpuReady = true; + } + + //============================================================================== + /** Runs one frame of compute + render and returns the rendered texture. */ + yup::GpuTexture::Ptr simulateAndRender (int viewW, int viewH) + { + if (! gpuReady || computePipeline == nullptr || renderPipeline == nullptr) + return nullptr; + + auto device = capturedContext->getGpuDevice(); + + // ---- Compute pass: simulate particles --------------------------------- + const auto now = yup::Time::getHighResolutionTicks(); + const float deltaTime = yup::Time::highResolutionTicksToSeconds (now - lastFrameStamp); + lastFrameStamp = now; + + const float clampedDt = yup::jmin (deltaTime, 0.1f); + + if (particleSSBO == nullptr) + return nullptr; + + const size_t readbackBytes = cpuParticleData.size() * sizeof (float); + + struct alignas (16) ComputeParams + { + float deltaTime; + float gravity; + float restitution; + float particleCountF; + float simLeft; + float simRight; + float simBottom; + float simTop; + }; + + // Compute sim boundaries from the viewport aspect ratio so the + // simulation fills the full area with uniform scale (circular particles). + const float viewAspect = (float) viewW / (float) yup::jmax ((float) viewH, 1.0f); + constexpr float baseSimHeight = 2.5f; // fixed vertical range + const float simWidth = baseSimHeight * viewAspect; + const float simLeft = -simWidth * 0.5f; + const float simRight = simWidth * 0.5f; + constexpr float simBottom = -1.0f; + constexpr float simTop = 1.5f; + + ComputeParams cparams { clampedDt, simGravity, 0.45f, (float) kParticleCount, simLeft, simRight, simBottom, simTop }; + + { + auto pass = yup::GpuComputePass::begin (device); + if (pass.isValid()) + { + pass.setPipeline (computePipeline); + pass.setStorageBuffer (0, 1, particleSSBO); + pass.setUniformBuffer (0, 0, &cparams, sizeof (cparams)); + pass.dispatch ((uint32_t) kWorkgroupCount, 1, 1); + pass.finish(); + } + } + + // Pull the latest particle snapshot. On backends that cannot map a buffer + // synchronously (WebGPU) this is pipelined, so it trails the GPU by a frame + // or two and returns false on frames where nothing new landed. + // cpuParticleData then still holds the previous snapshot, so keep drawing + // it instead of dropping the frame. + if (device->readBuffer (particleSSBO, cpuParticleData.data(), readbackBytes)) + ++snapshotCount; + + // ---- Build vertex buffer from particle data --------------------------- + const float sizeY = 20.0f / (float) yup::jmax (viewH, 1); + const float sizeX = sizeY * (float) viewH / (float) yup::jmax (viewW, 1); + + // Map sim space to clip space. + const float simMidX = (simLeft + simRight) * 0.5f; + const float simMidY = (simBottom + simTop) * 0.5f; + const float scaleX = 2.0f / (simRight - simLeft); + const float scaleY = 2.0f / (simTop - simBottom); + + const int totalVertices = kParticleCount * kVerticesPerParticle; + float* vtx = cpuVertexData.data(); + + for (int p = 0; p < kParticleCount; ++p) + { + const int base = p * kParticleGpuStrideFloats; + const float px = cpuParticleData[(size_t) base + kGpuPosX]; + const float py = cpuParticleData[(size_t) base + kGpuPosY]; + + const float cx = (px - simMidX) * scaleX; + const float cy = (py - simMidY) * scaleY; + + const float cr = cpuParticleData[(size_t) base + kGpuColR]; + const float cg = cpuParticleData[(size_t) base + kGpuColG]; + const float cb = cpuParticleData[(size_t) base + kGpuColB]; + const float ca = cpuParticleData[(size_t) base + kGpuColA]; + + for (int v = 0; v < kVerticesPerParticle; ++v) + { + *vtx++ = cx; + *vtx++ = cy; + *vtx++ = kQuadOffsets[v * 2 + 0]; + *vtx++ = kQuadOffsets[v * 2 + 1]; + *vtx++ = cr; + *vtx++ = cg; + *vtx++ = cb; + *vtx++ = ca; + *vtx++ = sizeX; + *vtx++ = sizeY; + } + } + + const size_t vertexBytes = (size_t) totalVertices * kVertexStrideFloats * sizeof (float); + + // ---- Render pass ------------------------------------------------------ + if (! device->updateBuffer (particleVBO, cpuVertexData.data(), vertexBytes)) + { + statusLabel->setText ("VBO update failed!", yup::dontSendNotification); + YUP_DBG ("VBO update failed!"); + return nullptr; + } + + // Create or resize render target. + if (renderTarget == nullptr || renderTarget->getWidth() != viewW || renderTarget->getHeight() != viewH) + renderTarget = yup::GpuTarget::create (device, viewW, viewH); + + if (renderTarget == nullptr) + { + statusLabel->setText ("Render target creation failed!", yup::dontSendNotification); + YUP_DBG ("Render target creation failed!"); + return nullptr; + } + + { + auto frame = yup::GpuFrame::begin (device); + + auto pass = renderTarget->beginRenderPass (frame, { true, yup::GpuColor::transparentBlack() }); + pass.setPipeline (renderPipeline); + pass.setVertexBuffer (0, particleVBO); + pass.draw ((uint32_t) totalVertices); + pass.finish(); + + frame.submit(); + } + + frameCount++; + fpsUpdateAccum += (double) clampedDt; + + if (fpsUpdateAccum >= 0.25) + { + statusLabel->setText (yup::String::formatted ("GPU compute | %d particles | f=%d | s=%d | p0=(%.2f,%.2f) | g=%.2f", + kParticleCount, + frameCount, + snapshotCount, + (double) cpuParticleData[(size_t) kGpuPosX], + (double) cpuParticleData[(size_t) kGpuPosY], + (double) simGravity), + yup::dontSendNotification); + fpsUpdateAccum = 0.0; + } + + return renderTarget->asTexture(); + } + + //============================================================================== + yup::GraphicsContext* capturedContext = nullptr; + + // GPU resources. + yup::GpuComputePipeline::Ptr computePipeline; + yup::GpuPipeline::Ptr renderPipeline; + yup::GpuBuffer::Ptr particleSSBO; + yup::GpuBuffer::Ptr particleVBO; + yup::GpuTarget::Ptr renderTarget; + + // CPU-side data. + std::vector cpuParticleData; + std::vector cpuVertexData; + + // Simulation state. + float simGravity = 3.5f; + yup::int64 lastFrameStamp = 0; + bool gpuReady = false; + + // FPS counter. snapshotCount tracks how many readbacks actually landed, which + // on async-readback backends is lower than frameCount. + int frameCount = 0; + int snapshotCount = 0; + double fpsUpdateAccum = 0.0; + + // UI. + std::unique_ptr gravitySlider; + std::unique_ptr gravityLabel; + std::unique_ptr statusLabel; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComputeParticlesDemo) +}; diff --git a/examples/graphics/source/examples/GpuAudioProcessingDemo.h b/examples/graphics/source/examples/GpuAudioProcessingDemo.h new file mode 100644 index 000000000..41e5a3ba1 --- /dev/null +++ b/examples/graphics/source/examples/GpuAudioProcessingDemo.h @@ -0,0 +1,545 @@ +/* + ============================================================================== + + 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 +#include + +//============================================================================== + +/** + Renders the GPU output peak meter on its own 60fps timer, independent of + the rest of GpuAudioProcessingDemo's UI repaint cycle. +*/ +class GpuPeakMeterComponent : public yup::Component +{ +public: + GpuPeakMeterComponent() + : yup::Component ("GpuPeakMeterComponent") + { + } + + void setPeakLevel (float newPeak) noexcept + { + peakLevel.store (newPeak, std::memory_order_relaxed); + } + + void refreshDisplay (double /*lastFrameTimeSeconds*/) override + { + repaint(); + } + + void paint (yup::Graphics& g) override + { + auto bounds = getLocalBounds().to(); + + g.setFillColor (yup::Colors::darkgreen); + g.fillRect (bounds); + + const auto peakWidth = bounds.getWidth() * peakLevel.load (std::memory_order_relaxed) * 4.0f; + g.setFillColor (yup::Colors::lime); + g.fillRect (bounds.withWidth (peakWidth)); + } + +private: + std::atomic peakLevel { 0.0f }; +}; + +//============================================================================== + +/** + Demonstrates GPU-accelerated audio effect processing using compute shaders. + + Plays a looped audio file through a GPU compute shader (gain + soft clip). + The compute pipeline is compiled from GLSL at runtime and dispatched each + audio callback block. GPU readback uses a synchronous staging path per + backend. + + Requirements: + - A GpuDevice with compute shader support (Metal, D3D11, WebGPU, GL 4.3+) + - YUP_ENABLE_SHADER_TRANSPILER for online GLSL→native compilation + - An audio file at examples/graphics/data/break_boomblastic_92bpm.mp3 +*/ +class GpuAudioProcessingDemo : public yup::Component + , public yup::AudioIODeviceCallback + , private yup::AsyncUpdater +{ +public: + //============================================================================== + GpuAudioProcessingDemo() + : yup::Component ("GpuAudioProcessingDemo") +#if YUP_ENABLE_SHADER_TRANSPILER + , currentGlslSource (yup::String::fromUTF8 (kDefaultGlslSource, sizeof (kDefaultGlslSource) - 1)) +#endif + { + loadAudioFile(); + + peakMeter = std::make_unique(); + addAndMakeVisible (peakMeter.get()); + + gainSlider = std::make_unique (yup::Slider::LinearHorizontal); + gainSlider->setRange (0.0, 4.0); + gainSlider->setValue (3.0); + gainSlider->onValueChanged = [this] (double v) + { + gain = (float) v; + }; + addAndMakeVisible (gainSlider.get()); + + gainLabel = std::make_unique ("gainLabel"); + gainLabel->setText ("Gain: 3.00", yup::dontSendNotification); + addAndMakeVisible (gainLabel.get()); + + mixSlider = std::make_unique (yup::Slider::LinearHorizontal); + mixSlider->setRange (0.0, 1.0); + mixSlider->setValue (1.0); + mixSlider->onValueChanged = [this] (double v) + { + mix = (float) v; + }; + addAndMakeVisible (mixSlider.get()); + + mixLabel = std::make_unique ("mixLabel"); + mixLabel->setText ("Mix: 1.00", yup::dontSendNotification); + addAndMakeVisible (mixLabel.get()); + + statusLabel = std::make_unique ("status"); + statusLabel->setText ("Initializing audio and GPU...", yup::dontSendNotification); + addAndMakeVisible (statusLabel.get()); + +#if YUP_ENABLE_SHADER_TRANSPILER + shaderEditor = std::make_unique ("shaderEditor"); + shaderEditor->setMultiLine (true); + shaderEditor->setReadOnly (false); + shaderEditor->setText (currentGlslSource, yup::dontSendNotification); + shaderEditor->onTextChange = [this] + { + currentGlslSource = shaderEditor->getText(); + }; + addAndMakeVisible (shaderEditor.get()); + + recompileButton = std::make_unique ("Recompile"); + recompileButton->onClick = [this] + { + recompileShader(); + }; + addAndMakeVisible (recompileButton.get()); + + compileStatusLabel = std::make_unique ("compileStatus"); + compileStatusLabel->setText ("Shader: default GLSL", yup::dontSendNotification); + addAndMakeVisible (compileStatusLabel.get()); +#endif + + yup::MessageManager::callAsync ([this] + { + initAudio(); + }); + } + + ~GpuAudioProcessingDemo() override + { + deviceManager.removeAudioCallback (this); + } + + //============================================================================== + // AudioIODeviceCallback + //============================================================================== + + void audioDeviceAboutToStart (yup::AudioIODevice* device) override + { + if (audioBuffer.getNumSamples() == 0) + return; + + gpuBlockSize = yup::jmin (device->getCurrentBufferSizeSamples(), kMaxGpuBlockSize); + + if (computeDevice == nullptr) + { + const yup::GpuPlatform platforms[] = { +#if YUP_MAC || YUP_IOS + yup::GpuPlatform::Metal, +#else +#if YUP_WINDOWS + yup::GpuPlatform::Direct3D, +#endif + yup::GpuPlatform::OpenGL, +#endif + }; + + for (auto plat : platforms) + { + yup::GpuDevice::Options opts; + opts.allowHeadlessRendering = true; + + computeDevice = yup::GpuDevice::create (plat, opts); + if (computeDevice != nullptr && computeDevice->isComputeAvailable()) + break; + + computeDevice = nullptr; + } + } + + if (computeDevice == nullptr || ! computeDevice->isComputeAvailable()) + return; + + const auto bufBytes = static_cast (gpuBlockSize) * sizeof (float); + std::vector zeroData (static_cast (gpuBlockSize), 0.0f); + + for (int i = 0; i < kRingSize; ++i) + { + gpuInputBuf[i] = computeDevice->createBuffer (yup::GpuBufferType::storage, zeroData.data(), bufBytes); + gpuOutputBuf[i] = computeDevice->createBuffer (yup::GpuBufferType::storage, zeroData.data(), bufBytes); + cpuUploadBuf[i].resize (static_cast (gpuBlockSize)); + cpuOutputBuf[i].resize (static_cast (gpuBlockSize)); + } + + writePos = 0; + + recompileShader(); + triggerAsyncUpdate(); + } + + void audioDeviceStopped() override + { + computePipeline = nullptr; + + for (int i = 0; i < kRingSize; ++i) + { + gpuInputBuf[i] = nullptr; + gpuOutputBuf[i] = nullptr; + cpuUploadBuf[i].clear(); + cpuOutputBuf[i].clear(); + } + + computeDevice = nullptr; + + triggerAsyncUpdate(); + } + + void audioDeviceIOCallbackWithContext (const float* const* /*inputChannelData*/, + int /*numInputChannels*/, + float* const* outputChannelData, + int numOutputChannels, + int numSamples, + const yup::AudioIODeviceCallbackContext&) override + { + for (int ch = 0; ch < numOutputChannels; ++ch) + { + if (outputChannelData[ch] != nullptr) + yup::FloatVectorOperations::clear (outputChannelData[ch], numSamples); + } + + if (numOutputChannels == 0 || audioBuffer.getNumSamples() == 0) + return; + + const int totalSamples = audioBuffer.getNumSamples(); + const int numChannels = audioBuffer.getNumChannels(); + const int processSamples = yup::jmin (numSamples, gpuBlockSize); + const int slot = writePos % kRingSize; + + // Build input from the looped audio file into the preallocated CPU buffer. + auto& inputBuf = cpuUploadBuf[slot]; + for (int i = 0; i < processSamples; ++i) + { + float audioSample = 0.0f; + + if (numChannels == 1) + audioSample = audioBuffer.getSample (0, readPosition); + else + for (int ch = 0; ch < yup::jmin (2, numChannels); ++ch) + audioSample += audioBuffer.getSample (ch, readPosition); + + audioSample /= yup::jmin (2, numChannels); + + readPosition++; + if (readPosition >= totalSamples) + readPosition = 0; + + inputBuf[static_cast (i)] = audioSample; + } + + if (computePipeline != nullptr && computeDevice != nullptr) + { + // Write fresh data into the preallocated ring buffers in place — + // no GPU allocation happens on the audio thread. + computeDevice->updateBuffer (gpuInputBuf[slot], inputBuf.data(), inputBuf.size() * sizeof (float)); + + Params params { gain, mix, 0.0f, 0.0f }; + + uint32_t workgroupsX = (static_cast (processSamples) + 255) / 256; + + auto pass = yup::GpuComputePass::begin (computeDevice); + if (pass.isValid()) + { + pass.setPipeline (computePipeline); + pass.setStorageBuffer (0, 0, gpuInputBuf[slot]); + pass.setStorageBuffer (0, 1, gpuOutputBuf[slot]); + pass.setUniformBuffer (0, 2, ¶ms, sizeof (params)); + pass.dispatch (workgroupsX, 1, 1); + pass.finish(); + } + + // Read back from TWO slots ago — the GPU has had two full audio + // callbacks to finish its work. No blocking wait needed. + const int readSlot = (writePos + kRingSize - kReadLatency) % kRingSize; + auto& outputBuf = cpuOutputBuf[readSlot]; + + if (writePos >= kReadLatency + && computeDevice->readBuffer (gpuOutputBuf[readSlot], outputBuf.data(), outputBuf.size() * sizeof (float))) + { + for (int ch = 0; ch < numOutputChannels; ++ch) + yup::FloatVectorOperations::copy (outputChannelData[ch], outputBuf.data(), processSamples); + + float peak = 0.0f; + for (int i = 0; i < processSamples; ++i) + peak = yup::jmax (peak, std::abs (outputBuf[static_cast (i)])); + lastPeakOutput = peak; + peakMeter->setPeakLevel (peak); + } + else + { + // Pipeline still filling — output silence. + lastPeakOutput = 0.0f; + peakMeter->setPeakLevel (0.0f); + } + } + else + { + // No GPU: passthrough the looped file. + for (int ch = 0; ch < numOutputChannels; ++ch) + yup::FloatVectorOperations::copy (outputChannelData[ch], inputBuf.data(), processSamples); + + lastPeakOutput = 0.0f; + peakMeter->setPeakLevel (0.0f); + } + + writePos++; + } + + //============================================================================== + // Component overrides + //============================================================================== + + void paint (yup::Graphics& g) override + { + g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::darkslategray)); + g.fillAll(); + } + + void resized() override + { + auto bounds = getLocalBounds().to().reduced (10.0f); + + peakMeter->setBounds (bounds.removeFromTop (30.0f)); + bounds.removeFromTop (4.0f); + + statusLabel->setBounds (bounds.removeFromTop (22.0f)); + + auto gainRow = bounds.removeFromTop (28.0f); + gainLabel->setBounds (gainRow.removeFromLeft (60.0f)); + gainSlider->setBounds (gainRow); + + bounds.removeFromTop (4.0f); + + auto mixRow = bounds.removeFromTop (28.0f); + mixLabel->setBounds (mixRow.removeFromLeft (60.0f)); + mixSlider->setBounds (mixRow); + + bounds.removeFromTop (8.0f); + +#if YUP_ENABLE_SHADER_TRANSPILER + compileStatusLabel->setBounds (bounds.removeFromTop (22.0f)); + bounds.removeFromTop (4.0f); + recompileButton->setBounds (bounds.removeFromTop (26.0f)); + bounds.removeFromTop (4.0f); + shaderEditor->setBounds (bounds); +#endif + } + +private: + //============================================================================== + + void loadAudioFile() + { + auto dataDir = yup::File (__FILE__).getParentDirectory().getParentDirectory().getParentDirectory().getChildFile ("data"); + + yup::File audioFile = dataDir.getChildFile ("break_boomblastic_92bpm.mp3"); + if (! audioFile.existsAsFile()) + return; + + yup::AudioFormatManager formatManager; + formatManager.registerDefaultFormats(); + + if (auto reader = formatManager.createReaderFor (audioFile)) + { + audioBuffer.setSize ((int) reader->numChannels, (int) reader->lengthInSamples); + reader->read (&audioBuffer, 0, (int) reader->lengthInSamples, 0, true, true); + } + } + + void initAudio() + { + auto result = deviceManager.initialiseWithDefaultDevices (0, 2); + if (result.isNotEmpty()) + { + statusLabel->setText ("Audio init failed: " + result, yup::dontSendNotification); + return; + } + + deviceManager.addAudioCallback (this); + statusLabel->setText ("Audio + GPU compute active.", yup::dontSendNotification); + } + + //============================================================================== + + void visibilityChanged() override + { + if (! isVisible()) + deviceManager.removeAudioCallback (this); + else + deviceManager.addAudioCallback (this); + } + + void handleAsyncUpdate() override + { + gainLabel->setText ("Gain: " + yup::String (gain, 2), yup::dontSendNotification); + mixLabel->setText ("Mix: " + yup::String (mix, 2), yup::dontSendNotification); + repaint(); + } + + void recompileShader() + { + if (computeDevice == nullptr || ! computeDevice->isComputeAvailable()) + return; + +#if YUP_ENABLE_SHADER_TRANSPILER + yup::String glslSource = currentGlslSource.isEmpty() + ? yup::String::fromUTF8 (kDefaultGlslSource, sizeof (kDefaultGlslSource) - 1) + : currentGlslSource; + + auto result = yup::GpuComputePipeline::compileFromGlsl (computeDevice, glslSource); + if (result.wasOk()) + { + computePipeline = result.getValue(); + compileStatusLabel->setText ("Shader: compiled OK", yup::dontSendNotification); + } + else + { + compileStatusLabel->setText ("Shader compile error: " + result.getErrorMessage().substring (0, 80), + yup::dontSendNotification); + } +#else + compileStatusLabel->setText ("Shader transpiler not available.", yup::dontSendNotification); +#endif + } + + //============================================================================== + + static constexpr const char kDefaultGlslSource[] = R"glsl(#version 450 +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout(std430, set = 0, binding = 0) buffer InputBuf { + float inputData[]; +}; + +layout(std430, set = 0, binding = 1) buffer OutputBuf { + float outputData[]; +}; + +layout(std140, set = 0, binding = 2) uniform Params { + float gain; + float mix; + float pad0; + float pad1; +} params; + +float excite(float x, float g) +{ + float y = x; + y = tanh(g * y); + y += 0.20 * sin(7.0 * y); + y += 0.08 * sin(19.0 * y); + y *= 1.0 + 0.3 * abs(y); + y = y / (1.0 + abs(y)); + return y; +} + +void main() +{ + uint idx = gl_GlobalInvocationID.x; + + float s = inputData[idx]; + float d = excite(s, params.gain); + + outputData[idx] = d * params.mix + s * (1.0 - params.mix); +} +)glsl"; + + //============================================================================== + + struct alignas (16) Params + { + float gainVal; + float mixVal; + float pad0; + float pad1; + }; + + static constexpr int kMaxGpuBlockSize = 4096; + static constexpr int kRingSize = 4; + static constexpr int kReadLatency = 2; + + // Audio file playback. + yup::AudioDeviceManager deviceManager; + yup::AudioBuffer audioBuffer; + int readPosition = 0; + int gpuBlockSize = 0; + + // GPU compute. + yup::GpuDevice::Ptr computeDevice; + yup::GpuComputePipeline::Ptr computePipeline; + yup::GpuBuffer::Ptr gpuInputBuf[kRingSize]; + yup::GpuBuffer::Ptr gpuOutputBuf[kRingSize]; + std::vector cpuUploadBuf[kRingSize]; + std::vector cpuOutputBuf[kRingSize]; + int writePos = 0; + + // Parameters. + float gain = 3.0f; + float mix = 1.0f; + + // UI. + float lastPeakOutput = 0.0f; + std::unique_ptr peakMeter; + std::unique_ptr gainSlider; + std::unique_ptr gainLabel; + std::unique_ptr mixSlider; + std::unique_ptr mixLabel; + std::unique_ptr statusLabel; + +#if YUP_ENABLE_SHADER_TRANSPILER + yup::String currentGlslSource; + std::unique_ptr shaderEditor; + std::unique_ptr recompileButton; + std::unique_ptr compileStatusLabel; +#endif +}; diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 50faf849f..b9b2de1a2 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -69,10 +69,12 @@ inline yup::File getAssetPath (yup::StringRef subPath = {}) #include "examples/ClipboardDemo.h" #include "examples/ColorLab.h" #include "examples/ComponentEffectsDemo.h" +#include "examples/ComputeParticlesDemo.h" #include "examples/ConvolutionDemo.h" #include "examples/CrossoverDemo.h" #include "examples/FileChooser.h" #include "examples/FilterDemo.h" +#include "examples/GpuAudioProcessingDemo.h" #include "examples/Images.h" #include "examples/LayoutFonts.h" #include "examples/LottieDemo.h" @@ -178,10 +180,12 @@ class CustomWindow addDemo ("Clipboard", [] { return std::make_unique(); }); addDemo ("Color Lab", [] { return std::make_unique(); }); addDemo ("Component Effects", [] { return std::make_unique(); }); + addDemo ("Compute Particles", [] { return std::make_unique(); }); addDemo ("Convolution Demo", [] { return std::make_unique(); }); addDemo ("Crossover Demo", [] { return std::make_unique(); }); addDemo ("File Chooser", [] { return std::make_unique(); }); addDemo ("Filter Demo", [] { return std::make_unique(); }); + addDemo ("GPU Audio", [] { return std::make_unique(); }); addDemo ("Images", [] { return std::make_unique(); }); addDemo ("Layout Fonts", [] { return std::make_unique(); }); addDemo ("Lottie", [] { return std::make_unique(); }); diff --git a/justfile b/justfile index 9ada9de3b..8a2b9addc 100644 --- a/justfile +++ b/justfile @@ -118,3 +118,12 @@ shader_bundler *COMPILE_ARGS: rive_update REF="runtime-v0.1.62": uv run python tools/rive_update.py --rive-ref {{REF}} --allow-dirty --keep-work-dir + +rive_shaders_update: + uv venv .venv --clear + source .venv/bin/activate + uv pip install ply + uv run make -C thirdparty/rive_renderer/source/shaders -j 8 + cp -R thirdparty/rive_renderer/source/shaders/out/generated/* thirdparty/rive_renderer/source/generated/shaders/ + rm -Rf thirdparty/rive_renderer/source/shaders/out + .venv/bin/deactivate diff --git a/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp b/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp index 593ce7138..3802a1870 100644 --- a/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp +++ b/modules/yup_animation/renderer/yup_AnimationRenderResources.cpp @@ -208,6 +208,15 @@ AnimationRenderResources::MatteCanvasLease AnimationRenderResources::acquireMatt slot.width = width; slot.height = height; slot.inUse = true; + for (size_t i = 0; i < matteCanvasPool.size(); ++i) + { + if (! matteCanvasPool[i].inUse) + { + matteCanvasPool[i] = std::move (slot); + return { *this, i }; + } + } + matteCanvasPool.push_back (std::move (slot)); return { *this, matteCanvasPool.size() - 1 }; } diff --git a/modules/yup_animation/renderer/yup_AnimationRenderer.cpp b/modules/yup_animation/renderer/yup_AnimationRenderer.cpp index 01baec2d5..684bcf107 100644 --- a/modules/yup_animation/renderer/yup_AnimationRenderer.cpp +++ b/modules/yup_animation/renderer/yup_AnimationRenderer.cpp @@ -212,7 +212,8 @@ void AnimationRenderer::renderComposition (Graphics& g, sceneCtx.buildParentTransforms (comp.layers); PrecompCache precompCache; - RenderContext ctx { sceneCtx, viewXf, opacity, std::move (paintOverride), &precompCache, renderResources }; + std::vector matteLeases; + RenderContext ctx { sceneCtx, viewXf, opacity, std::move (paintOverride), &precompCache, renderResources, &matteLeases }; renderLayerList (g, comp.layers, ctx); } @@ -457,6 +458,13 @@ bool AnimationRenderer::renderLayerWithMatte (Graphics& g, return false; // 3. Composite target * coverage(source) into resultCanvas. + // + // The result canvas is written *only* by this pass: it never goes through + // beginDraw(), so nothing else clears it, and its backing texture is allocated + // uninitialized. Compositing it after a failed encode therefore blits undefined + // GPU memory over the whole layer, which reads as a flash of an arbitrary color. + // Every step is checked so a failure falls through to the geometric-clip path + // instead. { MatteParams params { matteModeValue (layer.matteType), (float) w, (float) h, 0.0f }; @@ -472,10 +480,19 @@ bool AnimationRenderer::renderLayerWithMatte (Graphics& g, pass.setTexture (0, 0, targetTex); pass.setTexture (0, 1, sourceTex); pass.setUniformBuffer (0, 3, ¶ms, sizeof (params)); - pass.draw (3); - pass.finish(); - frame.submit(); + if (! pass.draw (3)) + return false; + + if (! pass.finish()) + return false; + + if (! frame.submit()) + return false; + + // Leaving this scope waits for the GPU before releasing the views, uniform + // buffer and sampler the pass references by raw pointer, so the result + // canvas is complete before it is sampled below. } auto resultTex = canvases.getResultCanvas().asTexture(); @@ -487,6 +504,16 @@ bool AnimationRenderer::renderLayerWithMatte (Graphics& g, g.setOpacity (g.getOpacity() * opacity); g.drawTexture (resultTex, fittedRect); + // drawTexture only queues a reference to resultTex - the enclosing frame reads + // it at flush time, after this function has returned. Keep the lease alive for + // the rest of the composition render so the pool cannot hand these canvases to + // another matte layer, which would overwrite the pixels just queued. Every + // matte in a composition is sized to the same fitted rectangle, so without this + // the pool reuses one canvas triple for all of them and only the last matte + // survives (e.g. world_locations.json's four matted dots collapse to one). + if (ctx.matteLeases != nullptr) + ctx.matteLeases->push_back (std::move (canvases)); + return true; } @@ -621,7 +648,7 @@ void AnimationRenderer::renderPrecompLayer (Graphics& g, const PrecompLayer& lay SceneContext offscreenScene { ctx.scene.comp, localFrame, layer.layerSize }; offscreenScene.buildParentTransforms (asset->layers); - RenderContext offscreenCtx { offscreenScene, AffineTransform::scaling (deviceScale), 1.0f, ctx.paintOverride, ctx.precompCache, ctx.renderResources }; + RenderContext offscreenCtx { offscreenScene, AffineTransform::scaling (deviceScale), 1.0f, ctx.paintOverride, ctx.precompCache, ctx.renderResources, ctx.matteLeases }; renderLayerList (offscreenG, asset->layers, offscreenCtx); } @@ -640,7 +667,7 @@ void AnimationRenderer::renderPrecompLayer (Graphics& g, const PrecompLayer& lay precompScene.buildParentTransforms (asset->layers); - RenderContext precompCtx { precompScene, precompViewXf, opacity, ctx.paintOverride, nullptr, ctx.renderResources }; + RenderContext precompCtx { precompScene, precompViewXf, opacity, ctx.paintOverride, nullptr, ctx.renderResources, ctx.matteLeases }; renderLayerList (g, asset->layers, precompCtx); } @@ -969,7 +996,8 @@ void AnimationRenderer::renderGroup (Graphics& g, const AnimationGroup& group, const RenderContext& ctx, float opacity, - const AnimationRoundedCorner* parentRoundedCorner) + const AnimationRoundedCorner* parentRoundedCorner, + std::vector* geometryOut) { if (group.hidden) return; @@ -1022,6 +1050,9 @@ void AnimationRenderer::renderGroup (Graphics& g, // (otherwise paint-less construction guides get filled - e.g. the stray // star shapes in pumped_up.json / mughead.json). const bool mergesNestedGeometry = activeMergePaths != nullptr && ! activeMergePaths->hidden; + // An enclosing group asking for this group's geometry also needs the geometry + // of any paint-less group nested inside it. + const bool collectsNestedGeometry = mergesNestedGeometry || geometryOut != nullptr; const bool hasModifiers = hasRounded || hasTrim || hasRepeater || hasMergePaths; const bool hasDirectPaint = std::any_of (group.children.begin(), group.children.end(), @@ -1189,12 +1220,9 @@ void AnimationRenderer::renderGroup (Graphics& g, } else if (child.kind == AnimationGroup::ChildKind::Group && child.group != nullptr) { - renderGroup (g, *child.group, ctx, opacity, activeRoundedCorner); - // A nested group without its own paint can supply geometry to a // parent paint or Merge Paths modifier. - if (! mergesNestedGeometry && ! hasDirectPaint) - continue; + const bool wantsNestedGeometry = collectsNestedGeometry || hasDirectPaint; const bool hasOwnPaint = std::any_of (child.group->children.begin(), child.group->children.end(), @@ -1204,17 +1232,44 @@ void AnimationRenderer::renderGroup (Graphics& g, || c.kind == AnimationGroup::ChildKind::Stroke; }); - if (! hasOwnPaint) + // Harvest the geometry from the nested render call rather than + // rebuilding it from raw shapes: the nested group's own modifiers are + // what define the outline. A trim reducing a 4-point star to an arc is + // how RubberHose rigs draw a limb (mughead.json, pumped_up.json) - drop + // it and the parent's stroke paints the whole star instead. + std::vector nestedGeometry; + const bool harvest = wantsNestedGeometry && ! hasOwnPaint; + + renderGroup (g, *child.group, ctx, opacity, activeRoundedCorner, harvest ? &nestedGeometry : nullptr); + + if (harvest) { - Path nestedGeometry = buildMatteClipPathForGroup (*child.group, frameNo, AffineTransform::identity()); - if (! nestedGeometry.isEmpty()) + Path combinedGeometry; + for (const auto& path : nestedGeometry) + combinedGeometry.appendPath (path); + + if (! combinedGeometry.isEmpty()) { - currentPaths.push_back (std::move (nestedGeometry)); + currentPaths.push_back (std::move (combinedGeometry)); preparedValid = false; } } } } + + if (geometryOut == nullptr || currentPaths.empty()) + return; + + // Report the modifier-applied geometry in the enclosing group's space. The + // group transform is applied here because the caller paints these paths under + // its own transform, not this group's. + if (hasModifiers && ! preparedValid) + computePrepared(); + + const auto groupTransform = group.transform.toAffineTransform (frameNo); + + for (const auto& path : (hasModifiers ? preparedCache : currentPaths)) + geometryOut->push_back (path.transformed (groupTransform)); } void AnimationRenderer::applyTrim (Path& path, const AnimationTrim& trim, float frameNo) diff --git a/modules/yup_animation/renderer/yup_AnimationRenderer.h b/modules/yup_animation/renderer/yup_AnimationRenderer.h index 036adc164..9add948d7 100644 --- a/modules/yup_animation/renderer/yup_AnimationRenderer.h +++ b/modules/yup_animation/renderer/yup_AnimationRenderer.h @@ -87,6 +87,17 @@ class YUP_API AnimationRenderer PrecompCache* precompCache = nullptr; ///< Owned by the outermost renderComposition call. AnimationRenderResources* renderResources = nullptr; ///< Optional persistent GPU resources (matte pipeline). + /** Matte canvas leases held until the composition render completes. + + Drawing a matte result only records a reference to its canvas texture; + the pixels are read when the enclosing frame flushes. Releasing the + lease before then lets the next matte layer acquire the same pooled + canvases and overwrite those pixels, so every matte in the frame ends up + showing the last one's content. Owned by the outermost + renderComposition call. + */ + std::vector* matteLeases = nullptr; + AffineTransform resolveLayerTransform (const AnimationLayer& layer) const; }; @@ -116,11 +127,21 @@ class YUP_API AnimationRenderer std::optional paintOverride, AnimationRenderResources* renderResources); + /** Renders @p group, and optionally reports the geometry it contributes to an + enclosing group's paints. + + When @p geometryOut is non-null the group's collected paths are appended to + it with the group's own modifiers (Merge Paths, Round Corners, Trim, + Repeater) already applied and mapped into the parent's space. A paint-less + construction group is only meaningful once its modifiers have run - the + modifiers are what turn the raw shape into the intended outline. + */ static void renderGroup (Graphics& g, const AnimationGroup& group, const RenderContext& ctx, float opacity, - const AnimationRoundedCorner* parentRoundedCorner = nullptr); + const AnimationRoundedCorner* parentRoundedCorner = nullptr, + std::vector* geometryOut = nullptr); static void renderLayerList (Graphics& g, const std::vector& layers, diff --git a/modules/yup_core/maths/yup_MathsFunctions.h b/modules/yup_core/maths/yup_MathsFunctions.h index a9015671c..8ed49bbdd 100644 --- a/modules/yup_core/maths/yup_MathsFunctions.h +++ b/modules/yup_core/maths/yup_MathsFunctions.h @@ -124,30 +124,76 @@ constexpr int numElementsInArray (Type (&)[N]) noexcept return N; } +//============================================================================== +/** Constexpr enabled square root function. + + This is a constexpr enabled square root function that can be used in compile-time + calculations. It uses Newton's method to calculate the square root of a number. + + @tags{Core} +*/ +template +constexpr Type yup_sqrt (Type x) noexcept +{ + if (isConstantEvaluated()) + { + if (x < static_cast (0)) + return std::numeric_limits::quiet_NaN(); + + if (x == static_cast (0)) + return static_cast (0); + + Type r = x; + + for (int i = 0; i < 30; ++i) + r = static_cast (0.5) * (r + x / r); + + return r; + } + else + { + return std::sqrt (x); + } +} + //============================================================================== // Some useful maths functions that aren't always present with all compilers and build settings. /** Using yup_hypot is easier than dealing with the different types of hypot function that are provided by the various platforms and compilers. */ template -Type yup_hypot (Type a, Type b) noexcept +constexpr Type yup_hypot (Type a, Type b) noexcept { + if (isConstantEvaluated()) + { + return yup_sqrt (a * a + b * b); + } + else + { #if YUP_MSVC - return static_cast (_hypot (a, b)); + return static_cast (_hypot (a, b)); #else - return static_cast (hypot (a, b)); + return static_cast (hypot (a, b)); #endif + } } #ifndef DOXYGEN template <> -inline float yup_hypot (float a, float b) noexcept +constexpr float yup_hypot (float a, float b) noexcept { + if (isConstantEvaluated()) + { + return yup_sqrt (a * a + b * b); + } + else + { #if YUP_MSVC - return _hypotf (a, b); + return _hypotf (a, b); #else - return hypotf (a, b); + return hypotf (a, b); #endif + } } #endif diff --git a/modules/yup_graphics/context/yup_GraphicsContext.cpp b/modules/yup_graphics/context/yup_GraphicsContext.cpp index 327fac63e..058303dc3 100644 --- a/modules/yup_graphics/context/yup_GraphicsContext.cpp +++ b/modules/yup_graphics/context/yup_GraphicsContext.cpp @@ -26,7 +26,7 @@ namespace yup bool GraphicsContext::isGpuAvailable() const noexcept { if (auto device = getGpuDevice()) - return device->gpuContext() != nullptr; + return device->getGpuContext() != nullptr; return false; } diff --git a/modules/yup_graphics/context/yup_GraphicsContext.h b/modules/yup_graphics/context/yup_GraphicsContext.h index 80942d6e9..8280f6527 100644 --- a/modules/yup_graphics/context/yup_GraphicsContext.h +++ b/modules/yup_graphics/context/yup_GraphicsContext.h @@ -89,19 +89,19 @@ class YUP_API GraphicsContext @return Pointer to a rive::Factory object. */ - virtual rive::Factory* factory() = 0; + virtual rive::Factory* getFactory() = 0; /** Gets the PLS render context, if available. @return Pointer to a rive::pls::PLSRenderContext, or nullptr if not available. */ - virtual rive::gpu::RenderContext* renderContext() = 0; + virtual rive::gpu::RenderContext* getRenderContext() = 0; /** Gets the PLS render target, if available. @return Pointer to a rive::pls::PLSRenderTarget, or nullptr if not available. */ - virtual rive::gpu::RenderTarget* renderTarget() = 0; + virtual rive::gpu::RenderTarget* getRenderTarget() = 0; /** Creates a renderer suitable for the specified dimensions. diff --git a/modules/yup_graphics/formats/yup_JpegImageFormat.cpp b/modules/yup_graphics/formats/yup_JpegImageFormat.cpp index 97f6508a3..72c1dbac9 100644 --- a/modules/yup_graphics/formats/yup_JpegImageFormat.cpp +++ b/modules/yup_graphics/formats/yup_JpegImageFormat.cpp @@ -148,7 +148,7 @@ JpegImageFormatReader::JpegImageFormatReader (InputStream* stream, const ImageFo metadata->rawChunks["jpeg/exif"] = MemoryBlock (data + 6, dataLength - 6); } else if (marker->marker == JPEG_APP0 + 1 && dataLength > 29 - && std::memcmp (data, "http://ns.adobe.com/xap/", 29) == 0) + && std::memcmp (data, "http://ns.adobe.com/xap/1.0/", 29) == 0) { if (getOptions().parseRawChunks) metadata->rawChunks["jpeg/xmp"] = MemoryBlock (data, dataLength); diff --git a/modules/yup_graphics/formats/yup_PngImageFormat.cpp b/modules/yup_graphics/formats/yup_PngImageFormat.cpp index 8955a72c3..5ca22286b 100644 --- a/modules/yup_graphics/formats/yup_PngImageFormat.cpp +++ b/modules/yup_graphics/formats/yup_PngImageFormat.cpp @@ -372,7 +372,7 @@ void PngImageFormatReader::parseChunks() // tIME chunk if (std::memcmp (type, "tIME", 4) == 0 && chunkLen >= 7) { - char buf[20]; + char buf[32]; snprintf (buf, sizeof (buf), "%04d:%02d:%02d %02d:%02d:%02d", readBE16 (chunkData), chunkData[2], chunkData[3], chunkData[4], chunkData[5], chunkData[6]); metadata->textEntries.set ("png/time", String (buf)); } diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index 59dac52d6..13761f13c 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -194,7 +194,7 @@ rive::Factory* getOffscreenFactory (GraphicsContext& context, RenderableTarget* if (auto* renderContext = target->getRenderContext()) return renderContext; - return context.factory(); + return context.getFactory(); } std::unique_ptr makeOffscreenRenderer (GraphicsContext& context, RenderableTarget* target, int width, int height) @@ -240,7 +240,7 @@ void Graphics::SavedState::restore() Graphics::Graphics (GraphicsContext& context, rive::Renderer& renderer, float scale) noexcept : context (context) , offscreenTarget (nullptr) - , factory (*context.factory()) + , factory (*context.getFactory()) , ownedRenderer (nullptr) , renderer (renderer) , contextScale (scale) @@ -939,7 +939,7 @@ void Graphics::drawTexture (const GpuTexture::Ptr& texture, const Rectangle texture, const Rectangle& targetArea) { - auto renderContext = context.renderContext(); + auto renderContext = context.getRenderContext(); if (renderContext == nullptr || texture == nullptr) return false; diff --git a/modules/yup_graphics/imaging/yup_Image.cpp b/modules/yup_graphics/imaging/yup_Image.cpp index 66477b2d8..c08002efb 100644 --- a/modules/yup_graphics/imaging/yup_Image.cpp +++ b/modules/yup_graphics/imaging/yup_Image.cpp @@ -249,7 +249,7 @@ bool Image::createTextureIfNotPresent (GraphicsContext& context) const auto width = getWidth(); auto height = getHeight(); - auto renderContext = context.renderContext(); + auto renderContext = context.getRenderContext(); if (renderContext == nullptr || renderContext->impl() == nullptr) return false; diff --git a/modules/yup_graphics/imaging/yup_ImagePixelData.cpp b/modules/yup_graphics/imaging/yup_ImagePixelData.cpp index b537d5070..b953a161c 100644 --- a/modules/yup_graphics/imaging/yup_ImagePixelData.cpp +++ b/modules/yup_graphics/imaging/yup_ImagePixelData.cpp @@ -50,17 +50,17 @@ std::vector ImagePixelData::toRGBA (bool premultiplyAlpha) const switch (format) { case PixelFormat::Grayscale: - ColorVectorOperations::convertGrayscaleToRGBA (src, result.data(), numPixels); + ColorVectorOperations::convertGrayscaleToRGBA (src, reinterpret_cast (result.data()), numPixels); break; case PixelFormat::RGB: - ColorVectorOperations::convertRGBToRGBA (src, result.data(), numPixels); + ColorVectorOperations::convertRGBToRGBA (src, reinterpret_cast (result.data()), numPixels); break; case PixelFormat::RGBA: std::memcpy (result.data(), src, result.size()); if (premultiplyAlpha) - ColorVectorOperations::premultiplyRGBA (result.data(), numPixels); + ColorVectorOperations::premultiplyRGBA (reinterpret_cast (result.data()), numPixels); break; } diff --git a/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp b/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp index 94ced0c71..aa3f08316 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp @@ -28,44 +28,44 @@ namespace yup { -class LowLevelRenderContextD3D : public GraphicsContext +ID3D11Device* yup_getDirect3DDevice (GpuDevice&); +ID3D11DeviceContext* yup_getDirect3DDeviceContext (GpuDevice&); + +//============================================================================== + +class GraphicsContextD3D : public GraphicsContext { public: - LowLevelRenderContextD3D (ComPtr d3dFactory, - ComPtr gpu, - ComPtr gpuContext, - bool isHeadless, - const rive::gpu::D3DContextOptions& contextOptions, - Options options, - GpuDevice::Ptr existingGpu = {}) - : m_isHeadless (isHeadless) - , m_options (options) - , 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, contextOptions)) + GraphicsContextD3D (ComPtr d3dFactory, + ComPtr device, + ComPtr deviceContext, + bool isHeadless, + Options options, + GpuDevice::Ptr gpuDevice) + : isHeadless (isHeadless) + , options (options) + , gpuDevice (std::move (gpuDevice)) + , d3dFactory (std::move (d3dFactory)) + , device (std::move (device)) + , deviceContext (std::move (deviceContext)) { - if (existingGpu != nullptr) - m_gpuContextPtr = std::move (existingGpu); - else - m_gpuContextPtr = GpuDevice::create (GpuPlatform::Direct3D, options); } GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Direct3D; } - GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContextPtr; } + GpuDevice::Ptr getGpuDevice() const noexcept override { return gpuDevice; } - rive::Factory* factory() override { return m_renderContext.get(); } + rive::Factory* getFactory() override { return gpuDevice->getRenderContext(); } - rive::gpu::RenderContext* renderContext() override { return m_renderContext.get(); } + rive::gpu::RenderContext* getRenderContext() override { return gpuDevice->getRenderContext(); } - rive::gpu::RenderTarget* renderTarget() override { return m_renderTarget.get(); } + rive::gpu::RenderTarget* getRenderTarget() override { return renderTarget.get(); } void onSizeChanged (void* window, int width, int height, float dpiScale, uint32_t sampleCount) override { - if (! m_isHeadless) + if (! isHeadless) { - m_swapchain.Reset(); + swapchain.Reset(); DXGI_SWAP_CHAIN_DESC1 scd {}; scd.Width = width; scd.Height = height; @@ -75,12 +75,12 @@ class LowLevelRenderContextD3D : public GraphicsContext scd.BufferCount = 2; scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; - VERIFY_OK (m_d3dFactory->CreateSwapChainForHwnd (m_gpu.Get(), - (HWND) window, - &scd, - nullptr, - nullptr, - m_swapchain.ReleaseAndGetAddressOf())); + VERIFY_OK (d3dFactory->CreateSwapChainForHwnd (device.Get(), + (HWND) window, + &scd, + nullptr, + nullptr, + swapchain.ReleaseAndGetAddressOf())); } else { @@ -95,55 +95,55 @@ class LowLevelRenderContextD3D : public GraphicsContext desc.BindFlags = D3D11_BIND_RENDER_TARGET; desc.CPUAccessFlags = 0; desc.MiscFlags = 0; - VERIFY_OK (m_gpu->CreateTexture2D (&desc, nullptr, &m_headlessDrawTexture)); + VERIFY_OK (device->CreateTexture2D (&desc, nullptr, &headlessDrawTexture)); } - auto renderContextImpl = m_renderContext->static_impl_cast(); - m_renderTarget = renderContextImpl->makeRenderTarget (width, height); - m_readbackTexture = nullptr; + auto renderContextImpl = getRenderContext()->static_impl_cast(); + renderTarget = renderContextImpl->makeRenderTarget (width, height); + readbackTexture = nullptr; } std::unique_ptr makeRenderer (int width, int height) override { - return std::make_unique (m_renderContext.get()); + return std::make_unique (getRenderContext()); } void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDescriptor) override { - m_renderContext->beginFrame (frameDescriptor); + getRenderContext()->beginFrame (frameDescriptor); } void end (void*) override { - if (m_renderTarget->targetTexture() == nullptr) + if (renderTarget->targetTexture() == nullptr) { - if (m_isHeadless) - m_renderTarget->setTargetTexture (m_headlessDrawTexture); + if (isHeadless) + renderTarget->setTargetTexture (headlessDrawTexture); else { ComPtr backbuffer; - HRESULT hr = m_swapchain->GetBuffer (0, __uuidof (ID3D11Texture2D), reinterpret_cast (backbuffer.ReleaseAndGetAddressOf())); + HRESULT hr = swapchain->GetBuffer (0, __uuidof (ID3D11Texture2D), reinterpret_cast (backbuffer.ReleaseAndGetAddressOf())); if (FAILED (hr)) { - auto reason = m_gpu->GetDeviceRemovedReason(); + auto reason = device->GetDeviceRemovedReason(); fprintf (stderr, "D3D: GetBuffer failed: hr=0x%08X, deviceRemovedReason=0x%08X\n", static_cast (hr), static_cast (reason)); - m_renderTarget->setTargetTexture (nullptr); + renderTarget->setTargetTexture (nullptr); return; } - m_renderTarget->setTargetTexture (backbuffer); + renderTarget->setTargetTexture (backbuffer); } } rive::gpu::RenderContext::FlushResources flushDesc; - flushDesc.renderTarget = m_renderTarget.get(); - m_renderContext->flush (flushDesc); + flushDesc.renderTarget = renderTarget.get(); + getRenderContext()->flush (flushDesc); - if (! m_isHeadless) + if (! isHeadless) { - HRESULT hr = m_swapchain->Present (0, 0); + HRESULT hr = swapchain->Present (0, 0); if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) { - auto reason = m_gpu->GetDeviceRemovedReason(); + auto reason = device->GetDeviceRemovedReason(); fprintf (stderr, "D3D: Present returned device removed/reset: hr=0x%08X, deviceRemovedReason=0x%08X\n", static_cast (hr), static_cast (reason)); } else if (FAILED (hr)) @@ -152,63 +152,43 @@ class LowLevelRenderContextD3D : public GraphicsContext } } - m_renderTarget->setTargetTexture (nullptr); + renderTarget->setTargetTexture (nullptr); } private: - const bool m_isHeadless; - Options m_options; - 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; - rive::rcp m_renderTarget; + const bool isHeadless; + Options options; + GpuDevice::Ptr gpuDevice; + ComPtr d3dFactory; + ComPtr device; + ComPtr deviceContext; + ComPtr swapchain; + ComPtr readbackTexture; + ComPtr headlessDrawTexture; + rive::rcp renderTarget; }; -std::unique_ptr yup_constructDirect3DGraphicsContext (GpuDevice::Options fiddleOptions, GpuDevice::Ptr existingGpu) +std::unique_ptr yup_constructDirect3DGraphicsContext (GpuDevice::Options options, GpuDevice::Ptr existingGpu) { - ComPtr factory; - VERIFY_OK (CreateDXGIFactory (__uuidof (IDXGIFactory2), reinterpret_cast (factory.ReleaseAndGetAddressOf()))); - - ComPtr adapter; - DXGI_ADAPTER_DESC adapterDesc {}; - rive::gpu::D3DContextOptions contextOptions; - - if (fiddleOptions.disableRasterOrdering) - { - contextOptions.disableRasterizerOrderedViews = true; - contextOptions.disableTypedUAVLoadStore = true; - } - - 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 + // The swapchain (and the textures obtained from it) must belong to the very same ID3D11Device + // that owns the render context drawing into them, so the GpuDevice is resolved first and its + // native device is reused here instead of creating a second one. + auto gpuDevice = existingGpu != nullptr ? std::move (existingGpu) + : GpuDevice::create (GpuPlatform::Direct3D, options); + if (gpuDevice == nullptr) + return nullptr; - VERIFY_OK (D3D11CreateDevice (adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, creationFlags, featureLevels, std::size (featureLevels), D3D11_SDK_VERSION, gpu.ReleaseAndGetAddressOf(), nullptr, gpuContext.ReleaseAndGetAddressOf())); + ComPtr device = yup_getDirect3DDevice (*gpuDevice); + ComPtr deviceContext = yup_getDirect3DDeviceContext (*gpuDevice); - if (! gpu || ! gpuContext) + if (! device || ! deviceContext) return nullptr; - printf ("D3D device: %S\n", adapterDesc.Description); + ComPtr factory; + VERIFY_OK (CreateDXGIFactory (__uuidof (IDXGIFactory2), reinterpret_cast (factory.ReleaseAndGetAddressOf()))); - return std::make_unique ( - std::move (factory), std::move (gpu), std::move (gpuContext), fiddleOptions.allowHeadlessRendering, contextOptions, fiddleOptions, std::move (existingGpu)); + return std::make_unique ( + std::move (factory), std::move (device), std::move (deviceContext), options.allowHeadlessRendering, options, std::move (gpuDevice)); } } // namespace yup diff --git a/modules/yup_graphics/native/yup_GraphicsContext_dawn.cpp b/modules/yup_graphics/native/yup_GraphicsContext_dawn.cpp index 0a4ffac69..477c8f353 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_dawn.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_dawn.cpp @@ -88,27 +88,27 @@ static std::unique_ptr SetupDawnWindowAndGetSurfaceDescript } #endif -class LowLevelRenderContextDawnPLS : public GraphicsContext +class GraphicsContextDawn : public GraphicsContext { public: - LowLevelRenderContextDawnPLS (Options options, GpuDevice::Ptr existingGpu = {}) - : m_options (options) + GraphicsContextDawn (Options options, GpuDevice::Ptr existingGpu = {}) + : options (options) { // Obtain or create the GpuDevice if (existingGpu != nullptr) - m_gpuContext = std::move (existingGpu); + gpuDevice = std::move (existingGpu); else - m_gpuContext = GpuDevice::create (GpuPlatform::WebGPU, options); + gpuDevice = GpuDevice::create (GpuPlatform::WebGPU, options); WGPUInstanceDescriptor instanceDescriptor {}; instanceDescriptor.features.timedWaitAnyEnable = true; - m_instance = std::make_unique (&instanceDescriptor); + instance = std::make_unique (&instanceDescriptor); wgpu::RequestAdapterOptions adapterOptions = { .powerPreference = wgpu::PowerPreference::HighPerformance, }; - auto adapters = m_instance->EnumerateAdapters (&adapterOptions); + auto adapters = instance->EnumerateAdapters (&adapterOptions); wgpu::DawnAdapterPropertiesPowerPreference power_props {}; wgpu::AdapterProperties adapterProperties {}; @@ -146,39 +146,39 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext .requiredFeatures = requiredFeatures.data(), }; - m_backendDevice = preferredAdapter->CreateDevice (&deviceDesc); + 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); - backendProcs.deviceSetLoggingCallback (m_backendDevice, device_log_callback, nullptr); - - m_device = wgpu::Device::Acquire (m_backendDevice); - m_queue = m_device.GetQueue(); - m_plsContext = PLSRenderContextWebGPUImpl::MakeContext ( - m_device, m_queue, PLSRenderContextWebGPUImpl::ContextOptions()); + backendProcs.deviceSetUncapturedErrorCallback (backendDevice, print_device_error, nullptr); + backendProcs.deviceSetDeviceLostCallback (backendDevice, device_lost_callback, nullptr); + backendProcs.deviceSetLoggingCallback (backendDevice, device_log_callback, nullptr); + + device = wgpu::Device::Acquire (backendDevice); + queue = device.GetQueue(); + plsContext = PLSRenderContextWebGPUImpl::MakeContext ( + device, queue, PLSRenderContextWebGPUImpl::ContextOptions()); } GpuPlatform getPlatform() const noexcept override { return GpuPlatform::WebGPU; } - GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContext; } + GpuDevice::Ptr getGpuDevice() const noexcept override { return gpuDevice; } - Factory* factory() override { return m_plsContext.get(); } + Factory* getFactory() override { return plsContext.get(); } - rive::pls::PLSRenderContext* renderContext() override { return m_plsContext.get(); } + rive::gpu::RenderContext* getRenderContext() override { return plsContext.get(); } - rive::pls::PLSRenderTarget* renderTarget() override { return m_renderTarget.get(); } + rive::gpu::RenderTarget* getRenderTarget() override { return renderTarget.get(); } void onSizeChanged (void* window, int width, int height, float dpiScale, uint32_t sampleCount) override { DawnProcTable backendProcs = dawn::native::GetProcs(); - auto surfaceChainedDesc = SetupDawnWindowAndGetSurfaceDescriptor (window, m_options.retinaDisplay); + auto surfaceChainedDesc = SetupDawnWindowAndGetSurfaceDescriptor (window, options.retinaDisplay); WGPUSurfaceDescriptor surfaceDesc = { .nextInChain = reinterpret_cast (surfaceChainedDesc.get()), }; - WGPUSurface surface = backendProcs.instanceCreateSurface (m_instance->Get(), &surfaceDesc); + WGPUSurface surface = backendProcs.instanceCreateSurface (instance->Get(), &surfaceDesc); WGPUSwapChainDescriptor swapChainDesc = { .usage = WGPUTextureUsage_RenderAttachment, @@ -188,56 +188,56 @@ class LowLevelRenderContextDawnPLS : public GraphicsContext .presentMode = WGPUPresentMode_Immediate, }; - if (m_options.enableReadPixels) + if (options.readableFramebuffer) swapChainDesc.usage |= WGPUTextureUsage_CopySrc; - WGPUSwapChain backendSwapChain = backendProcs.deviceCreateSwapChain (m_backendDevice, surface, &swapChainDesc); - m_swapchain = wgpu::SwapChain::Acquire (backendSwapChain); + WGPUSwapChain backendSwapChain = backendProcs.deviceCreateSwapChain (backendDevice, surface, &swapChainDesc); + swapchain = wgpu::SwapChain::Acquire (backendSwapChain); - m_renderTarget = m_plsContext->static_impl_cast() - ->makeRenderTarget (wgpu::TextureFormat::BGRA8Unorm, width, height); + renderTarget = plsContext->static_impl_cast() + ->makeRenderTarget (wgpu::TextureFormat::BGRA8Unorm, width, height); - m_pixelReadBuff = {}; + pixelReadBuff = {}; } std::unique_ptr makeRenderer (int width, int height) override { - return std::make_unique (m_plsContext.get()); + return std::make_unique (plsContext.get()); } void begin (PLSRenderContext::FrameDescriptor&& frameDescriptor) override { - 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)); + assert (swapchain.GetCurrentTexture().GetWidth() == renderTarget->width()); + assert (swapchain.GetCurrentTexture().GetHeight() == renderTarget->height()); + renderTarget->setTargetTextureView (swapchain.GetCurrentTextureView()); + frameDescriptor.renderTarget = renderTarget; + plsContext->beginFrame (std::move (frameDescriptor)); } void end (void* window) override { - m_plsContext->flush(); - m_swapchain.Present(); + plsContext->flush(); + swapchain.Present(); } - void tick() override { m_device.Tick(); } + void tick() override { device.Tick(); } private: - Options m_options; - GpuDevice::Ptr m_gpuContext; - WGPUDevice m_backendDevice = {}; - wgpu::Device m_device = {}; - wgpu::Queue m_queue = {}; - wgpu::SwapChain m_swapchain = {}; - std::unique_ptr m_instance; - std::unique_ptr m_plsContext; - rcp m_renderTarget; - wgpu::Buffer m_pixelReadBuff; + Options options; + GpuDevice::Ptr gpuDevice; + WGPUDevice backendDevice = {}; + wgpu::Device device = {}; + wgpu::Queue queue = {}; + wgpu::SwapChain swapchain = {}; + std::unique_ptr instance; + std::unique_ptr plsContext; + rcp renderTarget; + wgpu::Buffer pixelReadBuff; }; std::unique_ptr yup_constructDawnGraphicsContext (GpuDevice::Options options, GpuDevice::Ptr existingGpu) { - return std::make_unique (options, std::move (existingGpu)); + return std::make_unique (options, std::move (existingGpu)); } } // namespace yup diff --git a/modules/yup_graphics/native/yup_GraphicsContext_dawn_helper.cpp b/modules/yup_graphics/native/yup_GraphicsContext_dawn_helper.cpp index f4882d206..a95fd9404 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_dawn_helper.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_dawn_helper.cpp @@ -58,7 +58,7 @@ float GetDawnWindowBackingScaleFactor (void* window, bool retina) std::unique_ptr SetupDawnWindowAndGetSurfaceDescriptor (void* window, bool retina) { - @autoreleasepool + YUP_AUTORELEASEPOOL { NSWindow* nsWindow = (__bridge NSWindow*) window; NSView* view = [nsWindow contentView]; diff --git a/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp b/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp index 155b6b45a..16203e20b 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_headless.cpp @@ -185,60 +185,42 @@ class NoOpRenderer : public rive::Renderer //============================================================================== -class NoOpGraphicsContext : public GraphicsContext +class GraphicsContextHeadless : public GraphicsContext { public: - NoOpGraphicsContext() + GraphicsContextHeadless() + : gpuDevice (GpuDevice::create (GpuPlatform::Headless, {})) { - gpuCtx = GpuDevice::create (GpuPlatform::Headless, {}); } GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Headless; } - GpuDevice::Ptr getGpuDevice() const noexcept override { return gpuCtx; } + GpuDevice::Ptr getGpuDevice() const noexcept override { return gpuDevice; } - rive::Factory* factory() override - { - return std::addressof (noOpFactory); - } + rive::Factory* getFactory() override { return std::addressof (noOpFactory); } - rive::gpu::RenderContext* renderContext() override - { - return nullptr; - } + rive::gpu::RenderContext* getRenderContext() override { return nullptr; } - rive::gpu::RenderTarget* renderTarget() override - { - return nullptr; - } + rive::gpu::RenderTarget* getRenderTarget() override { return nullptr; } - std::unique_ptr makeRenderer (int, int) override - { - return std::make_unique(); - } + std::unique_ptr makeRenderer (int, int) override { return std::make_unique(); } - void onSizeChanged (void*, int, int, float, uint32_t) override - { - } + void onSizeChanged (void*, int, int, float, uint32_t) override {} - void begin (const rive::gpu::RenderContext::FrameDescriptor&) override - { - } + void begin (const rive::gpu::RenderContext::FrameDescriptor&) override {} - void end (void*) override - { - } + void end (void*) override {} private: NoOpFactory noOpFactory; - GpuDevice::Ptr gpuCtx; + GpuDevice::Ptr gpuDevice; }; //============================================================================== -std::unique_ptr yup_constructHeadlessGraphicsContext (GpuDevice::Options fiddleOptions, GpuDevice::Ptr) +std::unique_ptr yup_constructHeadlessGraphicsContext (GpuDevice::Options, GpuDevice::Ptr) { - return std::make_unique(); + return std::make_unique(); } } // namespace yup diff --git a/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp b/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp index 408374d9b..310880431 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp @@ -73,36 +73,19 @@ MTLClearColor MTLClearColorFromARGB (uint32_t argb) //============================================================================== -class LowLevelRenderContextMetal : public GraphicsContext +class GraphicsContextMetal : public GraphicsContext { public: //============================================================================== - LowLevelRenderContextMetal (Options fiddleOptions, GpuDevice::Ptr existingGpu = {}) - : m_fiddleOptions (fiddleOptions) + GraphicsContextMetal (Options options, GpuDevice::Ptr existingGpu = {}) + : options (options) { // Obtain or create the GpuDevice if (existingGpu != nullptr) - { - m_gpuContext = std::move (existingGpu); - } + gpuDevice = 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; - - if (m_fiddleOptions.disableRasterOrdering) - m_renderContextOptions.disableFramebufferReads = true; - - m_renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (m_gpu, m_renderContextOptions); + gpuDevice = GpuDevice::create (GpuPlatform::Metal, options); // Compile PLS shaders for the fullscreen blit pipeline NSError* error = nil; @@ -113,7 +96,7 @@ class LowLevelRenderContextMetal : public GraphicsContext nil, nil); - auto* plsPrecompiledLibrary = [m_gpu newLibraryWithData:metallibData error:&error]; + auto* plsPrecompiledLibrary = [gpu newLibraryWithData:metallibData error:&error]; if (plsPrecompiledLibrary == nil || error != nil) { NSLog (@"Failed to load binary shaders: %@", error); @@ -139,8 +122,8 @@ class LowLevelRenderContextMetal : public GraphicsContext pipelineDescriptor.vertexDescriptor = vertexDescriptor; pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; - m_pipelineState = [m_gpu newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; - if (m_pipelineState == nil || error != nil) + pipelineState = [gpu newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; + if (pipelineState == nil || error != nil) { NSLog (@"Failed to create pipeline state: %@", error); @@ -148,22 +131,22 @@ class LowLevelRenderContextMetal : public GraphicsContext return; } - m_quadVertexBuffer = [m_gpu newBufferWithBytes:quadVertices length:sizeof (quadVertices) options:MTLResourceStorageModeShared]; + quadVertexBuffer = [gpu newBufferWithBytes:quadVertices length:sizeof (quadVertices) options:MTLResourceStorageModeShared]; } //============================================================================== GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Metal; } - GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContext; } + GpuDevice::Ptr getGpuDevice() const noexcept override { return gpuDevice; } //============================================================================== - rive::Factory* factory() override { return m_renderContext.get(); } + rive::Factory* getFactory() override { return gpuDevice->getRenderContext(); } - rive::gpu::RenderContext* renderContext() override { return m_renderContext.get(); } + rive::gpu::RenderContext* getRenderContext() override { return gpuDevice->getRenderContext(); } - rive::gpu::RenderTarget* renderTarget() override { return m_renderTarget.get(); } + rive::gpu::RenderTarget* getRenderTarget() override { return renderTarget.get(); } //============================================================================== @@ -174,72 +157,72 @@ class LowLevelRenderContextMetal : public GraphicsContext NSView* nsView = [nsWindow contentView]; #endif - if (m_swapchain == nil) + if (swapchain == nil) { #if YUP_MAC nsView.wantsLayer = YES; #endif - m_swapchain = [CAMetalLayer layer]; - m_swapchain.device = m_gpu; - m_swapchain.opaque = YES; - m_swapchain.framebufferOnly = ! m_fiddleOptions.readableFramebuffer; - m_swapchain.pixelFormat = MTLPixelFormatBGRA8Unorm; + swapchain = [CAMetalLayer layer]; + swapchain.device = gpu; + swapchain.opaque = YES; + swapchain.framebufferOnly = ! options.readableFramebuffer; + swapchain.pixelFormat = MTLPixelFormatBGRA8Unorm; #if YUP_MAC - m_swapchain.displaySyncEnabled = NO; + swapchain.displaySyncEnabled = NO; #endif #if YUP_IOS UIView* view = (__bridge UIView*) window; - m_swapchain.frame = view.bounds; - [view.layer addSublayer:m_swapchain]; + swapchain.frame = view.bounds; + [view.layer addSublayer:swapchain]; #else - nsView.layer = m_swapchain; + nsView.layer = swapchain; #endif } - m_swapchain.contentsScale = dpiScale; - m_swapchain.drawableSize = CGSizeMake (width, height); + swapchain.contentsScale = dpiScale; + swapchain.drawableSize = CGSizeMake (width, height); - auto renderContextImpl = m_renderContext->static_impl_cast(); - m_renderTarget = renderContextImpl->makeRenderTarget (MTLPixelFormatBGRA8Unorm, width, height); + auto renderContextImpl = getRenderContext()->static_impl_cast(); + renderTarget = renderContextImpl->makeRenderTarget (MTLPixelFormatBGRA8Unorm, width, height); - if (m_currentTexture != nil) - m_currentTexture = nil; + if (currentTexture != nil) + currentTexture = nil; MTLTextureDescriptor* descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:(MTLPixelFormatBGRA8Unorm) width:width height:height mipmapped:NO]; descriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; - m_currentTexture = [m_gpu newTextureWithDescriptor:descriptor]; + currentTexture = [gpu newTextureWithDescriptor:descriptor]; } //============================================================================== std::unique_ptr makeRenderer (int width, int height) override { - return std::make_unique (m_renderContext.get()); + return std::make_unique (getRenderContext()); } //============================================================================== void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDescriptor) override { - m_renderContext->beginFrame (frameDescriptor); + getRenderContext()->beginFrame (frameDescriptor); if (frameDescriptor.loadAction == rive::gpu::LoadAction::clear) { - id presentCommandBuffer = [m_queue commandBuffer]; + id presentCommandBuffer = [queue commandBuffer]; MTLRenderPassDescriptor* passDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - passDescriptor.colorAttachments[0].texture = m_currentTexture; + passDescriptor.colorAttachments[0].texture = currentTexture; passDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; passDescriptor.colorAttachments[0].clearColor = MTLClearColorFromARGB (frameDescriptor.clearColor); passDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; id encoder = [presentCommandBuffer renderCommandEncoderWithDescriptor:passDescriptor]; - [encoder setRenderPipelineState:m_pipelineState]; + [encoder setRenderPipelineState:pipelineState]; [encoder endEncoding]; [presentCommandBuffer commit]; @@ -248,62 +231,61 @@ class LowLevelRenderContextMetal : public GraphicsContext void end (void*) override { - jassert (m_renderTarget != nil); + jassert (renderTarget != nil); // Render into texture - jassert (m_currentTexture.width == m_renderTarget->width()); - jassert (m_currentTexture.height == m_renderTarget->height()); - m_renderTarget->setTargetTexture (m_currentTexture); + jassert (currentTexture.width == renderTarget->width()); + jassert (currentTexture.height == renderTarget->height()); + renderTarget->setTargetTexture (currentTexture); - id presentCommandBuffer = [m_queue commandBuffer]; - m_renderContext->flush ({ .renderTarget = m_renderTarget.get(), .externalCommandBuffer = (__bridge void*) presentCommandBuffer }); + id presentCommandBuffer = [queue commandBuffer]; + getRenderContext()->flush ({ .renderTarget = renderTarget.get(), .externalCommandBuffer = (__bridge void*) presentCommandBuffer }); // Render texture in view drawable - jassert (m_currentFrameSurface == nil); - m_currentFrameSurface = [m_swapchain nextDrawable]; - jassert (m_currentFrameSurface.texture.width == m_renderTarget->width()); - jassert (m_currentFrameSurface.texture.height == m_renderTarget->height()); + jassert (currentFrameSurface == nil); + currentFrameSurface = [swapchain nextDrawable]; + jassert (currentFrameSurface.texture.width == renderTarget->width()); + jassert (currentFrameSurface.texture.height == renderTarget->height()); MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - renderPassDescriptor.colorAttachments[0].texture = m_currentFrameSurface.texture; + renderPassDescriptor.colorAttachments[0].texture = currentFrameSurface.texture; renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionDontCare; renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; id renderEncoder = [presentCommandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - [renderEncoder setRenderPipelineState:m_pipelineState]; - [renderEncoder setFragmentTexture:m_currentTexture atIndex:0]; - [renderEncoder setVertexBuffer:m_quadVertexBuffer offset:0 atIndex:0]; + [renderEncoder setRenderPipelineState:pipelineState]; + [renderEncoder setFragmentTexture:currentTexture atIndex:0]; + [renderEncoder setVertexBuffer:quadVertexBuffer offset:0 atIndex:0]; [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; [renderEncoder endEncoding]; - [presentCommandBuffer presentDrawable:m_currentFrameSurface]; + [presentCommandBuffer presentDrawable:currentFrameSurface]; [presentCommandBuffer commit]; - m_currentFrameSurface = nil; - m_renderTarget->setTargetTexture (nil); + currentFrameSurface = nil; + renderTarget->setTargetTexture (nil); } private: - const Options m_fiddleOptions; - rive::gpu::RenderContextMetalImpl::ContextOptions m_renderContextOptions; - GpuDevice::Ptr m_gpuContext; - std::unique_ptr m_renderContext; - id m_gpu = MTLCreateSystemDefaultDevice(); - id m_queue = [m_gpu newCommandQueue]; - CAMetalLayer* m_swapchain = nil; - rive::rcp m_renderTarget; - id m_currentFrameSurface = nil; - id m_pipelineState = nil; - id m_currentTexture = nil; - id m_quadVertexBuffer = nil; + const Options options; + rive::gpu::RenderContextMetalImpl::ContextOptions renderContextOptions; + GpuDevice::Ptr gpuDevice; + id gpu = MTLCreateSystemDefaultDevice(); + id queue = [gpu newCommandQueue]; + CAMetalLayer* swapchain = nil; + rive::rcp renderTarget; + id currentFrameSurface = nil; + id pipelineState = nil; + id currentTexture = nil; + id quadVertexBuffer = nil; }; //============================================================================== -std::unique_ptr yup_constructMetalGraphicsContext (GpuDevice::Options fiddleOptions, +std::unique_ptr yup_constructMetalGraphicsContext (GpuDevice::Options options, GpuDevice::Ptr existingGpu) { - return std::make_unique (fiddleOptions, std::move (existingGpu)); + return std::make_unique (options, 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 b74933c08..7dd384e00 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp @@ -33,90 +33,22 @@ namespace yup { -#if RIVE_DESKTOP_GL && DEBUG -static void GLAPIENTRY err_msg_callback (GLenum source, - GLenum type, - GLuint id, - GLenum severity, - GLsizei length, - const GLchar* message, - const void* userParam) -{ - if (type == GL_DEBUG_TYPE_ERROR_KHR) - { - 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.") - == 0) - return; - if (strstr (message, "is being recompiled based on GL state.")) - return; - printf ("GL PERF: %s\n", message); - fflush (stdout); - } -} -#endif - //============================================================================== -class LowLevelRenderContextGL : public GraphicsContext +class GraphicsContextOpenGL : public GraphicsContext { public: - LowLevelRenderContextGL (Options options, GpuDevice::Ptr existingGpu = {}) - : m_options (options) + GraphicsContextOpenGL (Options options, GpuDevice::Ptr existingGpu = {}) + : options (options) { -#if RIVE_DESKTOP_GL - if (! gladLoadCustomLoader ((GLADloadfunc) options.loaderFunction)) - { - fprintf (stderr, "Failed to initialize glad.\n"); - return; - } -#endif - // Obtain or create the GpuDevice for offscreen/RHI operations if (existingGpu != nullptr) - m_gpuContext = std::move (existingGpu); + gpuDevice = 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) - { - fprintf (stderr, "Failed to create a renderer.\n"); - return; - } - - 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) - { - glEnable (GL_DEBUG_OUTPUT_KHR); - glDebugMessageControlKHR (GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); - glDebugMessageCallbackKHR (&err_msg_callback, nullptr); - } -#endif -#endif - -#if DEBUG && ! RIVE_ANDROID - int n; - glGetIntegerv (GL_NUM_EXTENSIONS, &n); - for (size_t i = 0; i < n; ++i) - printf (" %s\n", glGetStringi (GL_EXTENSIONS, i)); -#endif + gpuDevice = GpuDevice::create (getPlatform(), options); } - ~LowLevelRenderContextGL() + ~GraphicsContextOpenGL() { cleanupOffscreenResources(); } @@ -130,55 +62,55 @@ class LowLevelRenderContextGL : public GraphicsContext #endif } - GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContext; } + GpuDevice::Ptr getGpuDevice() const noexcept override { return gpuDevice; } - rive::Factory* factory() override { return m_renderContext.get(); } + rive::Factory* getFactory() override { return gpuDevice->getRenderContext(); } - rive::gpu::RenderContext* renderContext() override { return m_renderContext.get(); } + rive::gpu::RenderContext* getRenderContext() override { return gpuDevice->getRenderContext(); } - rive::gpu::RenderTarget* renderTarget() override { return m_offscreenRenderTarget.get(); } + rive::gpu::RenderTarget* getRenderTarget() override { return offscreenRenderTarget.get(); } - void onSizeChanged (void* window, int width, int height, float dpiScale, uint32_t sampleCount) override + void onSizeChanged (void* window, int newWidth, int newHeight, float dpiScale, uint32_t newSampleCount) override { - m_width = width; - m_height = height; - m_sampleCount = sampleCount; + width = newWidth; + height = newHeight; + sampleCount = newSampleCount; createOffscreenResources(); } std::unique_ptr makeRenderer (int width, int height) override { - return std::make_unique (m_renderContext.get()); + return std::make_unique (getRenderContext()); } void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDescriptor) override { - m_renderContext->static_impl_cast()->invalidateGLState(); - m_renderContext->beginFrame (frameDescriptor); + getRenderContext()->static_impl_cast()->invalidateGLState(); + getRenderContext()->beginFrame (frameDescriptor); } void end (void*) override { - m_renderContext->static_impl_cast()->invalidateGLState(); - m_renderContext->flush ({ m_offscreenRenderTarget.get() }); - m_renderContext->static_impl_cast()->unbindGLInternalResources(); + getRenderContext()->static_impl_cast()->invalidateGLState(); + getRenderContext()->flush ({ offscreenRenderTarget.get() }); + getRenderContext()->static_impl_cast()->unbindGLInternalResources(); blitToMainFramebuffer(); } private: void createOffscreenResources() { - if (m_width <= 0 || m_height <= 0) + if (width <= 0 || height <= 0) { - fprintf (stderr, "createOffscreenResources: Invalid size %dx%d\n", m_width, m_height); + fprintf (stderr, "createOffscreenResources: Invalid size %dx%d\n", width, height); return; } cleanupOffscreenResources(); - 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); + glGenTextures (1, &offscreenTexture); + glBindTexture (GL_TEXTURE_2D, offscreenTexture); + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); @@ -190,9 +122,9 @@ class LowLevelRenderContextGL : public GraphicsContext glBindTexture (GL_TEXTURE_2D, 0); - glGenFramebuffers (1, &m_offscreenFramebuffer); - glBindFramebuffer (GL_FRAMEBUFFER, m_offscreenFramebuffer); - glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_offscreenTexture, 0); + glGenFramebuffers (1, &offscreenFramebuffer); + glBindFramebuffer (GL_FRAMEBUFFER, offscreenFramebuffer); + glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, offscreenTexture, 0); GLenum status = glCheckFramebufferStatus (GL_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) @@ -200,55 +132,53 @@ class LowLevelRenderContextGL : public GraphicsContext glBindFramebuffer (GL_FRAMEBUFFER, 0); - m_offscreenRenderTarget = rive::make_rcp ( - m_width, m_height, m_offscreenFramebuffer, m_sampleCount); + offscreenRenderTarget = rive::make_rcp ( + width, height, offscreenFramebuffer, sampleCount); } void cleanupOffscreenResources() { - if (m_offscreenFramebuffer != 0) + if (offscreenFramebuffer != 0) { - glDeleteFramebuffers (1, &m_offscreenFramebuffer); - m_offscreenFramebuffer = 0; + glDeleteFramebuffers (1, &offscreenFramebuffer); + offscreenFramebuffer = 0; } - if (m_offscreenTexture != 0) + if (offscreenTexture != 0) { - glDeleteTextures (1, &m_offscreenTexture); - m_offscreenTexture = 0; + glDeleteTextures (1, &offscreenTexture); + offscreenTexture = 0; } - m_offscreenRenderTarget.reset(); + offscreenRenderTarget.reset(); } void blitToMainFramebuffer() { - if (m_offscreenTexture == 0) + if (offscreenTexture == 0) { fprintf (stderr, "blitToMainFramebuffer: Invalid program or texture\n"); return; } - glBindFramebuffer (GL_READ_FRAMEBUFFER, m_offscreenFramebuffer); + glBindFramebuffer (GL_READ_FRAMEBUFFER, 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); + glBlitFramebuffer (0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); } - Options m_options; - rive::gpu::RenderContextGLImpl::ContextOptions m_renderContextOptions; - GpuDevice::Ptr m_gpuContext; - std::unique_ptr m_renderContext; - rive::rcp m_offscreenRenderTarget; - - GLuint m_offscreenFramebuffer = 0; - GLuint m_offscreenTexture = 0; - int m_width = 0; - int m_height = 0; - uint32_t m_sampleCount = 0; + Options options; + GpuDevice::Ptr gpuDevice; + rive::rcp offscreenRenderTarget; + + GLuint offscreenFramebuffer = 0; + GLuint offscreenTexture = 0; + int width = 0; + int height = 0; + uint32_t sampleCount = 0; }; //============================================================================== std::unique_ptr yup_constructOpenGLGraphicsContext (GpuDevice::Options options, GpuDevice::Ptr existingGpu) { - return std::make_unique (options, std::move (existingGpu)); + 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 c6fdce901..cdb210cdc 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_webgpu.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_webgpu.cpp @@ -35,62 +35,53 @@ namespace yup { -class LowLevelRenderContextWebGPU : public GraphicsContext +class GraphicsContextWebGPU : public GraphicsContext { public: - LowLevelRenderContextWebGPU (Options options, GpuDevice::Ptr existingGpu = {}) - : m_options (options) + GraphicsContextWebGPU (Options options, GpuDevice::Ptr existingGpu = {}) + : options (options) { - m_device = wgpu::Device::Acquire (emscripten_webgpu_get_device()); - if (m_device == nullptr) + device = wgpu::Device::Acquire (emscripten_webgpu_get_device()); + if (device == nullptr) { jassertfalse; fprintf (stderr, "WebGPU: no device. Ensure Module.preinitializedWebGPUDevice is set before main().\n"); return; } - m_queue = m_device.GetQueue(); + queue = device.GetQueue(); // Obtain or create the GpuDevice for RHI/offscreen operations if (existingGpu != nullptr) - m_gpuContext = std::move (existingGpu); + gpuDevice = 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()); - - if (m_renderContext == nullptr) - { - fprintf (stderr, "WebGPU: failed to create a render context.\n"); - return; - } + gpuDevice = GpuDevice::create (GpuPlatform::WebGPU, options); } GpuPlatform getPlatform() const noexcept override { return GpuPlatform::WebGPU; } - GpuDevice::Ptr getGpuDevice() const noexcept override { return m_gpuContext; } + GpuDevice::Ptr getGpuDevice() const noexcept override { return gpuDevice; } - rive::Factory* factory() override { return m_renderContext.get(); } + rive::Factory* getFactory() override { return gpuDevice->getRenderContext(); } - rive::gpu::RenderContext* renderContext() override { return m_renderContext.get(); } + rive::gpu::RenderContext* getRenderContext() override { return gpuDevice->getRenderContext(); } - rive::gpu::RenderTarget* renderTarget() override { return m_renderTarget.get(); } + rive::gpu::RenderTarget* getRenderTarget() override { return renderTarget.get(); } std::unique_ptr makeRenderer (int width, int height) override { - return std::make_unique (m_renderContext.get()); + return std::make_unique (getRenderContext()); } - void onSizeChanged (void*, int width, int height, float dpiScale, uint32_t) override + void onSizeChanged (void*, int newWidth, int newHeight, float dpiScale, uint32_t) override { - if (m_renderContext == nullptr || width <= 0 || height <= 0) + if (gpuDevice == nullptr || getRenderContext() == nullptr || newWidth <= 0 || newHeight <= 0) return; - m_width = width; - m_height = height; + width = newWidth; + height = newHeight; - if (m_surface == nullptr) + if (surface == nullptr) { wgpu::EmscriptenSurfaceSourceCanvasHTMLSelector canvasDesc = {}; canvasDesc.selector = "#canvas"; @@ -99,11 +90,11 @@ class LowLevelRenderContextWebGPU : public GraphicsContext surfaceDesc.nextInChain = &canvasDesc; wgpu::Instance instance = wgpu::CreateInstance(); - m_surface = instance.CreateSurface (&surfaceDesc); + surface = instance.CreateSurface (&surfaceDesc); } wgpu::SurfaceConfiguration config = {}; - config.device = m_device; + config.device = device; config.format = wgpu::TextureFormat::BGRA8Unorm; config.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst; config.width = (uint32_t) width; @@ -111,7 +102,7 @@ class LowLevelRenderContextWebGPU : public GraphicsContext config.alphaMode = wgpu::CompositeAlphaMode::Auto; config.presentMode = wgpu::PresentMode::Fifo; - m_surface.Configure (&config); + surface.Configure (&config); wgpu::TextureDescriptor textureDesc = {}; textureDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; @@ -119,39 +110,38 @@ class LowLevelRenderContextWebGPU : public GraphicsContext textureDesc.size = { (uint32_t) width, (uint32_t) height, 1 }; textureDesc.format = wgpu::TextureFormat::BGRA8Unorm; - m_offscreenTexture = m_device.CreateTexture (&textureDesc); - m_offscreenTextureView = m_offscreenTexture.CreateView(); + offscreenTexture = device.CreateTexture (&textureDesc); + offscreenTextureView = offscreenTexture.CreateView(); - m_renderTarget = m_renderContext->static_impl_cast() - ->makeRenderTarget (wgpu::TextureFormat::BGRA8Unorm, (uint32_t) width, (uint32_t) height); + renderTarget = getRenderContext()->static_impl_cast()->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) + if (offscreenTextureView == nullptr || renderTarget == nullptr) return; - m_renderTarget->setTargetTextureView (m_offscreenTextureView, m_offscreenTexture); - m_renderContext->beginFrame (frameDescriptor); + renderTarget->setTargetTextureView (offscreenTextureView, offscreenTexture); + getRenderContext()->beginFrame (frameDescriptor); } void end (void*) override { - if (m_renderTarget == nullptr || m_offscreenTexture == nullptr || m_surface == nullptr) + if (renderTarget == nullptr || offscreenTexture == nullptr || surface == nullptr) return; wgpu::SurfaceTexture surfaceTexture = {}; - m_surface.GetCurrentTexture (&surfaceTexture); + surface.GetCurrentTexture (&surfaceTexture); if (surfaceTexture.texture == nullptr) return; - wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder(); + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); - m_renderContext->flush ({ .renderTarget = m_renderTarget.get(), - .externalCommandBuffer = encoder.Get() }); + getRenderContext()->flush ({ .renderTarget = renderTarget.get(), + .externalCommandBuffer = encoder.Get() }); wgpu::TexelCopyTextureInfo copySource = {}; - copySource.texture = m_offscreenTexture; + copySource.texture = offscreenTexture; copySource.aspect = wgpu::TextureAspect::All; wgpu::TexelCopyTextureInfo copyDestination = {}; @@ -159,35 +149,34 @@ class LowLevelRenderContextWebGPU : public GraphicsContext copyDestination.aspect = wgpu::TextureAspect::All; wgpu::Extent3D copySize = {}; - copySize.width = (uint32_t) m_width; - copySize.height = (uint32_t) m_height; + copySize.width = (uint32_t) width; + copySize.height = (uint32_t) height; copySize.depthOrArrayLayers = 1; encoder.CopyTextureToTexture (©Source, ©Destination, ©Size); wgpu::CommandBuffer commands = encoder.Finish(); - m_queue.Submit (1, &commands); + queue.Submit (1, &commands); - m_renderTarget->setTargetTextureView ({}, {}); + renderTarget->setTargetTextureView ({}, {}); } private: - Options m_options; - GpuDevice::Ptr m_gpuContext; - wgpu::Device m_device; - wgpu::Queue m_queue; - wgpu::Surface m_surface; - wgpu::Texture m_offscreenTexture; - wgpu::TextureView m_offscreenTextureView; - int m_width = 0; - int m_height = 0; - std::unique_ptr m_renderContext; - rive::rcp m_renderTarget; + Options options; + GpuDevice::Ptr gpuDevice; + wgpu::Device device; + wgpu::Queue queue; + wgpu::Surface surface; + wgpu::Texture offscreenTexture; + wgpu::TextureView offscreenTextureView; + int width = 0; + int height = 0; + rive::rcp renderTarget; }; std::unique_ptr yup_constructWebGPUGraphicsContext (GpuDevice::Options options, GpuDevice::Ptr existingGpu) { - return std::make_unique (options, std::move (existingGpu)); + return std::make_unique (options, std::move (existingGpu)); } } // namespace yup diff --git a/modules/yup_graphics/primitives/yup_AffineTransform.h b/modules/yup_graphics/primitives/yup_AffineTransform.h index ca264255b..4e0447ca8 100644 --- a/modules/yup_graphics/primitives/yup_AffineTransform.h +++ b/modules/yup_graphics/primitives/yup_AffineTransform.h @@ -783,13 +783,17 @@ class YUP_API AffineTransform /** Get the scale factor of the transformation - Calculates the average of the absolute values of the scale factors along the x and y axes. + Calculates the average of the scale factors along the x and y axes, measured + as the lengths of the transformed basis vectors. Taking the full column + including its shear term keeps the result independent of rotation: reading + only the diagonal would report scale * cos(angle), which collapses towards + zero as a rotation approaches 90 degrees. @return The scale factor of the transformation. */ [[nodiscard]] constexpr float getScaleFactor() const noexcept { - return (yup_abs (scaleX) + yup_abs (scaleY)) / 2.0f; + return (yup_hypot (scaleX, shearY) + yup_hypot (shearX, scaleY)) / 2.0f; } //============================================================================== diff --git a/modules/yup_graphics/rhi/yup_GpuCanvas.cpp b/modules/yup_graphics/rhi/yup_GpuCanvas.cpp index 2a4d42b37..2704c55be 100644 --- a/modules/yup_graphics/rhi/yup_GpuCanvas.cpp +++ b/modules/yup_graphics/rhi/yup_GpuCanvas.cpp @@ -22,27 +22,31 @@ namespace yup { -GpuCanvas::Ptr GpuCanvas::create (GraphicsContext& ctx, int width, int height) +GpuCanvas::Ptr GpuCanvas::create (GraphicsContext& context, int width, int height, std::optional clearColor) { if (width <= 0 || height <= 0) return nullptr; - auto gpuCtx = ctx.getGpuDevice(); - if (gpuCtx == nullptr) + auto gpuDevice = context.getGpuDevice(); + if (gpuDevice == nullptr) return nullptr; // GpuCanvas needs a dedicated render context for the 2D drawing path. - auto renderable = gpuCtx->createRenderableTarget (width, height); + auto renderable = gpuDevice->createRenderableTarget (width, height); if (renderable == nullptr) return nullptr; - auto target = GpuTarget::createFromTarget (gpuCtx, std::move (renderable)); + auto target = GpuTarget::createFromTarget (gpuDevice, std::move (renderable)); if (target == nullptr) return nullptr; GpuCanvas::Ptr canvas = new GpuCanvas(); - canvas->ctx = &ctx; + canvas->context = &context; canvas->target = std::move (target); + + if (clearColor.has_value()) + gpuDevice->clearOffscreen (*canvas->target->getRenderableTarget(), *clearColor); + return canvas; } @@ -69,17 +73,15 @@ int GpuCanvas::getHeight() const noexcept Graphics& GpuCanvas::beginDraw() { - jassert (ctx != nullptr && target != nullptr); + jassert (context != nullptr && target != nullptr); - // Drop the previous frame's Graphics so a fresh offscreen 2D frame opens on - // the existing (already-allocated) target. The cached texture wraps the same - // GPU render target whose contents are overwritten by the new frame, so it is - // reset to force a rewrap on the next asTexture(). graphics.reset(); frameOpen = false; committed = false; - graphics = std::make_unique (*ctx, *target->getRenderableTarget(), 0u); + target->invalidateCachedTexture(); + + graphics = std::make_unique (*context, *target->getRenderableTarget(), 0u); frameOpen = true; return *graphics; @@ -89,16 +91,13 @@ Graphics& GpuCanvas::beginDraw() bool GpuCanvas::commit() { - if (! frameOpen || committed || ctx == nullptr || target == nullptr) + if (! frameOpen || committed || context == nullptr || target == nullptr) return false; auto* renderableTarget = target->getRenderableTarget(); if (renderableTarget == nullptr) return false; - // Ensure the Y-flipped sampled mirror exists before the flush so that - // blitMirrorIfRegistered (called inside endOffscreen) can update it. - // No-op on non-GL backends and for canvases never used as sampled inputs. renderableTarget->getOrCreateSampledTexture(); if (graphics == nullptr || ! graphics->commitOffscreenTarget()) @@ -115,8 +114,6 @@ GpuTexture::Ptr GpuCanvas::asTexture() if (target == nullptr) return nullptr; - // Auto-commit a 2D frame opened via beginDraw() so callers don't need to - // call commit() explicitly. Render-pass-only usage never sets frameOpen. if (frameOpen && ! committed) commit(); @@ -138,7 +135,7 @@ Image GpuCanvas::asImage() bool GpuCanvas::readPixels (void* dst, size_t byteSize) { - if (target == nullptr || ctx == nullptr) + if (target == nullptr || context == nullptr) return false; if (frameOpen && ! committed) diff --git a/modules/yup_graphics/rhi/yup_GpuCanvas.h b/modules/yup_graphics/rhi/yup_GpuCanvas.h index fb44f47bd..439b24080 100644 --- a/modules/yup_graphics/rhi/yup_GpuCanvas.h +++ b/modules/yup_graphics/rhi/yup_GpuCanvas.h @@ -73,15 +73,25 @@ class YUP_API GpuCanvas : public ReferenceCountedObject //============================================================================== /** Creates a GpuCanvas of the given pixel dimensions. - @param ctx The graphics context that owns the GPU device. - @param width Width in pixels (must be > 0). - @param height Height in pixels (must be > 0). + By default the new canvas is filled with transparent black, so it is safe to + sample from before anything has been drawn into it. Pass std::nullopt to skip + that and leave the contents undefined, which is only safe when the canvas is + guaranteed to be fully written before it is next sampled. + + @param ctx The graphics context that owns the GPU device. + @param width Width in pixels (must be > 0). + @param height Height in pixels (must be > 0). + @param clearColor Color to fill the new canvas with, or std::nullopt to + leave its contents undefined. @returns A reference-counted pointer to a GpuCanvas, or nullptr on failure. @warning Requires ctx.isGpuAvailable() (GPU context available on this backend). */ - static GpuCanvas::Ptr create (GraphicsContext& ctx, int width, int height); + static GpuCanvas::Ptr create (GraphicsContext& ctx, + int width, + int height, + std::optional clearColor = Colors::transparentBlack); //============================================================================== /** Returns the underlying GpuTarget backing this canvas. @@ -178,7 +188,7 @@ class YUP_API GpuCanvas : public ReferenceCountedObject //============================================================================== GpuCanvas() = default; - GraphicsContext* ctx = nullptr; + GraphicsContext* context = nullptr; GpuTarget::Ptr target; std::unique_ptr graphics; bool frameOpen = false; diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index 9f4c626c0..195d6feb0 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -1211,6 +1211,10 @@ std::optional Component::findMetric (const Identifier& metricId) const void Component::setComponentEffect (ComponentEffect::Ptr effect) { componentEffect = std::move (effect); + + if (componentEffect == nullptr) + effectOffscreenCanvas = nullptr; + repaint(); } @@ -1335,7 +1339,7 @@ void Component::paintChildrenAndOverChildren (Graphics& g, const Rectangle (getWidth()); const auto h = static_cast (getHeight()); - auto canvas = GpuCanvas::create (ctx, w, h); + GpuCanvas::Ptr canvas; + if (reuseCanvas != nullptr && reuseCanvas->getWidth() == w && reuseCanvas->getHeight() == h) + { + canvas = std::move (reuseCanvas); + } + else + { + reuseCanvas = nullptr; + canvas = GpuCanvas::create (ctx, w, h); + } + if (canvas == nullptr) return nullptr; @@ -1468,7 +1482,7 @@ void Component::internalPaint (Graphics& g, const Rectangle& repaintArea, // Effect path: render full subtree offscreen, apply effect, composite if (componentEffect != nullptr) { - auto canvas = renderSubtreeOffscreen (g.getGraphicsContext(), opacity, renderContinuous); + auto canvas = renderSubtreeOffscreen (g.getGraphicsContext(), opacity, renderContinuous, std::move (effectOffscreenCanvas)); if (canvas == nullptr) return; @@ -1485,6 +1499,8 @@ void Component::internalPaint (Graphics& g, const Rectangle& repaintArea, componentEffect->apply (g, texture, getLocalBounds()); } + effectOffscreenCanvas = canvas; + if (options.cachedToTexture) cachedTextureCanvas = canvas; diff --git a/modules/yup_gui/component/yup_Component.h b/modules/yup_gui/component/yup_Component.h index 849768889..e5ba5860f 100644 --- a/modules/yup_gui/component/yup_Component.h +++ b/modules/yup_gui/component/yup_Component.h @@ -1474,7 +1474,7 @@ class YUP_API Component : public MouseListener bool hasOpaqueChildCoveringArea (const Rectangle& area); void paintSubtree (Graphics& g, const Rectangle& drawingArea, const Rectangle& clipArea, float opacity, bool renderContinuous); void paintChildrenAndOverChildren (Graphics& g, const Rectangle& clipArea, bool renderContinuous); - GpuCanvas::Ptr renderSubtreeOffscreen (GraphicsContext& ctx, float opacity, bool renderContinuous); + GpuCanvas::Ptr renderSubtreeOffscreen (GraphicsContext& ctx, float opacity, bool renderContinuous, GpuCanvas::Ptr reuseCanvas = nullptr); GpuCanvas::Ptr renderSnapshotOffscreen (GraphicsContext& ctx, bool includeEffects); friend class ComponentNative; @@ -1501,6 +1501,7 @@ class YUP_API Component : public MouseListener MouseCursor mouseCursor; ComponentEffect::Ptr componentEffect; GpuCanvas::Ptr cachedTextureCanvas; + GpuCanvas::Ptr effectOffscreenCanvas; float contentScale = 1.0f; uint8 opacity = 255; diff --git a/modules/yup_gui/native/yup_Initialisation_sdl.cpp b/modules/yup_gui/native/yup_Initialisation_sdl.cpp index 9b06f4803..c14bf663f 100644 --- a/modules/yup_gui/native/yup_Initialisation_sdl.cpp +++ b/modules/yup_gui/native/yup_Initialisation_sdl.cpp @@ -77,7 +77,11 @@ bool displayEventDispatcher (void* userdata, SDL_Event* event) { float x = 0.0f, y = 0.0f; SDL_GetGlobalMouseState (&x, &y); - auto cursorPosition = Point { x, y }; + + const SDL_Point pt { static_cast (x), static_cast (y) }; + const auto displayScale = getDisplayUnitsPerPoint (SDL_GetDisplayForPoint (&pt)); + + auto cursorPosition = Point { x / displayScale, y / displayScale }; auto keyModifiers = toKeyModifiers (SDL_GetModState()); MouseEvent mouseEvent ( @@ -98,7 +102,11 @@ bool displayEventDispatcher (void* userdata, SDL_Event* event) { float x = 0.0f, y = 0.0f; SDL_GetGlobalMouseState (&x, &y); - auto cursorPosition = Point { x, y }; + + const SDL_Point pt { static_cast (x), static_cast (y) }; + const auto displayScale = getDisplayUnitsPerPoint (SDL_GetDisplayForPoint (&pt)); + + auto cursorPosition = Point { x / displayScale, y / displayScale }; auto button = toMouseButton (event->button.button); auto keyModifiers = toKeyModifiers (SDL_GetModState()); @@ -115,7 +123,11 @@ bool displayEventDispatcher (void* userdata, SDL_Event* event) { float x = 0.0f, y = 0.0f; SDL_GetGlobalMouseState (&x, &y); - auto cursorPosition = Point { x, y }; + + const SDL_Point pt { static_cast (x), static_cast (y) }; + const auto displayScale = getDisplayUnitsPerPoint (SDL_GetDisplayForPoint (&pt)); + + auto cursorPosition = Point { x / displayScale, y / displayScale }; auto button = toMouseButton (event->button.button); auto keyModifiers = toKeyModifiers (SDL_GetModState()); @@ -132,7 +144,11 @@ bool displayEventDispatcher (void* userdata, SDL_Event* event) { float x = 0.0f, y = 0.0f; SDL_GetGlobalMouseState (&x, &y); - auto cursorPosition = Point { x, y }; + + const SDL_Point pt { static_cast (x), static_cast (y) }; + const auto displayScale = getDisplayUnitsPerPoint (SDL_GetDisplayForPoint (&pt)); + + auto cursorPosition = Point { x / displayScale, y / displayScale }; auto keyModifiers = toKeyModifiers (SDL_GetModState()); auto mouseWheelData = MouseWheelData { static_cast (event->wheel.x), static_cast (event->wheel.y) }; diff --git a/modules/yup_gui/native/yup_Windowing_sdl.cpp b/modules/yup_gui/native/yup_Windowing_sdl.cpp index e8d154eb8..70eb48f92 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl.cpp @@ -382,11 +382,15 @@ void SDLComponentNative::setPosition (const Point& newPosition) Point SDLComponentNative::getPosition() const { int x = 0, y = 0; + float scale = 0.0f; +#if ! (YUP_MOBILE || YUP_EMSCRIPTEN) if (window != nullptr) SDL_GetWindowPosition (window, &x, &y); - const auto scale = getWindowUnitsPerPoint (window); + scale = getWindowUnitsPerPoint (window); +#endif + return { roundToInt (x / scale), roundToInt (y / scale) }; } @@ -719,7 +723,7 @@ Point SDLComponentNative::getCursorPosition() const rive::Factory* SDLComponentNative::getFactory() { - return context ? context->factory() : nullptr; + return context ? context->getFactory() : nullptr; } //============================================================================== @@ -836,7 +840,7 @@ void SDLComponentNative::handleAsyncUpdate() if (! isThreadRunning() || ! isInitialised.test_and_set()) return; - renderContext(); + getRenderContext(); renderEvent.signal(); } @@ -863,12 +867,12 @@ void SDLComponentNative::timerCallback() pollCapturedMouseState(); #endif - renderContext(); + getRenderContext(); } //============================================================================== -void SDLComponentNative::renderContext() +void SDLComponentNative::getRenderContext() { YUP_PROFILE_NAMED_INTERNAL_TRACE (RenderContext); diff --git a/modules/yup_gui/native/yup_Windowing_sdl.h b/modules/yup_gui/native/yup_Windowing_sdl.h index 9971efe30..6a6568d3f 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl.h +++ b/modules/yup_gui/native/yup_Windowing_sdl.h @@ -175,7 +175,7 @@ class SDLComponentNative final Component* findComponentForMouseEvent (const Point& position); void updateComponentUnderMouse (const MouseEvent& event); - void renderContext(); + void getRenderContext(); void startRendering(); void stopRendering(); diff --git a/modules/yup_rhi/context/yup_GpuDevice.cpp b/modules/yup_rhi/context/yup_GpuDevice.cpp index 27143dffc..cbf39ef5b 100644 --- a/modules/yup_rhi/context/yup_GpuDevice.cpp +++ b/modules/yup_rhi/context/yup_GpuDevice.cpp @@ -22,7 +22,8 @@ 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); @@ -39,6 +40,8 @@ std::unique_ptr yup_constructWebGPUGpuDevice (GpuDevice::Options); std::unique_ptr yup_constructDawnGpuDevice (GpuDevice::Options); #endif +//============================================================================== + GpuDevice::Ptr GpuDevice::create (GpuPlatform gpuApi, Options options) { std::unique_ptr ctx; @@ -92,4 +95,156 @@ GpuDevice::Ptr GpuDevice::create (GpuPlatform gpuApi, Options options) return ctx.release(); } +//============================================================================== + +ReferenceCountedObjectPtr GpuDevice::createBuffer (GpuBufferType type, + const void* data, + size_t byteSize) +{ + if (data == nullptr || byteSize == 0) + return nullptr; + + // Storage buffers must be handled by backend overrides. + if (type == GpuBufferType::storage) + return nullptr; + + auto* oreCtx = getGpuContext(); + if (oreCtx == nullptr) + return nullptr; + + rive::ore::BufferDesc desc; + switch (type) + { + case GpuBufferType::vertex: + desc.usage = rive::ore::BufferUsage::vertex; + break; + case GpuBufferType::index: + desc.usage = rive::ore::BufferUsage::index; + break; + default: + desc.usage = rive::ore::BufferUsage::uniform; + break; + } + + desc.size = (uint32_t) byteSize; + desc.data = data; + desc.immutable = true; + desc.label = "GpuBuffer"; + + auto buffer = oreCtx->makeBuffer (desc); + if (buffer == nullptr) + return nullptr; + + return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, std::move (buffer) }); +} + +bool GpuDevice::readBuffer (GpuBuffer::Ptr, void*, size_t) +{ + return false; +} + +bool GpuDevice::updateBuffer (GpuBuffer::Ptr buffer, const void* data, size_t byteSize) +{ + if (buffer == nullptr || data == nullptr || byteSize == 0) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr) + return false; + + // For ore-backed buffers (vertex, index, uniform), update in place. + if (impl->oreBuffer != nullptr) + { + if (byteSize > buffer->getSizeInBytes()) + return false; + + impl->oreBuffer->update (data, (uint32_t) byteSize); + return true; + } + + return false; +} + +//============================================================================== + +GpuDevice::~GpuDevice() +{ + // A backend that forgets releasePooledResources() would let pooled ore buffers + // be destroyed after the ore context that created them. + jassert (uniformBufferPool.isEmpty()); +} + +void GpuDevice::releasePooledResources() noexcept +{ + uniformBufferPool.clear(); +} + +//============================================================================== + +size_t GpuDevice::UniformBufferPool::bucketFor (size_t byteSize) noexcept +{ + size_t index = 0; + + for (size_t capacity = minimumCapacity; capacity < byteSize; capacity <<= 1) + ++index; + + return index; +} + +rive::rcp GpuDevice::UniformBufferPool::acquire (rive::ore::Context& oreCtx, size_t byteSize) +{ + if (byteSize == 0) + return nullptr; + + const auto index = bucketFor (byteSize); + + if (index >= buckets.size()) + buckets.resize (index + 1); + + auto& bucket = buckets[index]; + + if (! bucket.empty()) + { + auto buffer = std::move (bucket.back()); + bucket.pop_back(); + return buffer; + } + + rive::ore::BufferDesc desc; + desc.usage = rive::ore::BufferUsage::uniform; + desc.size = static_cast (minimumCapacity << index); + desc.data = nullptr; + desc.immutable = false; // Rewritten in place every time it is handed out. + desc.label = "GpuRenderPass uniform"; + + return oreCtx.makeBuffer (desc); +} + +void GpuDevice::UniformBufferPool::release (rive::rcp buffer) +{ + if (buffer == nullptr) + return; + + // Capacities are exactly minimumCapacity << index, so the buffer lands back in + // the bucket it came from, which acquire() has already created. + const auto index = bucketFor (buffer->size()); + + if (index < buckets.size()) + buckets[index].push_back (std::move (buffer)); +} + +void GpuDevice::UniformBufferPool::clear() noexcept +{ + buckets.clear(); +} + +bool GpuDevice::UniformBufferPool::isEmpty() const noexcept +{ + for (const auto& bucket : buckets) + if (! bucket.empty()) + return false; + + return true; +} + } // namespace yup diff --git a/modules/yup_rhi/context/yup_GpuDevice.h b/modules/yup_rhi/context/yup_GpuDevice.h index 2f5c8ceae..94c5de567 100644 --- a/modules/yup_rhi/context/yup_GpuDevice.h +++ b/modules/yup_rhi/context/yup_GpuDevice.h @@ -22,6 +22,8 @@ namespace yup { +class GpuBuffer; + //============================================================================== /** Encapsulates a GPU context that abstracts low-level GPU device operations across various graphics APIs without requiring a window or framebuffer. @@ -56,24 +58,35 @@ class YUP_API GpuDevice : public ReferenceCountedObject 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; + /** Creates 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); - /** Destructor. */ - ~GpuDevice() override = default; + //============================================================================== + /** Destructor. + + Asserts that the concrete device released the resources this base class + pools on its behalf. + + @see releasePooledResources + */ + ~GpuDevice() override; //============================================================================== - /** Copy and move constructors and assignment operators. */ - GpuDevice (const GpuDevice& other) noexcept = delete; + /** Move constructors and assignment operators. */ GpuDevice (GpuDevice&& other) noexcept = default; - GpuDevice& operator= (const GpuDevice& other) noexcept = delete; GpuDevice& operator= (GpuDevice&& other) noexcept = default; //============================================================================== @@ -84,21 +97,33 @@ class YUP_API GpuDevice : public ReferenceCountedObject virtual GpuPlatform getPlatform() const noexcept = 0; //============================================================================== + /** Returns the backend-specific GPU render context, or nullptr if unavailable. + + This is the native GPU context used by the Rive renderer. It may be + nullptr on backends that do not support rendering (e.g., headless compute + or OpenGL without a window). + + @return A pointer to the backend-specific RenderContext, or nullptr if unavailable. + */ + virtual rive::gpu::RenderContext* getRenderContext() const { return nullptr; } + /** 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. + + @return A pointer to the ore::Context, or nullptr if unavailable. */ - virtual rive::ore::Context* gpuContext() const noexcept { return nullptr; } + virtual rive::ore::Context* getGpuContext() 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 + Equivalent to getGpuContext() != 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; } + bool isGpuAvailable() const noexcept { return getGpuContext() != nullptr; } /** Returns true if compute shaders are available on this backend. @@ -110,11 +135,11 @@ class YUP_API GpuDevice : public ReferenceCountedObject //============================================================================== /** 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. + The returned target is backed by the device's main render context and does + not reserve a dedicated one, so it cannot drive a 2D Graphics frame. Use it + for render-pass-only surfaces. 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. @@ -130,6 +155,10 @@ class YUP_API GpuDevice : public ReferenceCountedObject (GpuCanvas::beginDraw). Prefer createOffscreenTarget() for render-pass-only surfaces to avoid allocating a dedicated context. + The reservation is exclusive and lasts for the target's whole lifetime; the + context returns to the pool when the target is destroyed. Two live targets + therefore never share a context, which is what allows their frames to nest. + @param width The width of the offscreen target in pixels. @param height The height of the offscreen target in pixels. @@ -167,16 +196,164 @@ class YUP_API GpuDevice : public ReferenceCountedObject */ virtual bool readOffscreenPixels (OffscreenTarget& target, void* dst, size_t dstSize) = 0; + /** Clears an offscreen target's color attachment to the given color. + + Encodes the clear with the backend's native API, outside any ore frame or + render pass. A clear binds no pipeline, buffers or samplers, so nothing + transient has to outlive the encoded work and no GPU sync is required - + unlike a GpuRenderPass clear, whose descriptor references its texture view + by raw pointer and so obliges the frame to wait before releasing it. + + Needs no active frame, and may be called immediately after the target is + created to give it defined contents. + + @param target The OffscreenTarget whose color attachment to clear. + @param color The color to fill the attachment with. + + @return True if the clear was encoded, false if unsupported on this backend. + */ + virtual bool clearOffscreen (OffscreenTarget&, GpuColor) { return false; } + //============================================================================== - /** Static factory method to create a GPU context using a specific GPU API. + /** Creates a GPU buffer of the given type with initial data. - @param gpuApi The GPU API to use. - @param options Configuration options for the GPU context. + The default implementation routes vertex, index, and uniform buffers + through the ore context. Backends that support compute override this to + also handle storage buffers natively. - @return A reference-counted pointer to a GpuDevice, using the specified - GPU API and configured according to the options. + @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). + + @returns A valid GpuBuffer, or nullptr on failure. */ - static GpuDevice::Ptr create (GpuPlatform gpuApi, Options options); + virtual ReferenceCountedObjectPtr createBuffer (GpuBufferType type, + const void* data, + size_t byteSize); + + /** Reads storage buffer contents back to CPU memory. + + The buffer must have been created with GpuBufferType::storage. + + Whether this blocks is a property of the backend, because not all of them + can map a buffer synchronously: + + - Metal, D3D11 and OpenGL read back in lockstep — the call waits for the + GPU and fills @p dst every time. + - WebGPU maps buffers through a promise that only resolves on a later turn + of the JavaScript event loop, so there the readback is pipelined over + several staging buffers instead. The call never blocks; it writes @p dst + once a snapshot has finished mapping, which trails the GPU by a frame or + two. + + Callers must therefore treat @p dst as persistent storage that they own + across calls, and a false return as "no new data yet" rather than as an + error — the previous contents remain valid and usable. A per-frame reader + that redraws its last snapshot works on every backend; one that demands + fresh data on every single call does not. + + @param buffer A storage buffer to read from. + @param dst Destination buffer in CPU memory, persistent across calls. + @param dstSize Size in bytes (must be at least the buffer's byte size). + + @returns true if @p dst was filled with buffer contents. + */ + virtual bool readBuffer (ReferenceCountedObjectPtr buffer, void* dst, size_t dstSize); + + /** Overwrites storage buffer contents in place, without reallocating the buffer. + + The buffer must have been created with GpuBufferType::storage. Unlike + createBuffer(), this does not allocate a new native GPU resource — it + writes into the existing one, which is the only allocation-free way to + feed new data to a storage buffer every frame or audio callback. + + @param buffer A storage buffer to write into. + @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 and + not exceed the buffer's size). + + @returns true on success. + */ + virtual bool updateBuffer (ReferenceCountedObjectPtr buffer, const void* data, size_t byteSize); + +protected: + /** Default constructor. */ + GpuDevice() noexcept = default; + + /** Releases the GPU resources this base class pools on the backend's behalf. + + Every concrete device must call this from its own destructor. Base-class + members are destroyed after the derived class', so pooled resources would + otherwise be released after the ore context that created them - which is + fatal on any backend whose ore resource destructors reach back into + context-owned state, as ore's Vulkan buffer does when it frees through the + context's VMA allocator. + + ~GpuDevice() asserts this has happened, so a backend that forgets the call + fails loudly in debug on every platform instead of crashing at shutdown on + one. + */ + void releasePooledResources() noexcept; + +private: + friend class GpuFrame; + + //============================================================================== + /** Pool of small uniform buffers recycled across frames. + + Every draw in a render pass needs its own uniform buffer, and creating one + per draw is a native GPU allocation on all backends. Buffers are handed out + with a power-of-two capacity and returned once the frame that bound them is + finished, so a steady-state workload stops allocating after its first few + frames. + + Rewriting a returned buffer is safe even when the GPU may still be reading + it: the ore Buffer implementations orphan onto a fresh backing when a buffer + is updated after having been bound (D3D11 instead lets the driver rename + it), and that is what makes reuse across frames sound. + + Pooled buffers must not outlive the ore context that created them, because + an ore Buffer destructor may reach back into context-owned state - ore's + Vulkan buffer frees through the context's VMA allocator. That is why the + pool is emptied by releasePooledResources() rather than by its own + destructor, which would run too late. + */ + class UniformBufferPool + { + public: + /** Returns a buffer with room for at least byteSize bytes. + + Creates one only when no returned buffer of that capacity is available. + + @param oreCtx Context used to create a buffer on a pool miss. + @param byteSize Minimum required capacity in bytes. + + @returns A uniform buffer, or nullptr if creation failed. + */ + rive::rcp acquire (rive::ore::Context& oreCtx, size_t byteSize); + + /** Takes a buffer back so a later acquire() of the same capacity reuses it. */ + void release (rive::rcp buffer); + + /** Drops every pooled buffer. */ + void clear() noexcept; + + /** Returns true when the pool holds no buffers. */ + bool isEmpty() const noexcept; + + private: + /** Smallest capacity handed out - uniform blocks are 16-byte aligned. */ + static constexpr size_t minimumCapacity = 16; + + /** Index of the bucket holding buffers big enough for byteSize. */ + static size_t bucketFor (size_t byteSize) noexcept; + + std::vector>> buckets; + }; + + UniformBufferPool uniformBufferPool; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuDevice) }; } // namespace yup diff --git a/modules/yup_rhi/context/yup_RenderableTarget.h b/modules/yup_rhi/context/yup_RenderableTarget.h index 71522f729..2654950dd 100644 --- a/modules/yup_rhi/context/yup_RenderableTarget.h +++ b/modules/yup_rhi/context/yup_RenderableTarget.h @@ -26,12 +26,18 @@ namespace yup /** An OffscreenTarget backed by a dedicated Rive render context. Created by GraphicsContext::createRenderableTarget(). Unlike a plain - OffscreenTarget, a RenderableTarget reserves a backend-owned RenderContext. - A context is reserved while a target frame is active, allowing recursive - offscreen rendering (TransparencyLayer inside GpuCanvas, nested precomps) - without re-entering beginFrame and allowing sequential targets to reuse an - idle context. This dedicated context is what enables 2D drawing through a - Graphics frame (GpuCanvas::beginDraw). + OffscreenTarget, a RenderableTarget reserves a backend-owned RenderContext + exclusively, for its whole lifetime, and returns it to the pool when + destroyed. This dedicated context is what enables 2D drawing through a + Graphics frame (GpuCanvas::beginDraw) and what makes recursive offscreen + rendering safe (TransparencyLayer inside GpuCanvas, a Lottie matte inside a + precomp inside another matte) without re-entering beginFrame. + + The reservation deliberately outlives any single frame. Targets are commonly + pooled and reused across frames, and the nesting order between two pooled + targets can differ from one frame to the next; sharing one context between + them would make the inner target skip beginFrame and then be flushed against + the outer frame's descriptor, producing undefined contents. The GraphicsContext must outlive every RenderableTarget it creates. */ diff --git a/modules/yup_rhi/native/yup_GpuComputePass_d3d.cpp b/modules/yup_rhi/native/yup_GpuComputePass_d3d.cpp new file mode 100644 index 000000000..eb957c215 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuComputePass_d3d.cpp @@ -0,0 +1,153 @@ +/* + ============================================================================== + + 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 && YUP_WINDOWS + +namespace yup +{ + +//============================================================================== + +class GpuComputePassImplD3D11 final : public GpuComputePass::Impl +{ +public: + GpuComputePassImplD3D11 (ID3D11Device* dev, ID3D11DeviceContext* ctx) + : device (dev) + , context (ctx) + { + } + + bool isValid() const override { return device != nullptr && context != nullptr; } + + //========================================================================== + + bool dispatch (uint32_t groupsX, uint32_t groupsY, uint32_t groupsZ) override + { + if (device == nullptr || context == nullptr || pipelineRef == nullptr) + return false; + + auto* pipe = dynamic_cast (pipelineRef.get()); + if (pipe == nullptr || pipe->getComputeShader() == nullptr) + return false; + + context->CSSetShader (pipe->getComputeShader(), nullptr, 0); + + // UAVs (storage buffers). + for (auto& sb : storageBindings) + { + if (sb.buffer == nullptr) + continue; + auto* bufImpl = sb.buffer->getImpl(); + if (bufImpl == nullptr || bufImpl->d3dUav == nullptr) + continue; + + UINT slot = static_cast (sb.group * 16 + sb.binding); + ID3D11UnorderedAccessView* uavs[1] = { bufImpl->d3dUav.Get() }; + context->CSSetUnorderedAccessViews (slot, 1, uavs, nullptr); + + rememberBoundUavSlot (slot); + } + + // Constant buffers (uniforms) — 16-byte aligned, DYNAMIC + CPU_WRITE. + for (auto& ub : uboBindings) + { + if (ub.data.empty()) + continue; + + UINT cbSize = static_cast ((ub.data.size() + 15) & ~15u); + + D3D11_BUFFER_DESC cbDesc {}; + cbDesc.ByteWidth = cbSize; + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + + D3D11_SUBRESOURCE_DATA initData {}; + initData.pSysMem = ub.data.data(); + + ComPtr cb; + HRESULT hr = device->CreateBuffer (&cbDesc, &initData, cb.ReleaseAndGetAddressOf()); + if (FAILED (hr) || cb == nullptr) + continue; + + UINT slot = static_cast (ub.group * 16 + ub.binding); + context->CSSetConstantBuffers (slot, 1, cb.GetAddressOf()); + tempBuffers.push_back (std::move (cb)); + } + + context->Dispatch (groupsX, groupsY, groupsZ); + return true; + } + + //========================================================================== + + void finish() override + { + // Leaving the storage buffers bound as UAVs makes any later read of them — + // a staging copy in GpuDevice::readBuffer(), or a draw sampling the same + // resource — a simultaneous write/read binding, which the debug layer + // flags. Only the slots this pass actually bound are cleared, so the + // render context's own UAV state is left alone. + if (context != nullptr) + { + ID3D11UnorderedAccessView* nullUav[1] = { nullptr }; + + for (auto slot : boundUavSlots) + context->CSSetUnorderedAccessViews (slot, 1, nullUav, nullptr); + } + + boundUavSlots.clear(); + tempBuffers.clear(); + device = nullptr; + context = nullptr; + } + +private: + /** Records a UAV slot for unbinding in finish(), ignoring repeats across dispatches. */ + void rememberBoundUavSlot (UINT slot) + { + for (auto existing : boundUavSlots) + { + if (existing == slot) + return; + } + + boundUavSlots.push_back (slot); + } + + ID3D11Device* device; + ID3D11DeviceContext* context; + std::vector> tempBuffers; + std::vector boundUavSlots; +}; + +//============================================================================== + +std::unique_ptr yup_createComputePassImplD3D11 (GpuDevice& ctx) +{ + auto& d3dCtx = static_cast (ctx); + return std::make_unique (d3dCtx.getD3DDevice(), + d3dCtx.getD3DDeviceContext()); +} + +} // namespace yup + +#endif // YUP_RIVE_USE_D3D diff --git a/modules/yup_rhi/native/yup_GpuComputePass_metal.cpp b/modules/yup_rhi/native/yup_GpuComputePass_metal.cpp new file mode 100644 index 000000000..fbf53ac44 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuComputePass_metal.cpp @@ -0,0 +1,164 @@ +/* + ============================================================================== + + 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 && (YUP_MAC || YUP_IOS) + +namespace yup +{ + +//============================================================================== + +class GpuComputePassImplMetal final : public GpuComputePass::Impl +{ +public: + GpuComputePassImplMetal (id device, id queue) + : device (device) + { + if (device == nil || queue == nil) + return; + + YUP_AUTORELEASEPOOL + { + commandBuffer = [queue commandBuffer]; + if (commandBuffer == nil) + return; + + encoder = [commandBuffer computeCommandEncoder]; + if (encoder == nil) + commandBuffer = nil; + } + } + + ~GpuComputePassImplMetal() override + { + if (! finished) + finish(); + } + + //========================================================================== + + bool isValid() const override + { + return encoder != nil; + } + + //========================================================================== + + bool dispatch (uint32_t groupsX, uint32_t groupsY, uint32_t groupsZ) override + { + if (encoder == nil || pipelineRef == nullptr) + return false; + + auto* pipe = dynamic_cast (pipelineRef.get()); + if (pipe == nullptr || pipe->getPipelineState() == nil) + return false; + + YUP_AUTORELEASEPOOL + { + [encoder setComputePipelineState:pipe->getPipelineState()]; + + for (auto& sb : storageBindings) + { + if (sb.buffer == nullptr) + continue; + + auto* bufImpl = sb.buffer->getImpl(); + if (bufImpl == nullptr || bufImpl->mtlStorageBuffer == nil) + continue; + + [encoder setBuffer:bufImpl->mtlStorageBuffer + offset:0 + atIndex:static_cast (sb.group * 16 + sb.binding)]; + } + + for (auto& ub : uboBindings) + { + if (ub.data.empty()) + continue; + + NSUInteger idx = static_cast (ub.group * 16 + ub.binding); + + id tmp = [device newBufferWithBytes:ub.data.data() + length:ub.data.size() + options:MTLResourceStorageModeShared]; + if (tmp != nil) + { + [encoder setBuffer:tmp offset:0 atIndex:idx]; + tempBuffers.push_back (tmp); + } + } + + auto wgs = pipe->getWorkgroupSize(); + MTLSize tgSize = MTLSizeMake (wgs.x, wgs.y, wgs.z); + MTLSize tgCount = MTLSizeMake (groupsX, groupsY, groupsZ); + + [encoder dispatchThreadgroups:tgCount threadsPerThreadgroup:tgSize]; + } + + return true; + } + + //========================================================================== + + void finish() override + { + YUP_AUTORELEASEPOOL + { + if (encoder != nil) + { + [encoder endEncoding]; + encoder = nil; + } + + if (commandBuffer != nil) + { + [commandBuffer commit]; + commandBuffer = nil; + } + + tempBuffers.clear(); + } + } + +private: + id device = nil; + id commandBuffer = nil; + id encoder = nil; + std::vector> tempBuffers; +}; + +//============================================================================== + +std::unique_ptr yup_createComputePassImplMetal (GpuDevice& ctx) +{ + auto& metalCtx = static_cast (ctx); + + auto impl = std::make_unique (metalCtx.getDevice(), + metalCtx.getCommandQueue()); + if (! impl->isValid()) + return nullptr; + + return impl; +} + +} // namespace yup + +#endif // YUP_RIVE_USE_METAL diff --git a/modules/yup_rhi/native/yup_GpuComputePass_opengl.cpp b/modules/yup_rhi/native/yup_GpuComputePass_opengl.cpp new file mode 100644 index 000000000..7bc6521b9 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuComputePass_opengl.cpp @@ -0,0 +1,114 @@ +/* + ============================================================================== + + 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_ANDROID + +namespace yup +{ + +//============================================================================== + +class GpuComputePassImplGL final : public GpuComputePass::Impl +{ +public: + bool isValid() const override { return true; } + + //========================================================================== + + bool dispatch (uint32_t groupsX, uint32_t groupsY, uint32_t groupsZ) override + { + if (pipelineRef == nullptr) + return false; + + auto* pipe = dynamic_cast (pipelineRef.get()); + if (pipe == nullptr || pipe->getProgram() == 0) + return false; + + glUseProgram (pipe->getProgram()); + + for (auto& sb : storageBindings) + { + if (sb.buffer == nullptr) + continue; + + auto* bufImpl = sb.buffer->getImpl(); + if (bufImpl == nullptr || bufImpl->glBuffer == 0) + continue; + + GLuint index = static_cast (sb.group * 16 + sb.binding); + glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, bufImpl->glBuffer); + } + + for (auto& ub : uboBindings) + { + if (ub.data.empty()) + continue; + + GLuint ubo = 0; + glGenBuffers (1, &ubo); + if (ubo == 0) + continue; + + glBindBuffer (GL_UNIFORM_BUFFER, ubo); + glBufferData (GL_UNIFORM_BUFFER, + static_cast (ub.data.size()), + ub.data.data(), + GL_DYNAMIC_DRAW); + + GLuint index = static_cast (ub.group * 16 + ub.binding); + glBindBufferBase (GL_UNIFORM_BUFFER, index, ubo); + + tempBuffers.push_back (ubo); + } + + glDispatchCompute (groupsX, groupsY, groupsZ); + return true; + } + + //========================================================================== + + void finish() override + { + if (! tempBuffers.empty()) + { + glDeleteBuffers (static_cast (tempBuffers.size()), tempBuffers.data()); + tempBuffers.clear(); + } + + glMemoryBarrier (GL_SHADER_STORAGE_BARRIER_BIT + | GL_UNIFORM_BARRIER_BIT + | GL_BUFFER_UPDATE_BARRIER_BIT); + } + +private: + std::vector tempBuffers; +}; + +//============================================================================== + +std::unique_ptr yup_createComputePassImplGL (GpuDevice&) +{ + return std::make_unique(); +} + +} // namespace yup + +#endif // OpenGL diff --git a/modules/yup_rhi/native/yup_GpuComputePass_webgpu.cpp b/modules/yup_rhi/native/yup_GpuComputePass_webgpu.cpp new file mode 100644 index 000000000..c4a8ee3e1 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuComputePass_webgpu.cpp @@ -0,0 +1,180 @@ +/* + ============================================================================== + + 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) || YUP_RIVE_USE_DAWN + +namespace yup +{ + +//============================================================================== + +class GpuComputePassImplWebGPU final : public GpuComputePass::Impl +{ +public: + GpuComputePassImplWebGPU (wgpu::Device dev, wgpu::Queue q) + : device (dev) + , queue (q) + { + if (device == nullptr || queue == nullptr) + return; + + wgpu::CommandEncoderDescriptor encDesc {}; + encDesc.label = "GpuComputePass"; + encoder = device.CreateCommandEncoder (&encDesc); + if (encoder == nullptr) + return; + + wgpu::ComputePassDescriptor passDesc {}; + passDesc.label = "GpuComputePass"; + pass = encoder.BeginComputePass (&passDesc); + } + + bool isValid() const override { return pass != nullptr; } + + //========================================================================== + + bool dispatch (uint32_t groupsX, uint32_t groupsY, uint32_t groupsZ) override + { + if (pass == nullptr || pipelineRef == nullptr) + return false; + + auto* pipe = dynamic_cast (pipelineRef.get()); + if (pipe == nullptr || pipe->getPipeline() == nullptr) + return false; + + pass.SetPipeline (pipe->getPipeline()); + + std::vector entries; + + for (auto& sb : storageBindings) + { + if (sb.buffer == nullptr) + continue; + + auto* bufImpl = sb.buffer->getImpl(); + if (bufImpl == nullptr || bufImpl->webgpuStorageBuffer == nullptr) + continue; + + wgpu::BindGroupEntry e {}; + e.binding = static_cast (sb.binding); + e.buffer = bufImpl->webgpuStorageBuffer; + e.offset = 0; + e.size = bufImpl->webgpuStorageBuffer.GetSize(); + entries.push_back (e); + } + + for (auto& ub : uboBindings) + { + if (ub.data.empty()) + continue; + + wgpu::BufferDescriptor bd {}; + bd.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst; + bd.size = (ub.data.size() + 15) & ~15u; + bd.label = "GpuComputePass UBO"; + + wgpu::Buffer buf = device.CreateBuffer (&bd); + if (buf == nullptr) + continue; + + queue.WriteBuffer (buf, 0, ub.data.data(), ub.data.size()); + + wgpu::BindGroupEntry e {}; + e.binding = static_cast (ub.binding); + e.buffer = buf; + e.offset = 0; + e.size = ub.data.size(); + entries.push_back (e); + + tempUniformBuffers.push_back (std::move (buf)); + } + + if (! entries.empty()) + { + // When the pipeline was created with an implicit layout (the default), + // we must use its layout for bind groups — not a manually-built one. + wgpu::BindGroupLayout layout = pipe->getPipeline().GetBindGroupLayout (0); + if (layout != nullptr) + { + wgpu::BindGroupDescriptor bgDesc {}; + bgDesc.layout = layout; + bgDesc.entryCount = entries.size(); + bgDesc.entries = entries.data(); + + wgpu::BindGroup bg = device.CreateBindGroup (&bgDesc); + if (bg != nullptr) + pass.SetBindGroup (0, bg, 0, nullptr); + } + } + + pass.DispatchWorkgroups (groupsX, groupsY, groupsZ); + return true; + } + + //========================================================================== + + void finish() override + { + if (pass != nullptr) + { + pass.End(); + pass = nullptr; + } + + if (encoder != nullptr) + { + wgpu::CommandBuffer commands = encoder.Finish(); + encoder = nullptr; + + if (commands != nullptr) + { + wgpu::CommandBuffer cmds[] = { commands }; + queue.Submit (1, cmds); + } + } + + tempUniformBuffers.clear(); + } + +private: + wgpu::Device device; + wgpu::Queue queue; + wgpu::CommandEncoder encoder; + wgpu::ComputePassEncoder pass; + std::vector tempUniformBuffers; +}; + +//============================================================================== + +std::unique_ptr yup_createComputePassImplWebGPU (GpuDevice& ctx) +{ +#if YUP_EMSCRIPTEN && RIVE_WEBGPU + auto& wc = static_cast (ctx); + return std::make_unique (wc.getWgpuDevice(), wc.getWgpuQueue()); +#elif YUP_RIVE_USE_DAWN + auto& dc = static_cast (ctx); + return std::make_unique (dc.getDevice(), dc.getQueue()); +#endif +} + +} // namespace yup + +#endif // WebGPU / Dawn diff --git a/modules/yup_rhi/native/yup_GpuComputePipeline_d3d.cpp b/modules/yup_rhi/native/yup_GpuComputePipeline_d3d.cpp new file mode 100644 index 000000000..ce3ce8143 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuComputePipeline_d3d.cpp @@ -0,0 +1,71 @@ +/* + ============================================================================== + + 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 && YUP_WINDOWS + +namespace yup +{ + +//============================================================================== + +class GpuComputePipelineD3D11 final : public GpuComputePipeline +{ +public: + GpuComputePipelineD3D11 (ComPtr shader, GpuWorkgroupSize wgs) + : computeShader (std::move (shader)) + , workgroupSize (wgs) + { + } + + GpuWorkgroupSize getWorkgroupSize() const noexcept override { return workgroupSize; } + + ID3D11ComputeShader* getComputeShader() const noexcept { return computeShader.Get(); } + +private: + ComPtr computeShader; + GpuWorkgroupSize workgroupSize; +}; + +//============================================================================== + +ResultValue yup_constructComputePipelineD3D11 (GpuDevice& ctx, + const GpuShaderSource& source, + const GpuWorkgroupSize& workgroupSize) +{ + if (source.code == nullptr || source.codeSize == 0) + return makeResultValueFail ("Compute shader source is empty"); + + if (source.language != GpuShaderLanguage::hlsl) + return makeResultValueFail ("D3D11 compute shaders must be HLSL"); + + auto& d3dCtx = static_cast (ctx); + + ComPtr computeShader; + HRESULT hr = d3dCtx.getD3DDevice()->CreateComputeShader (source.code, source.codeSize, nullptr, computeShader.ReleaseAndGetAddressOf()); + if (FAILED (hr) || computeShader == nullptr) + return makeResultValueFail ("D3D11 compute shader compilation failed"); + + return makeResultValueOk (GpuComputePipeline::Ptr (new GpuComputePipelineD3D11 (std::move (computeShader), workgroupSize))); +} + +} // namespace yup + +#endif // YUP_RIVE_USE_D3D diff --git a/modules/yup_rhi/native/yup_GpuComputePipeline_metal.cpp b/modules/yup_rhi/native/yup_GpuComputePipeline_metal.cpp new file mode 100644 index 000000000..ddc93fadc --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuComputePipeline_metal.cpp @@ -0,0 +1,102 @@ +/* + ============================================================================== + + 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 && (YUP_MAC || YUP_IOS) + +namespace yup +{ + +//============================================================================== + +class GpuComputePipelineMetal final : public GpuComputePipeline +{ +public: + GpuComputePipelineMetal (id state, GpuWorkgroupSize wgs) + : pipelineState (state) + , workgroupSize (wgs) + { + } + + GpuWorkgroupSize getWorkgroupSize() const noexcept override { return workgroupSize; } + + id getPipelineState() const noexcept { return pipelineState; } + +private: + id pipelineState; + GpuWorkgroupSize workgroupSize; +}; + +//============================================================================== + +ResultValue yup_constructComputePipelineMetal (GpuDevice& ctx, + const GpuShaderSource& source, + const GpuWorkgroupSize& workgroupSize) +{ + if (source.code == nullptr || source.codeSize == 0) + return makeResultValueFail ("Compute shader source is empty"); + + auto& metalCtx = static_cast (ctx); + id device = metalCtx.getDevice(); + + NSString* mslSource = [[NSString alloc] initWithBytes:source.code + length:source.codeSize + encoding:NSUTF8StringEncoding]; + if (mslSource == nil) + return makeResultValueFail ("Failed to create MSL source string"); + + MTLCompileOptions* compileOptions = [[MTLCompileOptions alloc] init]; + + NSError* error = nil; + id library = [device newLibraryWithSource:mslSource + options:compileOptions + error:&error]; + + if (library == nil) + { + String errMsg = "Metal compute shader compilation failed: "; + errMsg += error != nil ? [error.localizedDescription UTF8String] : "unknown error"; + return makeResultValueFail (errMsg); + } + + const char* entryPointName = source.entryPoint != nullptr ? source.entryPoint : "main0"; + NSString* entryPoint = [NSString stringWithUTF8String:entryPointName]; + + id function = [library newFunctionWithName:entryPoint]; + + if (function == nil) + return makeResultValueFail (String ("Metal compute function not found: ") + entryPointName); + + id pipelineState = [device newComputePipelineStateWithFunction:function + error:&error]; + + if (pipelineState == nil) + { + String errMsg = "Metal compute pipeline creation failed: "; + errMsg += error != nil ? [error.localizedDescription UTF8String] : "unknown error"; + return makeResultValueFail (errMsg); + } + + return makeResultValueOk (GpuComputePipeline::Ptr (new GpuComputePipelineMetal (pipelineState, workgroupSize))); +} + +} // namespace yup + +#endif // YUP_RIVE_USE_METAL diff --git a/modules/yup_rhi/native/yup_GpuComputePipeline_opengl.cpp b/modules/yup_rhi/native/yup_GpuComputePipeline_opengl.cpp new file mode 100644 index 000000000..9f826ba81 --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuComputePipeline_opengl.cpp @@ -0,0 +1,122 @@ +/* + ============================================================================== + + 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_ANDROID + +namespace yup +{ + +//============================================================================== + +class GpuComputePipelineGL final : public GpuComputePipeline +{ +public: + GpuComputePipelineGL (GLuint program, GpuWorkgroupSize wgs) + : glProgram (program) + , workgroupSize (wgs) + { + } + + ~GpuComputePipelineGL() override + { + if (glProgram != 0) + glDeleteProgram (glProgram); + } + + GpuWorkgroupSize getWorkgroupSize() const noexcept override { return workgroupSize; } + + GLuint getProgram() const noexcept { return glProgram; } + +private: + GLuint glProgram; + GpuWorkgroupSize workgroupSize; +}; + +//============================================================================== + +ResultValue yup_constructComputePipelineGL (const GpuShaderSource& source, + const GpuWorkgroupSize& workgroupSize) +{ + if (source.code == nullptr || source.codeSize == 0) + return makeResultValueFail ("Compute shader source is empty"); + + if (source.language != GpuShaderLanguage::glsl) + return makeResultValueFail ("OpenGL compute shaders must be GLSL"); + + const auto* glslSource = static_cast (source.code); + auto glslLength = static_cast (source.codeSize); + + GLuint shader = glCreateShader (GL_COMPUTE_SHADER); + if (shader == 0) + return makeResultValueFail ("Failed to create GL compute shader object"); + + glShaderSource (shader, 1, &glslSource, &glslLength); + glCompileShader (shader); + + GLint compiled = GL_FALSE; + glGetShaderiv (shader, GL_COMPILE_STATUS, &compiled); + if (compiled != GL_TRUE) + { + GLint logLen = 0; + glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &logLen); + String errMsg = "GL compute shader compilation failed"; + if (logLen > 1) + { + std::vector log (static_cast (logLen)); + glGetShaderInfoLog (shader, logLen, nullptr, log.data()); + errMsg += ": "; + errMsg += log.data(); + } + glDeleteShader (shader); + return makeResultValueFail (errMsg); + } + + GLuint program = glCreateProgram(); + glAttachShader (program, shader); + glLinkProgram (program); + + GLint linked = GL_FALSE; + glGetProgramiv (program, GL_LINK_STATUS, &linked); + if (linked != GL_TRUE) + { + GLint logLen = 0; + glGetProgramiv (program, GL_INFO_LOG_LENGTH, &logLen); + String errMsg = "GL compute program linking failed"; + if (logLen > 1) + { + std::vector log (static_cast (logLen)); + glGetProgramInfoLog (program, logLen, nullptr, log.data()); + errMsg += ": "; + errMsg += log.data(); + } + glDeleteShader (shader); + glDeleteProgram (program); + return makeResultValueFail (errMsg); + } + + glDeleteShader (shader); + + return makeResultValueOk (GpuComputePipeline::Ptr (new GpuComputePipelineGL (program, workgroupSize))); +} + +} // namespace yup + +#endif // OpenGL diff --git a/modules/yup_rhi/native/yup_GpuComputePipeline_webgpu.cpp b/modules/yup_rhi/native/yup_GpuComputePipeline_webgpu.cpp new file mode 100644 index 000000000..3e37aef8e --- /dev/null +++ b/modules/yup_rhi/native/yup_GpuComputePipeline_webgpu.cpp @@ -0,0 +1,94 @@ +/* + ============================================================================== + + 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) || YUP_RIVE_USE_DAWN + +namespace yup +{ + +//============================================================================== + +class GpuComputePipelineWebGPU final : public GpuComputePipeline +{ +public: + GpuComputePipelineWebGPU (wgpu::ComputePipeline pipeline, GpuWorkgroupSize wgs) + : computePipeline (std::move (pipeline)) + , workgroupSize (wgs) + { + } + + GpuWorkgroupSize getWorkgroupSize() const noexcept override { return workgroupSize; } + + wgpu::ComputePipeline getPipeline() const noexcept { return computePipeline; } + +private: + wgpu::ComputePipeline computePipeline; + GpuWorkgroupSize workgroupSize; +}; + +//============================================================================== + +ResultValue yup_constructComputePipelineWebGPU (GpuDevice& ctx, + const GpuShaderSource& source, + const GpuWorkgroupSize& workgroupSize) +{ + if (source.code == nullptr || source.codeSize == 0) + return makeResultValueFail ("Compute shader source is empty"); + + if (source.language != GpuShaderLanguage::wgsl) + return makeResultValueFail ("WebGPU compute shaders must be WGSL"); + + const char* wgslSource = static_cast (source.code); + const char* entryPointName = source.entryPoint != nullptr ? source.entryPoint : "main"; + + wgpu::Device device; +#if YUP_EMSCRIPTEN && RIVE_WEBGPU + device = static_cast (ctx).getWgpuDevice(); +#elif YUP_RIVE_USE_DAWN + device = static_cast (ctx).getDevice(); +#endif + + wgpu::ShaderSourceWGSL wgslDesc {}; + wgslDesc.code = wgslSource; + + wgpu::ShaderModuleDescriptor shaderModuleDesc {}; + shaderModuleDesc.nextInChain = &wgslDesc; + shaderModuleDesc.label = "GpuComputePipeline CS"; + + wgpu::ShaderModule shaderModule = device.CreateShaderModule (&shaderModuleDesc); + if (shaderModule == nullptr) + return makeResultValueFail ("WebGPU compute shader module creation failed"); + + wgpu::ComputePipelineDescriptor pipelineDesc {}; + pipelineDesc.label = "GpuComputePipeline"; + pipelineDesc.compute.module = shaderModule; + pipelineDesc.compute.entryPoint = entryPointName; + + wgpu::ComputePipeline computePipeline = device.CreateComputePipeline (&pipelineDesc); + if (computePipeline == nullptr) + return makeResultValueFail ("WebGPU compute pipeline creation failed"); + + return makeResultValueOk (GpuComputePipeline::Ptr (new GpuComputePipelineWebGPU (std::move (computePipeline), workgroupSize))); +} + +} // namespace yup + +#endif // WebGPU / Dawn diff --git a/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp b/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp index 2068f8053..51de34230 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp @@ -23,7 +23,9 @@ #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 @@ -32,35 +34,174 @@ namespace yup class GpuDeviceD3D : public GpuDevice { public: - GpuDeviceD3D (ComPtr gpu, - ComPtr gpuContext, + GpuDeviceD3D (ComPtr gpuToUse, + ComPtr gpuContextToUse, 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())) + : options (options) + , renderContextOptions (contextOptions) + , gpu (std::move (gpuToUse)) + , gpuContext (std::move (gpuContextToUse)) + , renderContext (rive::gpu::RenderContextD3DImpl::MakeContext (gpu, gpuContext, renderContextOptions)) + , oreContext (rive::ore::ContextD3D11::Make (gpu.Get(), gpuContext.Get())) { } + ~GpuDeviceD3D() override { releasePooledResources(); } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Direct3D; } - rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } + rive::gpu::RenderContext* getRenderContext() const override { return renderContext.get(); } + + rive::ore::Context* getGpuContext() const noexcept override { return oreContext.get(); } bool isComputeAvailable() const noexcept override { return true; } + /** Returns the native ID3D11Device for compute operations. */ + ID3D11Device* getD3DDevice() const noexcept { return gpu.Get(); } + + /** Returns the native ID3D11DeviceContext for compute operations. */ + ID3D11DeviceContext* getD3DDeviceContext() const noexcept { return gpuContext.Get(); } + + //============================================================================== + + ReferenceCountedObjectPtr createBuffer (GpuBufferType type, const void* data, size_t byteSize) override + { + if (type == GpuBufferType::storage) + { + jassert (data != nullptr && byteSize > 0); + if (data == nullptr || byteSize == 0) + return nullptr; + + D3D11_BUFFER_DESC bufDesc {}; + bufDesc.ByteWidth = static_cast (byteSize); + bufDesc.Usage = D3D11_USAGE_DEFAULT; + bufDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE; + bufDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS; + bufDesc.StructureByteStride = 0; + + D3D11_SUBRESOURCE_DATA initData {}; + initData.pSysMem = data; + + ComPtr d3dBuffer; + HRESULT hr = gpu->CreateBuffer (&bufDesc, &initData, d3dBuffer.ReleaseAndGetAddressOf()); + if (FAILED (hr) || d3dBuffer == nullptr) + return nullptr; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc {}; + uavDesc.Format = DXGI_FORMAT_R32_TYPELESS; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; + uavDesc.Buffer.NumElements = static_cast (byteSize / 4); + uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW; + + ComPtr uav; + hr = gpu->CreateUnorderedAccessView (d3dBuffer.Get(), &uavDesc, uav.ReleaseAndGetAddressOf()); + if (FAILED (hr) || uav == nullptr) + return nullptr; + + return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, std::move (d3dBuffer), std::move (uav) }); + } + + return GpuDevice::createBuffer (type, data, byteSize); + } + + //============================================================================== + + bool readBuffer (GpuBuffer::Ptr buffer, void* dst, size_t dstSize) override + { + if (buffer == nullptr || dst == nullptr) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr || impl->d3dStorageBuffer == nullptr) + return false; + + const auto byteSize = buffer->getSizeInBytes(); + if (dstSize < byteSize) + return false; + + // The storage buffer is D3D11_USAGE_DEFAULT and so not CPU accessible; a + // staging copy is the only way to reach its contents. + if (impl->d3dReadbackStaging == nullptr) + { + D3D11_BUFFER_DESC stagingDesc {}; + stagingDesc.ByteWidth = static_cast (byteSize); + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.BindFlags = 0; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + stagingDesc.MiscFlags = 0; + stagingDesc.StructureByteStride = 0; + + HRESULT hr = gpu->CreateBuffer (&stagingDesc, nullptr, impl->d3dReadbackStaging.ReleaseAndGetAddressOf()); + if (FAILED (hr) || impl->d3dReadbackStaging == nullptr) + return false; + } + + // The compute dispatch was issued on this same immediate context, so the + // copy is ordered after it, and Map (without DO_NOT_WAIT) blocks until the + // copy has retired. D3D11 can therefore read back in lockstep and always + // hand the caller current data. + gpuContext->CopyResource (impl->d3dReadbackStaging.Get(), impl->d3dStorageBuffer.Get()); + + D3D11_MAPPED_SUBRESOURCE mapped {}; + HRESULT hr = gpuContext->Map (impl->d3dReadbackStaging.Get(), 0, D3D11_MAP_READ, 0, &mapped); + if (FAILED (hr) || mapped.pData == nullptr) + return false; + + memcpy (dst, mapped.pData, byteSize); + gpuContext->Unmap (impl->d3dReadbackStaging.Get(), 0); + return true; + } + + //============================================================================== + + bool updateBuffer (GpuBuffer::Ptr buffer, const void* data, size_t byteSize) override + { + if (buffer == nullptr || data == nullptr || byteSize == 0) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr) + return false; + + // For ore-backed buffers (vertex, index, uniform), delegate to base class. + if (impl->d3dStorageBuffer == nullptr) + return GpuDevice::updateBuffer (buffer, data, byteSize); + + const auto fullSize = buffer->getSizeInBytes(); + if (byteSize > fullSize) + return false; + + if (byteSize == fullSize) + { + gpuContext->UpdateSubresource (impl->d3dStorageBuffer.Get(), 0, nullptr, data, static_cast (byteSize), 0); + } + else + { + D3D11_BOX box { 0, 0, 0, static_cast (byteSize), 1, 1 }; + gpuContext->UpdateSubresource (impl->d3dStorageBuffer.Get(), 0, &box, data, static_cast (byteSize), 0); + } + + return true; + } + //============================================================================== struct OffscreenContextSlot { std::unique_ptr renderContext; bool frameActive = false; + bool leased = false; }; struct OffscreenTargetD3D : public RenderableTarget { + ~OffscreenTargetD3D() override + { + if (contextSlot != nullptr) + contextSlot->leased = false; + } + int width = 0; int height = 0; ComPtr stagingTexture; @@ -108,7 +249,7 @@ class GpuDeviceD3D : public GpuDevice stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; ComPtr staging; - auto hr = m_gpu->CreateTexture2D (&stagingDesc, nullptr, staging.ReleaseAndGetAddressOf()); + auto hr = gpu->CreateTexture2D (&stagingDesc, nullptr, staging.ReleaseAndGetAddressOf()); if (FAILED (hr)) return nullptr; return staging; @@ -116,7 +257,7 @@ class GpuDeviceD3D : public GpuDevice std::unique_ptr createOffscreenTarget (int width, int height) override { - if (width <= 0 || height <= 0 || m_renderContext == nullptr) + if (width <= 0 || height <= 0 || renderContext == nullptr) return nullptr; auto target = std::make_unique(); @@ -125,8 +266,8 @@ class GpuDeviceD3D : public GpuDevice target->renderContext = nullptr; target->contextSlot = nullptr; - target->renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), - static_cast (height)); + target->renderCanvas = renderContext->makeRenderCanvas (static_cast (width), + static_cast (height)); if (target->renderCanvas == nullptr) return nullptr; @@ -192,11 +333,29 @@ class GpuDeviceD3D : public GpuDevice renderContext->flush (flushDesc); if (auto* renderTarget = static_cast (target.getRenderTarget())) - m_gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); + gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); target.contextSlot->frameActive = false; } + bool clearOffscreen (OffscreenTarget& baseTarget, GpuColor color) override + { + auto& target = static_cast (baseTarget); + + auto* renderTarget = static_cast (target.getRenderTarget()); + if (renderTarget == nullptr || renderTarget->targetTexture() == nullptr) + return false; + + ComPtr renderTargetView; + if (FAILED (gpu->CreateRenderTargetView (renderTarget->targetTexture(), nullptr, renderTargetView.ReleaseAndGetAddressOf()))) + return false; + + const FLOAT rgba[4] { color.red, color.green, color.blue, color.alpha }; + gpuContext->ClearRenderTargetView (renderTargetView.Get(), rgba); + + return true; + } + bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override { auto& target = static_cast (baseTarget); @@ -211,11 +370,11 @@ class GpuDeviceD3D : public GpuDevice if (target.getRenderContext() == nullptr) { if (auto* renderTarget = static_cast (target.getRenderTarget())) - m_gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); + 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); + HRESULT hr = gpuContext->Map (target.stagingTexture.Get(), 0, D3D11_MAP_READ, 0, &mapped); if (FAILED (hr)) return false; @@ -229,40 +388,63 @@ class GpuDeviceD3D : public GpuDevice bytesPerRow); } - m_gpuContext->Unmap (target.stagingTexture.Get(), 0); + gpuContext->Unmap (target.stagingTexture.Get(), 0); return true; } private: OffscreenContextSlot* acquireOffscreenContext() { - for (const auto& slot : m_offscreenContextPool) + for (const auto& slot : offscreenContextPool) { - if (! slot->frameActive) + if (! slot->leased) + { + slot->leased = true; return slot.get(); + } } auto slot = std::make_unique(); - slot->renderContext = rive::gpu::RenderContextD3DImpl::MakeContext (m_gpu, m_gpuContext, m_renderContextOptions); + slot->renderContext = rive::gpu::RenderContextD3DImpl::MakeContext (gpu, gpuContext, renderContextOptions); if (slot->renderContext == nullptr) return nullptr; + slot->leased = true; + auto* result = slot.get(); - m_offscreenContextPool.push_back (std::move (slot)); + 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; + Options options; + rive::gpu::D3DContextOptions renderContextOptions; + ComPtr gpu; + ComPtr gpuContext; + std::unique_ptr renderContext; + std::vector> offscreenContextPool; + std::unique_ptr oreContext; }; //============================================================================== +ID3D11Device* yup_getDirect3DDevice (GpuDevice& gpuDevice) +{ + if (gpuDevice.getPlatform() != GpuPlatform::Direct3D) + return nullptr; + + return static_cast (gpuDevice).getD3DDevice(); +} + +ID3D11DeviceContext* yup_getDirect3DDeviceContext (GpuDevice& gpuDevice) +{ + if (gpuDevice.getPlatform() != GpuPlatform::Direct3D) + return nullptr; + + return static_cast (gpuDevice).getD3DDeviceContext(); +} + +//============================================================================== + std::unique_ptr yup_constructDirect3DGpuDevice (GpuDevice::Options fiddleOptions) { ComPtr adapter; diff --git a/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp b/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp index a92e7d4d8..6bf196631 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp @@ -66,17 +66,17 @@ class GpuDeviceDawn : public GpuDevice { public: GpuDeviceDawn (Options options) - : m_options (options) + : options (options) { WGPUInstanceDescriptor instanceDescriptor {}; instanceDescriptor.features.timedWaitAnyEnable = true; - m_instance = std::make_unique (&instanceDescriptor); + instance = std::make_unique (&instanceDescriptor); wgpu::RequestAdapterOptions adapterOptions = { .powerPreference = wgpu::PowerPreference::HighPerformance, }; - auto adapters = m_instance->EnumerateAdapters (&adapterOptions); + auto adapters = instance->EnumerateAdapters (&adapterOptions); wgpu::DawnAdapterPropertiesPowerPreference power_props {}; wgpu::AdapterProperties adapterProperties {}; @@ -118,23 +118,72 @@ class GpuDeviceDawn : public GpuDevice .requiredFeatures = requiredFeatures.data(), }; - m_backendDevice = preferredAdapter->CreateDevice (&deviceDesc); + 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); + backendProcs.deviceSetUncapturedErrorCallback (backendDevice, print_device_error, nullptr); + backendProcs.deviceSetDeviceLostCallback (backendDevice, device_lost_callback, nullptr); - m_device = wgpu::Device::Acquire (m_backendDevice); - m_queue = m_device.GetQueue(); + device = wgpu::Device::Acquire (backendDevice); + queue = device.GetQueue(); } + ~GpuDeviceDawn() override { releasePooledResources(); } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::WebGPU; } - rive::ore::Context* gpuContext() const noexcept override { return nullptr; } + rive::gpu::RenderContext* getRenderContext() const override { return nullptr; } + + rive::ore::Context* getGpuContext() const noexcept override { return nullptr; } bool isComputeAvailable() const noexcept override { return true; } + ReferenceCountedObjectPtr createBuffer (GpuBufferType type, const void* data, size_t byteSize) override + { + if (type == GpuBufferType::storage) + { + jassert (data != nullptr && byteSize > 0); + if (data == nullptr || byteSize == 0) + return nullptr; + + wgpu::BufferDescriptor bufDesc {}; + bufDesc.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst; + bufDesc.size = byteSize; + bufDesc.label = "GpuBuffer storage"; + + wgpu::Buffer wgpuBuffer = device.CreateBuffer (&bufDesc); + if (wgpuBuffer == nullptr) + return nullptr; + + queue.WriteBuffer (wgpuBuffer, 0, data, byteSize); + + return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, std::move (wgpuBuffer) }); + } + + return GpuDevice::createBuffer (type, data, byteSize); + } + + bool updateBuffer (GpuBuffer::Ptr buffer, const void* data, size_t byteSize) override + { + if (buffer == nullptr || data == nullptr || byteSize == 0) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr) + return false; + + // For ore-backed buffers (vertex, index, uniform), delegate to base class. + if (impl->webgpuStorageBuffer == nullptr) + return GpuDevice::updateBuffer (buffer, data, byteSize); + + if (byteSize > buffer->getSizeInBytes()) + return false; + + queue.WriteBuffer (impl->webgpuStorageBuffer, 0, data, byteSize); + return true; + } + // Dawn doesn't support PLS offscreen targets through ore yet. std::unique_ptr createOffscreenTarget (int, int) override { return nullptr; } @@ -147,20 +196,20 @@ class GpuDeviceDawn : public GpuDevice bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override { return false; } /** Returns the native WGPU device for compute operations. */ - WGPUDevice getBackendDevice() const noexcept { return m_backendDevice; } + WGPUDevice getBackendDevice() const noexcept { return backendDevice; } /** Returns the wgpu::Device for compute operations. */ - wgpu::Device getDevice() const noexcept { return m_device; } + wgpu::Device getDevice() const noexcept { return device; } /** Returns the wgpu::Queue for compute operations. */ - wgpu::Queue getQueue() const noexcept { return m_queue; } + wgpu::Queue getQueue() const noexcept { return queue; } private: - Options m_options; - WGPUDevice m_backendDevice = {}; - wgpu::Device m_device = {}; - wgpu::Queue m_queue = {}; - std::unique_ptr m_instance; + Options options; + WGPUDevice backendDevice = {}; + wgpu::Device device = {}; + wgpu::Queue queue = {}; + std::unique_ptr instance; }; //============================================================================== diff --git a/modules/yup_rhi/native/yup_GpuDevice_headless.cpp b/modules/yup_rhi/native/yup_GpuDevice_headless.cpp index c4ef8ecf0..59ecbdef4 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_headless.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_headless.cpp @@ -29,6 +29,8 @@ class HeadlessGpuDevice : public GpuDevice public: HeadlessGpuDevice() = default; + ~HeadlessGpuDevice() override { releasePooledResources(); } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Headless; } std::unique_ptr createOffscreenTarget (int, int) override diff --git a/modules/yup_rhi/native/yup_GpuDevice_metal.cpp b/modules/yup_rhi/native/yup_GpuDevice_metal.cpp index 33502c5e3..ef6c56ada 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_metal.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_metal.cpp @@ -35,30 +35,136 @@ class GpuDeviceMetal : public GpuDevice { public: GpuDeviceMetal (GpuDevice::Options options) - : fiddleOptions (options) + : options (options) { - rive::gpu::RenderContextMetalImpl::ContextOptions renderCtxOpts; + rive::gpu::RenderContextMetalImpl::ContextOptions renderContexOptions; - if (fiddleOptions.synchronousShaderCompilations) - renderCtxOpts.shaderCompilationMode = rive::gpu::ShaderCompilationMode::alwaysSynchronous; + if (options.synchronousShaderCompilations) + renderContexOptions.shaderCompilationMode = rive::gpu::ShaderCompilationMode::alwaysSynchronous; - if (fiddleOptions.disableRasterOrdering) - renderCtxOpts.disableFramebufferReads = true; + if (options.disableRasterOrdering) + renderContexOptions.disableFramebufferReads = true; - renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (gpu, renderCtxOpts); - oreContext = rive::ore::ContextMetal::Make (gpu, queue); + renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (device, renderContexOptions); + oreContext = rive::ore::ContextMetal::Make (device, queue); } //============================================================================== + ~GpuDeviceMetal() override { releasePooledResources(); } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::Metal; } - rive::ore::Context* gpuContext() const noexcept override { return oreContext.get(); } + rive::gpu::RenderContext* getRenderContext() const override { return renderContext.get(); } + + rive::ore::Context* getGpuContext() const noexcept override { return oreContext.get(); } bool isComputeAvailable() const noexcept override { return true; } //============================================================================== + ReferenceCountedObjectPtr createBuffer (GpuBufferType type, const void* data, size_t byteSize) override + { + if (type == GpuBufferType::storage) + { + jassert (data != nullptr && byteSize > 0); + if (data == nullptr || byteSize == 0) + return nullptr; + + MTLResourceOptions options = MTLResourceStorageModeShared; + id mtlBuffer = [device newBufferWithBytes:data + length:byteSize + options:options]; + if (mtlBuffer == nil) + return nullptr; + + return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, mtlBuffer }); + } + + return GpuDevice::createBuffer (type, data, byteSize); + } + + //============================================================================== + + bool readBuffer (GpuBuffer::Ptr buffer, void* dst, size_t dstSize) override + { + if (buffer == nullptr || dst == nullptr) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr || impl->mtlStorageBuffer == nil) + return false; + + const auto byteSize = buffer->getSizeInBytes(); + if (dstSize < byteSize) + return false; + + YUP_AUTORELEASEPOOL + { + // Fence the GPU queue with a tiny fill so we know the compute + // dispatch has finished, then read directly from the shared buffer. + // Avoids a full-size staging allocation + blit (~2 MB for the + // particle demo) by exploiting Apple Silicon's unified memory. + id fenceBuf = [device newBufferWithLength:4 + options:MTLResourceStorageModeShared]; + if (fenceBuf == nil) + return false; + + id cmd = [queue commandBuffer]; + id blit = [cmd blitCommandEncoder]; + [blit fillBuffer:fenceBuf range:NSMakeRange (0, 4) value:0]; + [blit endEncoding]; + [cmd commit]; + [cmd waitUntilCompleted]; + + std::memcpy (dst, [impl->mtlStorageBuffer contents], byteSize); + } + + return true; + } + + //============================================================================== + + bool updateBuffer (GpuBuffer::Ptr buffer, const void* data, size_t byteSize) override + { + if (buffer == nullptr || data == nullptr || byteSize == 0) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr) + return false; + + // Storage buffers: write directly into the shared MTLBuffer. + if (impl->mtlStorageBuffer != nil) + { + if (byteSize > buffer->getSizeInBytes()) + return false; + + YUP_AUTORELEASEPOOL + { + // Serialise after all prior GPU work with a blit, then write. + id stagingBuf = [device newBufferWithLength:4 + options:MTLResourceStorageModeShared]; + + id cmd = [queue commandBuffer]; + id blit = [cmd blitCommandEncoder]; + [blit fillBuffer:stagingBuf range:NSMakeRange (0, 4) value:0]; + [blit endEncoding]; + [cmd commit]; + [cmd waitUntilCompleted]; + + std::memcpy ([impl->mtlStorageBuffer contents], data, byteSize); + } + + return true; + } + + // Vertex, index, and uniform buffers go through ore's update(). + return GpuDevice::updateBuffer (buffer, data, byteSize); + } + + //============================================================================== + std::unique_ptr createOffscreenTarget (int width, int height) override { if (width <= 0 || height <= 0 || renderContext == nullptr) @@ -125,6 +231,32 @@ class GpuDeviceMetal : public GpuDevice target.contextSlot->frameActive = false; } + bool clearOffscreen (OffscreenTarget& baseTarget, GpuColor color) override + { + auto& target = static_cast (baseTarget); + + id texture = target.targetTexture(); + if (texture == nil) + return false; + + YUP_AUTORELEASEPOOL + { + MTLRenderPassDescriptor* descriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + descriptor.colorAttachments[0].texture = texture; + descriptor.colorAttachments[0].loadAction = MTLLoadActionClear; + descriptor.colorAttachments[0].clearColor = MTLClearColorMake (color.red, color.green, color.blue, color.alpha); + descriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + + // An encoder with no draws still performs the attachment's load action. + id commandBuffer = [queue commandBuffer]; + id encoder = [commandBuffer renderCommandEncoderWithDescriptor:descriptor]; + [encoder endEncoding]; + [commandBuffer commit]; + } + + return true; + } + bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override { auto& target = static_cast (baseTarget); @@ -148,7 +280,7 @@ class GpuDeviceMetal : public GpuDevice #else stagingDesc.storageMode = MTLStorageModeManaged; #endif - target.stagingTexture = [gpu newTextureWithDescriptor:stagingDesc]; + target.stagingTexture = [device newTextureWithDescriptor:stagingDesc]; if (target.stagingTexture == nil) return false; } @@ -190,7 +322,7 @@ class GpuDeviceMetal : public GpuDevice //============================================================================== /** Returns the native MTLDevice. Used by GraphicsContextMetal to share the device. */ - id getDevice() const noexcept { return gpu; } + id getDevice() const noexcept { return device; } /** Returns the native MTLCommandQueue. Used by GraphicsContextMetal. */ id getCommandQueue() const noexcept { return queue; } @@ -200,10 +332,17 @@ class GpuDeviceMetal : public GpuDevice { std::unique_ptr renderContext; bool frameActive = false; + bool leased = false; }; struct OffscreenTargetMetal : public RenderableTarget { + ~OffscreenTargetMetal() override + { + if (contextSlot != nullptr) + contextSlot->leased = false; + } + int width = 0; int height = 0; id stagingTexture = nil; @@ -241,8 +380,10 @@ class GpuDeviceMetal : public GpuDevice { if (renderCanvas == nullptr) return nil; + if (auto* target = static_cast (renderCanvas->renderTarget())) return target->targetTexture(); + return nil; } }; @@ -251,32 +392,37 @@ class GpuDeviceMetal : public GpuDevice { for (const auto& slot : offscreenContextPool) { - if (! slot->frameActive) + if (! slot->leased) + { + slot->leased = true; return slot.get(); + } } rive::gpu::RenderContextMetalImpl::ContextOptions renderCtxOpts; - if (fiddleOptions.synchronousShaderCompilations) + if (options.synchronousShaderCompilations) renderCtxOpts.shaderCompilationMode = rive::gpu::ShaderCompilationMode::alwaysSynchronous; - if (fiddleOptions.disableRasterOrdering) + if (options.disableRasterOrdering) renderCtxOpts.disableFramebufferReads = true; auto slot = std::make_unique(); - slot->renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (gpu, renderCtxOpts); + slot->renderContext = rive::gpu::RenderContextMetalImpl::MakeContext (device, renderCtxOpts); if (slot->renderContext == nullptr) return nullptr; + slot->leased = true; + auto* result = slot.get(); offscreenContextPool.push_back (std::move (slot)); return result; } - const GpuDevice::Options fiddleOptions; + const GpuDevice::Options options; std::unique_ptr renderContext; std::vector> offscreenContextPool; std::unique_ptr oreContext; - id gpu = MTLCreateSystemDefaultDevice(); - id queue = [gpu newCommandQueue]; + id device = MTLCreateSystemDefaultDevice(); + id queue = [device newCommandQueue]; }; //============================================================================== diff --git a/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp b/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp index 5becb83af..a5a79468e 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp @@ -24,17 +24,49 @@ #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 { +#if RIVE_DESKTOP_GL && DEBUG +static void GLAPIENTRY err_msg_callback (GLenum source, + GLenum type, + GLuint id, + GLenum severity, + GLsizei length, + const GLchar* message, + const void* userParam) +{ + if (type == GL_DEBUG_TYPE_ERROR_KHR) + { + 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.") + == 0) + return; + if (strstr (message, "is being recompiled based on GL state.")) + return; + printf ("GL PERF: %s\n", message); + fflush (stdout); + } +} +#endif + +//============================================================================== class GpuDeviceGL : public GpuDevice { public: + //============================================================================== GpuDeviceGL (Options options) - : m_options (options) + : options (options) { #if RIVE_DESKTOP_GL if (! gladLoadCustomLoader ((GLADloadfunc) options.loaderFunction)) @@ -44,25 +76,45 @@ class GpuDeviceGL : public GpuDevice } #endif - m_renderContext = rive::gpu::RenderContextGLImpl::MakeContext (m_renderContextOptions); - if (! m_renderContext) + renderContext = rive::gpu::RenderContextGLImpl::MakeContext (renderContextOptions); + if (! renderContext) { fprintf (stderr, "Failed to create a renderer.\n"); return; } - m_oreContext = rive::ore::ContextGL::Make(); + oreContext = rive::ore::ContextGL::Make(); + +#if YUP_ENABLE_GL_VERBOSE + 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); +#endif + +#if ! RIVE_ANDROID + int n; + glGetIntegerv (GL_NUM_EXTENSIONS, &n); + for (size_t i = 0; i < n; ++i) + printf (" %s\n", glGetStringi (GL_EXTENSIONS, i)); +#endif +#endif // YUP_ENABLE_GL_VERBOSE #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); + glDebugMessageCallbackKHR (&err_msg_callback, nullptr); } #endif } - ~GpuDeviceGL() override = default; + ~GpuDeviceGL() override { releasePooledResources(); } + + //============================================================================== GpuPlatform getPlatform() const noexcept override { @@ -73,7 +125,11 @@ class GpuDeviceGL : public GpuDevice #endif } - rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } + rive::gpu::RenderContext* getRenderContext() const override { return renderContext.get(); } + + rive::ore::Context* getGpuContext() const noexcept override { return oreContext.get(); } + + //============================================================================== bool isComputeAvailable() const noexcept override { @@ -101,14 +157,105 @@ class GpuDeviceGL : public GpuDevice //============================================================================== + ReferenceCountedObjectPtr createBuffer (GpuBufferType type, const void* data, size_t byteSize) override + { + if (type == GpuBufferType::storage) + { + jassert (data != nullptr && byteSize > 0); + if (data == nullptr || byteSize == 0) + return nullptr; + + GLuint buf = 0; + glGenBuffers (1, &buf); + if (buf == 0) + return nullptr; + + glBindBuffer (GL_SHADER_STORAGE_BUFFER, buf); + glBufferData (GL_SHADER_STORAGE_BUFFER, static_cast (byteSize), data, GL_DYNAMIC_COPY); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + + return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, buf }); + } + + return GpuDevice::createBuffer (type, data, byteSize); + } + + //============================================================================== + + bool readBuffer (GpuBuffer::Ptr buffer, void* dst, size_t dstSize) override + { +#if YUP_WASM + // WebGL 2.0 (GLES 3.0) has no GL_SHADER_STORAGE_BUFFER — fall back to base. + return GpuDevice::readBuffer (std::move (buffer), dst, dstSize); +#else + if (buffer == nullptr || dst == nullptr) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr || impl->glBuffer == 0) + return false; + + const auto byteSize = buffer->getSizeInBytes(); + if (dstSize < byteSize) + return false; + + glFinish(); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, impl->glBuffer); + void* mapped = glMapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, static_cast (byteSize), GL_MAP_READ_BIT); + if (mapped != nullptr) + { + std::memcpy (dst, mapped, byteSize); + glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); + } + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + return mapped != nullptr; +#endif + } + + //============================================================================== + + bool updateBuffer (GpuBuffer::Ptr buffer, const void* data, size_t byteSize) override + { + if (buffer == nullptr || data == nullptr || byteSize == 0) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr) + return false; + + // For native GL storage buffers, use glBufferSubData. + if (impl->glBuffer != 0) + { + if (byteSize > buffer->getSizeInBytes()) + return false; + + glBindBuffer (GL_SHADER_STORAGE_BUFFER, impl->glBuffer); + glBufferSubData (GL_SHADER_STORAGE_BUFFER, 0, static_cast (byteSize), data); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + return true; + } + + // For ore-backed buffers (vertex, index, uniform), delegate to base class. + return GpuDevice::updateBuffer (buffer, data, byteSize); + } + + //============================================================================== + struct OffscreenContextSlot { std::unique_ptr renderContext; bool frameActive = false; + bool leased = false; }; struct OffscreenTargetGL : public RenderableTarget { + ~OffscreenTargetGL() override + { + if (contextSlot != nullptr) + contextSlot->leased = false; + } + int width = 0; int height = 0; rive::rcp renderCanvas; @@ -174,10 +321,10 @@ class GpuDeviceGL : public GpuDevice std::unique_ptr createOffscreenTarget (int width, int height) override { - if (width <= 0 || height <= 0 || m_renderContext == nullptr) + if (width <= 0 || height <= 0 || renderContext == nullptr) return nullptr; - auto renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); + auto renderCanvas = renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); if (renderCanvas == nullptr) return nullptr; @@ -185,7 +332,7 @@ class GpuDeviceGL : public GpuDevice target->width = width; target->height = height; target->renderContext = nullptr; - target->mirrorContext = m_renderContext.get(); + target->mirrorContext = renderContext.get(); target->contextSlot = nullptr; target->renderCanvas = std::move (renderCanvas); return target; @@ -239,6 +386,37 @@ class GpuDeviceGL : public GpuDevice target.contextSlot->frameActive = false; } + bool clearOffscreen (OffscreenTarget& baseTarget, GpuColor color) override + { + auto& target = static_cast (baseTarget); + + auto* renderTarget = static_cast (target.getRenderTarget()); + if (renderTarget == nullptr) + return false; + + // GL state is global and a canvas may be created part-way through a frame, + // so restore the draw framebuffer afterwards rather than leaving ours bound. + // Scissoring is disabled for the same reason: an enclosing scissor rect + // would otherwise clip the clear and leave part of the canvas undefined. + GLint previousFramebuffer = 0; + glGetIntegerv (GL_DRAW_FRAMEBUFFER_BINDING, &previousFramebuffer); + + const GLboolean scissorWasEnabled = glIsEnabled (GL_SCISSOR_TEST); + if (scissorWasEnabled) + glDisable (GL_SCISSOR_TEST); + + renderTarget->bindDestinationFramebuffer (GL_DRAW_FRAMEBUFFER); + glClearColor (color.red, color.green, color.blue, color.alpha); + glClear (GL_COLOR_BUFFER_BIT); + + if (scissorWasEnabled) + glEnable (GL_SCISSOR_TEST); + + glBindFramebuffer (GL_DRAW_FRAMEBUFFER, static_cast (previousFramebuffer)); + + return true; + } + bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override { auto& target = static_cast (baseTarget); @@ -254,16 +432,17 @@ class GpuDeviceGL : public GpuDevice glReadPixels (0, 0, target.width, target.height, GL_RGBA, GL_UNSIGNED_BYTE, dst); glBindFramebuffer (GL_READ_FRAMEBUFFER, 0); + // Flip vertically: OpenGL framebuffer origin is bottom-left. + offscreenPixelsRow.resize (bytesPerRow); 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 (offscreenPixelsRow.data(), top, bytesPerRow); std::memcpy (top, bottom, bytesPerRow); - std::memcpy (bottom, rowBuffer.data(), bytesPerRow); + std::memcpy (bottom, offscreenPixelsRow.data(), bytesPerRow); } return true; @@ -272,27 +451,33 @@ class GpuDeviceGL : public GpuDevice private: OffscreenContextSlot* acquireOffscreenContext() { - for (const auto& slot : m_offscreenContextPool) + for (const auto& slot : offscreenContextPool) { - if (! slot->frameActive) + if (! slot->leased) + { + slot->leased = true; return slot.get(); + } } auto slot = std::make_unique(); - slot->renderContext = rive::gpu::RenderContextGLImpl::MakeContext (m_renderContextOptions); + slot->renderContext = rive::gpu::RenderContextGLImpl::MakeContext (renderContextOptions); if (slot->renderContext == nullptr) return nullptr; + slot->leased = true; + auto* result = slot.get(); - m_offscreenContextPool.push_back (std::move (slot)); + 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; + Options options; + rive::gpu::RenderContextGLImpl::ContextOptions renderContextOptions; + std::unique_ptr renderContext; + std::vector> offscreenContextPool; + std::unique_ptr oreContext; + std::vector offscreenPixelsRow; }; //============================================================================== diff --git a/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp b/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp index be9b28f13..2805ef616 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp @@ -38,45 +38,175 @@ class GpuDeviceWebGPU : public GpuDevice { public: GpuDeviceWebGPU (Options options) - : m_options (options) + : options (options) { - m_device = wgpu::Device::Acquire (emscripten_webgpu_get_device()); - if (m_device == nullptr) + device = wgpu::Device::Acquire (emscripten_webgpu_get_device()); + if (device == nullptr) { fprintf (stderr, "WebGPU: no device. Ensure Module.preinitializedWebGPUDevice is set before main().\n"); return; } - m_queue = m_device.GetQueue(); + queue = device.GetQueue(); - m_renderContext = rive::gpu::RenderContextWebGPUImpl::MakeContext ( - {}, m_device, m_queue, rive::gpu::RenderContextWebGPUImpl::ContextOptions()); + renderContext = rive::gpu::RenderContextWebGPUImpl::MakeContext ( + {}, device, queue, rive::gpu::RenderContextWebGPUImpl::ContextOptions()); - if (m_renderContext == nullptr) + if (renderContext == nullptr) { fprintf (stderr, "WebGPU: failed to create a render context.\n"); return; } - m_oreContext = m_renderContext->static_impl_cast()->makeOreContext(); + oreContext = renderContext->static_impl_cast()->makeOreContext(); } + ~GpuDeviceWebGPU() override { releasePooledResources(); } + GpuPlatform getPlatform() const noexcept override { return GpuPlatform::WebGPU; } - rive::ore::Context* gpuContext() const noexcept override { return m_oreContext.get(); } + rive::gpu::RenderContext* getRenderContext() const override { return renderContext.get(); } + + rive::ore::Context* getGpuContext() const noexcept override { return oreContext.get(); } bool isComputeAvailable() const noexcept override { return true; } + /** Returns the native wgpu::Device for compute operations. */ + wgpu::Device getWgpuDevice() const noexcept { return device; } + + /** Returns the native wgpu::Queue for compute operations. */ + wgpu::Queue getWgpuQueue() const noexcept { return queue; } + + //============================================================================== + + ReferenceCountedObjectPtr createBuffer (GpuBufferType type, const void* data, size_t byteSize) override + { + if (type == GpuBufferType::storage) + { + jassert (data != nullptr && byteSize > 0); + if (data == nullptr || byteSize == 0) + return nullptr; + + wgpu::BufferDescriptor bufDesc {}; + bufDesc.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc; + bufDesc.size = byteSize; + bufDesc.label = "GpuBuffer storage"; + + wgpu::Buffer wgpuBuffer = device.CreateBuffer (&bufDesc); + if (wgpuBuffer == nullptr) + return nullptr; + + queue.WriteBuffer (wgpuBuffer, 0, data, byteSize); + + return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, std::move (wgpuBuffer) }); + } + + return GpuDevice::createBuffer (type, data, byteSize); + } + + //============================================================================== + + bool updateBuffer (GpuBuffer::Ptr buffer, const void* data, size_t byteSize) override + { + if (buffer == nullptr || data == nullptr || byteSize == 0) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr) + return false; + + // For ore-backed buffers (vertex, index, uniform), delegate to base class. + if (impl->webgpuStorageBuffer == nullptr) + return GpuDevice::updateBuffer (buffer, data, byteSize); + + if (byteSize > buffer->getSizeInBytes()) + return false; + + queue.WriteBuffer (impl->webgpuStorageBuffer, 0, data, byteSize); + return true; + } + + //============================================================================== + + /** Pipelined readback: WebGPU buffer mapping resolves through the JavaScript + event loop, so blocking here would need ASYNCIFY (which costs code size and + runtime speed across the whole module). Each call instead consumes the + oldest snapshot that has finished mapping and schedules a fresh one, so the + result trails the GPU by a frame or two but never stalls. */ + bool readBuffer (GpuBuffer::Ptr buffer, void* dst, size_t dstSize) override + { + if (buffer == nullptr || dst == nullptr || dstSize == 0) + return false; + + auto* impl = buffer->getImpl(); + if (impl == nullptr || impl->webgpuStorageBuffer == nullptr) + return false; + + const auto byteSize = buffer->getSizeInBytes(); + if (dstSize < byteSize) + return false; + + using ReadbackSlot = GpuBuffer::Impl::ReadbackSlot; + + if (impl->readbackUnavailable) + return false; + + if (impl->readbackSlots.empty() && ! createReadbackSlots (*impl, byteSize)) + { + // Retrying the same allocation on every frame would only spin. + impl->readbackUnavailable = true; + return false; + } + + // Consume the oldest snapshot whose mapping has completed. Copies are + // submitted to one queue and their promises resolve in order, so going + // oldest-first hands the caller successive states rather than jumping + // back and forth in time. + ReadbackSlot* oldestMapped = nullptr; + for (auto& slot : impl->readbackSlots) + { + if (slot->mapped && (oldestMapped == nullptr || slot->serial < oldestMapped->serial)) + oldestMapped = slot.get(); + } + + bool wroteSnapshot = false; + + if (oldestMapped != nullptr) + { + if (const void* mapped = oldestMapped->staging.GetConstMappedRange (0, byteSize)) + { + memcpy (dst, mapped, byteSize); + wroteSnapshot = true; + } + + // Unmapping before the slot is reused is mandatory: a mapped buffer + // cannot be a CopyBufferToBuffer destination. + oldestMapped->staging.Unmap(); + oldestMapped->mapped = false; + } + + scheduleReadback (*impl, byteSize); + + return wroteSnapshot; + } + //============================================================================== struct OffscreenContextSlot { std::unique_ptr renderContext; bool frameActive = false; + bool leased = false; }; struct OffscreenTargetWebGPU : public RenderableTarget { + ~OffscreenTargetWebGPU() override + { + if (contextSlot != nullptr) + contextSlot->leased = false; + } + int width = 0; int height = 0; rive::rcp renderCanvas; @@ -112,7 +242,7 @@ class GpuDeviceWebGPU : public GpuDevice std::unique_ptr createOffscreenTarget (int width, int height) override { - if (width <= 0 || height <= 0 || m_renderContext == nullptr) + if (width <= 0 || height <= 0 || renderContext == nullptr) return nullptr; auto target = std::make_unique(); @@ -120,8 +250,8 @@ class GpuDeviceWebGPU : public GpuDevice target->height = height; target->renderContext = nullptr; target->contextSlot = nullptr; - target->renderCanvas = m_renderContext->makeRenderCanvas (static_cast (width), - static_cast (height)); + target->renderCanvas = renderContext->makeRenderCanvas (static_cast (width), + static_cast (height)); if (target->renderCanvas == nullptr) return nullptr; @@ -170,48 +300,167 @@ class GpuDeviceWebGPU : public GpuDevice if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) return; - wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder(); + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); renderContext->flush ({ .renderTarget = target.getRenderTarget(), .externalCommandBuffer = encoder.Get() }); wgpu::CommandBuffer commands = encoder.Finish(); - m_queue.Submit (1, &commands); + queue.Submit (1, &commands); target.contextSlot->frameActive = false; } + bool clearOffscreen (OffscreenTarget& baseTarget, GpuColor color) override + { + auto& target = static_cast (baseTarget); + + auto* renderTarget = static_cast (target.getRenderTarget()); + if (renderTarget == nullptr) + return false; + + wgpu::RenderPassColorAttachment attachment {}; + attachment.view = renderTarget->targetTextureView(); + attachment.loadOp = wgpu::LoadOp::Clear; + attachment.storeOp = wgpu::StoreOp::Store; + attachment.clearValue = { color.red, color.green, color.blue, color.alpha }; + + wgpu::RenderPassDescriptor descriptor {}; + descriptor.colorAttachmentCount = 1; + descriptor.colorAttachments = std::addressof (attachment); + + // A pass with no draws still performs the attachment's load operation. + auto encoder = device.CreateCommandEncoder(); + auto pass = encoder.BeginRenderPass (std::addressof (descriptor)); + pass.End(); + + auto commands = encoder.Finish(); + queue.Submit (1, std::addressof (commands)); + + return true; + } + bool readOffscreenPixels (OffscreenTarget&, void*, size_t) override { return false; // GPU-to-CPU buffer mapping is async-only on the web. } private: + /** Allocates the ring of staging buffers used by readBuffer(). */ + bool createReadbackSlots (GpuBuffer::Impl& impl, size_t byteSize) + { + wgpu::BufferDescriptor stagingDesc {}; + stagingDesc.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst; + stagingDesc.size = byteSize; + stagingDesc.label = "GpuBuffer readback staging"; + + for (size_t i = 0; i < GpuBuffer::Impl::numReadbackSlots; ++i) + { + auto slot = std::make_shared(); + slot->staging = device.CreateBuffer (&stagingDesc); + + if (slot->staging == nullptr) + { + impl.readbackSlots.clear(); + return false; + } + + impl.readbackSlots.push_back (std::move (slot)); + } + + return true; + } + + /** Copies the storage buffer into the first free staging slot and starts mapping it. */ + void scheduleReadback (GpuBuffer::Impl& impl, size_t byteSize) + { + std::shared_ptr freeSlot; + + for (const auto& slot : impl.readbackSlots) + { + if (! slot->mapPending && ! slot->mapped) + { + freeSlot = slot; + break; + } + } + + if (freeSlot == nullptr) + return; + + wgpu::CommandEncoderDescriptor encDesc {}; + encDesc.label = "GpuBuffer readback copy"; + wgpu::CommandEncoder encoder = device.CreateCommandEncoder (&encDesc); + if (encoder == nullptr) + return; + + encoder.CopyBufferToBuffer (impl.webgpuStorageBuffer, 0, freeSlot->staging, 0, byteSize); + + wgpu::CommandBuffer commands = encoder.Finish(); + if (commands == nullptr) + return; + + queue.Submit (1, &commands); + + freeSlot->serial = impl.nextReadbackSerial++; + freeSlot->mapPending = true; + + // AllowSpontaneous is what makes this work without a pump: the callback + // is invoked straight from the JavaScript promise resolution, between + // main-loop ticks. AllowProcessEvents would instead sit in a queue until + // someone called wgpuInstanceProcessEvents(), and never complete here. + // + // The callback owns a strong reference to its slot, so the slot and its + // staging buffer stay alive even if the GpuBuffer is released while the + // mapping is still in flight. + WGPUBufferMapCallbackInfo callbackInfo = WGPU_BUFFER_MAP_CALLBACK_INFO_INIT; + callbackInfo.mode = WGPUCallbackMode_AllowSpontaneous; + callbackInfo.callback = [] (WGPUMapAsyncStatus status, WGPUStringView, void* userdata1, void*) + { + std::unique_ptr> owned { + static_cast*> (userdata1) + }; + + (*owned)->mapPending = false; + + // A failed or aborted mapping leaves the slot free to retry. + (*owned)->mapped = status == WGPUMapAsyncStatus_Success; + }; + callbackInfo.userdata1 = new std::shared_ptr (freeSlot); + + wgpuBufferMapAsync (freeSlot->staging.Get(), WGPUMapMode_Read, 0, byteSize, callbackInfo); + } + OffscreenContextSlot* acquireOffscreenContext() { - for (const auto& slot : m_offscreenContextPool) + for (const auto& slot : offscreenContextPool) { - if (! slot->frameActive) + if (! slot->leased) + { + slot->leased = true; return slot.get(); + } } auto slot = std::make_unique(); slot->renderContext = rive::gpu::RenderContextWebGPUImpl::MakeContext ( - {}, m_device, m_queue, rive::gpu::RenderContextWebGPUImpl::ContextOptions()); + {}, device, queue, rive::gpu::RenderContextWebGPUImpl::ContextOptions()); if (slot->renderContext == nullptr) return nullptr; + slot->leased = true; + auto* result = slot.get(); - m_offscreenContextPool.push_back (std::move (slot)); + 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; + Options options; + wgpu::Device device; + wgpu::Queue queue; + std::unique_ptr renderContext; + std::vector> offscreenContextPool; + std::unique_ptr oreContext; }; //============================================================================== diff --git a/modules/yup_rhi/rhi/yup_GpuBuffer.cpp b/modules/yup_rhi/rhi/yup_GpuBuffer.cpp index b9c0b13b5..14346521f 100644 --- a/modules/yup_rhi/rhi/yup_GpuBuffer.cpp +++ b/modules/yup_rhi/rhi/yup_GpuBuffer.cpp @@ -28,7 +28,69 @@ struct GpuBuffer::Impl { GpuBufferType type = GpuBufferType::vertex; size_t byteSize = 0; - rive::rcp buffer; + + // Ore buffer (for vertex, index, uniform). + rive::rcp oreBuffer; + + // Native storage buffer handles (for compute). +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) + id mtlStorageBuffer = nil; + + ~Impl() = default; +#elif YUP_RIVE_USE_D3D && YUP_WINDOWS + ComPtr d3dStorageBuffer; + ComPtr d3dUav; + + /** Staging copy used by readBuffer(), kept alive so a per-frame reader does not + reallocate it every frame. Created on first readback. */ + ComPtr d3dReadbackStaging; + + ~Impl() = default; +#elif (YUP_EMSCRIPTEN && RIVE_WEBGPU) || YUP_RIVE_USE_DAWN + wgpu::Buffer webgpuStorageBuffer; + + /** One staging buffer in the pipelined readback ring. + + Held by shared_ptr so an in-flight map callback keeps its slot (and the + staging buffer it unmaps) alive even if the GpuBuffer is released first. + */ + struct ReadbackSlot + { + wgpu::Buffer staging; + uint64_t serial = 0; ///< Submission order, so snapshots are consumed oldest-first. + bool mapPending = false; + bool mapped = false; + + ~ReadbackSlot() + { + if (mapped && staging != nullptr) + staging.Unmap(); + } + }; + + /** Three slots keep one copy in flight, one map pending and one ready to + consume, so a snapshot lands every frame once the ring is primed. */ + static constexpr size_t numReadbackSlots = 3; + + std::vector> readbackSlots; + uint64_t nextReadbackSerial = 0; + + /** Set when the staging buffers could not be allocated, so a per-frame reader + gives up instead of retrying the same failing allocation every frame. */ + bool readbackUnavailable = false; + + ~Impl() = default; +#elif YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) + GLuint glBuffer = 0; + + ~Impl() + { + if (glBuffer != 0) + glDeleteBuffers (1, &glBuffer); + } +#else + ~Impl() = default; +#endif }; //============================================================================== @@ -64,7 +126,25 @@ size_t GpuBuffer::getSizeInBytes() const noexcept bool GpuBuffer::isValid() const noexcept { auto* i = getImpl(); - return i != nullptr && i->buffer != nullptr; + if (i == nullptr) + return false; + + if (i->type == GpuBufferType::storage) + { +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) + return i->mtlStorageBuffer != nil; +#elif YUP_RIVE_USE_D3D && YUP_WINDOWS + return i->d3dStorageBuffer != nullptr; +#elif (YUP_EMSCRIPTEN && RIVE_WEBGPU) || YUP_RIVE_USE_DAWN + return i->webgpuStorageBuffer != nullptr; +#elif YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) + return i->glBuffer != 0; +#else + return false; +#endif + } + + return i->oreBuffer != nullptr; } //============================================================================== @@ -74,41 +154,16 @@ GpuBuffer::Ptr GpuBuffer::create (GpuDevice::Ptr ctx, const void* data, size_t byteSize) { - auto* oreCtx = ctx->gpuContext(); - if (oreCtx == nullptr) - return nullptr; - - jassert (data != nullptr && byteSize > 0); - if (data == nullptr || byteSize == 0) + if (ctx == nullptr) return nullptr; - rive::ore::BufferDesc desc; - switch (type) - { - case GpuBufferType::vertex: - desc.usage = rive::ore::BufferUsage::vertex; - break; - case GpuBufferType::index: - desc.usage = rive::ore::BufferUsage::index; - break; - case GpuBufferType::storage: - case GpuBufferType::uniform: - default: - desc.usage = rive::ore::BufferUsage::uniform; - break; - } - - desc.size = (uint32_t) byteSize; - desc.data = data; - desc.immutable = true; - desc.label = "GpuBuffer"; - - auto buffer = oreCtx->makeBuffer (desc); - if (buffer == nullptr) - return nullptr; + return ctx->createBuffer (type, data, byteSize); +} - GpuBuffer::Ptr result = new GpuBuffer(); - result->impl = TypeErasedObject (GpuBuffer::Impl { type, byteSize, std::move (buffer) }); +GpuBuffer::Ptr GpuBuffer::createWithImpl (Impl&& impl) +{ + auto* result = new GpuBuffer(); + result->impl = TypeErasedObject (std::move (impl)); return result; } diff --git a/modules/yup_rhi/rhi/yup_GpuBuffer.h b/modules/yup_rhi/rhi/yup_GpuBuffer.h index ea757943d..5151526dd 100644 --- a/modules/yup_rhi/rhi/yup_GpuBuffer.h +++ b/modules/yup_rhi/rhi/yup_GpuBuffer.h @@ -73,16 +73,24 @@ class YUP_API GpuBuffer : public ReferenceCountedObject /** Returns true if this buffer holds a valid GPU resource. */ bool isValid() const noexcept; + //============================================================================== + /** @internal */ + struct Impl; + + /** @internal */ + Impl* getImpl() noexcept; + const Impl* getImpl() const noexcept; + + /** @internal Creates a GpuBuffer from a pre-built Impl. Used by GpuDevice backends. */ + static Ptr createWithImpl (Impl&& impl); + private: + friend class GpuDevice; friend class GpuRenderPass; GpuBuffer() = default; - struct Impl; - Impl* getImpl() noexcept; - const Impl* getImpl() const noexcept; - - static constexpr size_t ImplSizeBytes = 32; + static constexpr size_t ImplSizeBytes = 128; TypeErasedObject impl; YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuBuffer) diff --git a/modules/yup_rhi/rhi/yup_GpuComputePass.cpp b/modules/yup_rhi/rhi/yup_GpuComputePass.cpp new file mode 100644 index 000000000..2b26d1868 --- /dev/null +++ b/modules/yup_rhi/rhi/yup_GpuComputePass.cpp @@ -0,0 +1,240 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== +// Backend factory functions — defined in native/yup_GpuComputePass_*.cpp +//============================================================================== + +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) +std::unique_ptr yup_createComputePassImplMetal (GpuDevice&); +#endif +#if YUP_RIVE_USE_D3D && YUP_WINDOWS +std::unique_ptr yup_createComputePassImplD3D11 (GpuDevice&); +#endif +#if (YUP_EMSCRIPTEN && RIVE_WEBGPU) || YUP_RIVE_USE_DAWN +std::unique_ptr yup_createComputePassImplWebGPU (GpuDevice&); +#endif +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID +std::unique_ptr yup_createComputePassImplGL (GpuDevice&); +#endif + +//============================================================================== +// GpuComputePass::Impl — base with common bindings, virtual dispatch / finish +//============================================================================== + +struct GpuComputePass::Impl +{ + GpuComputePipeline::Ptr pipelineRef; + bool finished = false; + + struct StorageBinding + { + int group; + int binding; + GpuBuffer::Ptr buffer; + }; + + struct UboBinding + { + int group; + int binding; + std::vector data; + }; + + struct TexBinding + { + int group; + int binding; + GpuTexture::Ptr texture; + }; + + std::vector storageBindings; + std::vector uboBindings; + std::vector texBindings; + + virtual ~Impl() = default; + virtual bool isValid() const = 0; + virtual bool dispatch (uint32_t groupsX, uint32_t groupsY, uint32_t groupsZ) = 0; + virtual void finish() = 0; +}; + +//============================================================================== + +GpuComputePass GpuComputePass::begin (GpuDevice::Ptr ctx) +{ + GpuComputePass pass; + if (ctx == nullptr || ! ctx->isComputeAvailable()) + return pass; + + switch (ctx->getPlatform()) + { +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) + case GpuPlatform::Metal: + pass.impl = yup_createComputePassImplMetal (*ctx); + break; +#endif +#if YUP_RIVE_USE_D3D && YUP_WINDOWS + case GpuPlatform::Direct3D: + pass.impl = yup_createComputePassImplD3D11 (*ctx); + break; +#endif +#if YUP_EMSCRIPTEN && RIVE_WEBGPU + case GpuPlatform::WebGPU: + pass.impl = yup_createComputePassImplWebGPU (*ctx); + break; +#elif YUP_RIVE_USE_DAWN + case GpuPlatform::WebGPU: + pass.impl = yup_createComputePassImplWebGPU (*ctx); + break; +#endif +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID + case GpuPlatform::OpenGL: + case GpuPlatform::OpenGLES: + pass.impl = yup_createComputePassImplGL (*ctx); + break; +#endif + default: + break; + } + + return pass; +} + +//============================================================================== + +GpuComputePass::GpuComputePass (GpuComputePass&&) noexcept = default; + +GpuComputePass& GpuComputePass::operator= (GpuComputePass&& other) noexcept +{ + if (this != &other) + { + finish(); + impl = std::move (other.impl); + } + return *this; +} + +GpuComputePass::~GpuComputePass() +{ + finish(); +} + +//============================================================================== + +bool GpuComputePass::isValid() const noexcept +{ + return impl != nullptr && ! impl->finished && impl->isValid(); +} + +void GpuComputePass::setPipeline (GpuComputePipeline::Ptr pipeline) +{ + if (impl) + impl->pipelineRef = std::move (pipeline); +} + +void GpuComputePass::setStorageBuffer (int group, int binding, GpuBuffer::Ptr buffer) +{ + if (! impl) + return; + + for (auto& sb : impl->storageBindings) + { + if (sb.group == group && sb.binding == binding) + { + sb.buffer = std::move (buffer); + return; + } + } + + impl->storageBindings.push_back ({ group, binding, std::move (buffer) }); +} + +void GpuComputePass::setUniformBuffer (int group, int binding, const void* data, size_t byteSize) +{ + if (! impl) + return; + + jassert (data != nullptr && byteSize > 0); + if (data == nullptr || byteSize == 0) + return; + + for (auto& ub : impl->uboBindings) + { + if (ub.group == group && ub.binding == binding) + { + ub.data.assign (static_cast (data), + static_cast (data) + byteSize); + return; + } + } + + Impl::UboBinding ub; + ub.group = group; + ub.binding = binding; + ub.data.assign (static_cast (data), + static_cast (data) + byteSize); + impl->uboBindings.push_back (std::move (ub)); +} + +void GpuComputePass::setTexture (int group, int binding, GpuTexture::Ptr texture) +{ + if (! impl) + return; + + for (auto& tb : impl->texBindings) + { + if (tb.group == group && tb.binding == binding) + { + tb.texture = std::move (texture); + return; + } + } + + impl->texBindings.push_back ({ group, binding, std::move (texture) }); +} + +//============================================================================== + +bool GpuComputePass::dispatch (uint32_t groupsX, uint32_t groupsY, uint32_t groupsZ) +{ + if (! isValid()) + return false; + + return impl->dispatch (groupsX, groupsY, groupsZ); +} + +//============================================================================== + +bool GpuComputePass::finish() +{ + if (impl == nullptr || impl->finished) + return false; + + impl->finished = true; + impl->finish(); + impl.reset(); + return true; +} + +} // namespace yup diff --git a/modules/yup_rhi/rhi/yup_GpuComputePass.h b/modules/yup_rhi/rhi/yup_GpuComputePass.h new file mode 100644 index 000000000..96c9da021 --- /dev/null +++ b/modules/yup_rhi/rhi/yup_GpuComputePass.h @@ -0,0 +1,151 @@ +/* + ============================================================================== + + 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 GpuComputePipeline; +class GpuBuffer; +class GpuTexture; +class GpuDevice; + +//============================================================================== +/** A transient encoder that dispatches compute work to the GPU. + + Obtain a GpuComputePass from GpuComputePass::begin(). Bind a compute + pipeline and resources (storage buffers, uniform buffers, textures), then + call dispatch() to run the compute shader and finish() to submit the work. + + The pass is move-only and follows RAII: the destructor calls finish() if + you haven't already, so the GPU work is never silently dropped. + + @code + auto pass = GpuComputePass::begin (device); + pass.setPipeline (pipeline); + pass.setStorageBuffer (0, 0, inputBuf); + pass.setStorageBuffer (0, 1, outputBuf); + pass.setUniformBuffer (0, 2, ¶ms, sizeof (params)); + pass.dispatch (workgroupCount, 1, 1); + pass.finish(); + @endcode + + @see GpuComputePipeline, GpuBuffer, GpuTexture, GpuDevice +*/ +class YUP_API GpuComputePass +{ +public: + //============================================================================== + /** Begins a compute pass on the given device. + + @param ctx A GpuDevice with compute shader support. + @returns A GpuComputePass ready for binding and dispatch, or an + invalid pass if compute is unavailable or the device is null. + */ + static GpuComputePass begin (GpuDevice::Ptr ctx); + + //============================================================================== + /** Move constructor. */ + GpuComputePass (GpuComputePass&&) noexcept; + + /** Move assignment operator. */ + GpuComputePass& operator= (GpuComputePass&&) noexcept; + + /** Destructor. Finishes the pass if not already finished. */ + ~GpuComputePass(); + + //============================================================================== + /** Returns true if the pass is valid and has not been finished. */ + bool isValid() const noexcept; + + //============================================================================== + /** Sets the compute pipeline used by subsequent dispatch() calls. + + @param pipeline The compiled GpuComputePipeline to use. + */ + void setPipeline (GpuComputePipeline::Ptr pipeline); + + /** Binds a read-write storage buffer to the given slot. + + The buffer must have been created with GpuBufferType::storage. The + (group, binding) indices must match the shader's layout declarations. + + @param group Binding group index declared in the shader (set). + @param binding Binding index within the group. + @param buffer The storage buffer to bind. + */ + void setStorageBuffer (int group, int binding, GpuBuffer::Ptr buffer); + + /** Uploads uniform data to the given slot. + + The data is copied immediately and does not need to outlive this call. + The (group, binding) indices must match the shader's layout declarations. + + @param group Binding group index declared in the shader (set). + @param binding Binding index within the group. + @param data Pointer to the uniform data. + @param byteSize Size of the data in bytes. + */ + void setUniformBuffer (int group, int binding, const void* data, size_t byteSize); + + /** Binds a read-only texture to the given slot. + + @param group Binding group index declared in the shader (set). + @param binding Binding index within the group. + @param texture The texture to bind for sampling. + */ + void setTexture (int group, int binding, GpuTexture::Ptr texture); + + //============================================================================== + /** Dispatches compute workgroups. + + The total number of GPU threads launched is + @code groupsX * groupsY * groupsZ * workgroupSize @endcode + where `workgroupSize` is the pipeline's local workgroup size. + + @param groupsX Number of workgroups in X. + @param groupsY Number of workgroups in Y. + @param groupsZ Number of workgroups in Z. + + @returns true on success, false if the pass is invalid. + */ + bool dispatch (uint32_t groupsX, uint32_t groupsY, uint32_t groupsZ); + + //============================================================================== + /** Submits all recorded dispatches to the GPU. + + Idempotent — calling finish() more than once is a no-op returning false. + + @returns true if work was submitted, false if already finished or invalid. + */ + bool finish(); + + //============================================================================== + struct Impl; + +private: + GpuComputePass() = default; + + std::unique_ptr impl; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuComputePass) +}; + +} // namespace yup diff --git a/modules/yup_rhi/rhi/yup_GpuComputePipeline.cpp b/modules/yup_rhi/rhi/yup_GpuComputePipeline.cpp new file mode 100644 index 000000000..f2e8e33bd --- /dev/null +++ b/modules/yup_rhi/rhi/yup_GpuComputePipeline.cpp @@ -0,0 +1,175 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== + +ResultValue GpuComputePipeline::compile (GpuDevice::Ptr ctx, + const GpuShaderSource& source, + const GpuWorkgroupSize& workgroupSize) +{ + if (ctx == nullptr) + return makeResultValueFail ("GpuDevice is null"); + + if (! ctx->isComputeAvailable()) + return makeResultValueFail ("Compute shaders are not available on this backend"); + + switch (ctx->getPlatform()) + { +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) + case GpuPlatform::Metal: + return yup_constructComputePipelineMetal (*ctx, source, workgroupSize); +#endif +#if YUP_RIVE_USE_D3D && YUP_WINDOWS + case GpuPlatform::Direct3D: + return yup_constructComputePipelineD3D11 (*ctx, source, workgroupSize); +#endif +#if YUP_EMSCRIPTEN && RIVE_WEBGPU + case GpuPlatform::WebGPU: + return yup_constructComputePipelineWebGPU (*ctx, source, workgroupSize); +#elif YUP_RIVE_USE_DAWN + case GpuPlatform::WebGPU: + return yup_constructComputePipelineWebGPU (*ctx, source, workgroupSize); +#endif +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID + case GpuPlatform::OpenGL: + case GpuPlatform::OpenGLES: + return yup_constructComputePipelineGL (source, workgroupSize); +#endif + default: + return makeResultValueFail ("Unsupported GPU platform for compute pipelines"); + } +} + +//============================================================================== + +ResultValue GpuComputePipeline::compileFromBundle (GpuDevice::Ptr ctx, + const ShaderBundle& bundle, + const GpuWorkgroupSize& workgroupSize) +{ + if (ctx == nullptr) + return makeResultValueFail ("GpuDevice is null"); + + if (! ctx->isComputeAvailable()) + return makeResultValueFail ("Compute shaders are not available on this backend"); + + GpuShaderLanguage targetLang; + switch (ctx->getPlatform()) + { + case GpuPlatform::Metal: + targetLang = GpuShaderLanguage::msl; + break; + case GpuPlatform::Direct3D: + targetLang = GpuShaderLanguage::hlsl; + break; + case GpuPlatform::WebGPU: + targetLang = GpuShaderLanguage::wgsl; + break; + case GpuPlatform::OpenGL: + case GpuPlatform::OpenGLES: + targetLang = GpuShaderLanguage::glsl; + break; + default: + return makeResultValueFail ("Unsupported GPU platform"); + } + + auto* shader = bundle.findShader (ShaderStage::compute, shaderLanguageForApi (ctx->getPlatform())); + if (shader == nullptr) + return makeResultValueFail ("Bundle does not contain a compute shader for this platform"); + + GpuShaderSource source; + source.language = targetLang; + source.code = static_cast (shader->source.toRawUTF8()); + source.codeSize = static_cast (shader->source.getNumBytesAsUTF8()); + source.entryPoint = shader->entryPoint.toRawUTF8(); + + GpuWorkgroupSize wgs = workgroupSize; + if (wgs.x == 1 && wgs.y == 1 && wgs.z == 1) + { + const auto& reflWgs = shader->reflection.workgroupSize; + if (reflWgs.x > 0 && reflWgs.y > 0 && reflWgs.z > 0) + wgs = GpuWorkgroupSize { reflWgs.x, reflWgs.y, reflWgs.z }; + } + + return compile (ctx, source, wgs); +} + +#if YUP_ENABLE_SHADER_TRANSPILER + +ResultValue GpuComputePipeline::compileFromGlsl (GpuDevice::Ptr ctx, + const String& glsl, + const GpuWorkgroupSize& workgroupSize) +{ + if (ctx == nullptr) + return makeResultValueFail ("GpuDevice is null"); + + if (! ctx->isComputeAvailable()) + return makeResultValueFail ("Compute shaders are not available on this backend"); + + GpuShaderLanguage targetLang; + switch (ctx->getPlatform()) + { + case GpuPlatform::Metal: + targetLang = GpuShaderLanguage::msl; + break; + case GpuPlatform::Direct3D: + targetLang = GpuShaderLanguage::hlsl; + break; + case GpuPlatform::WebGPU: + targetLang = GpuShaderLanguage::wgsl; + break; + case GpuPlatform::OpenGL: + case GpuPlatform::OpenGLES: + targetLang = GpuShaderLanguage::glsl; + break; + default: + return makeResultValueFail ("Unsupported GPU platform"); + } + + ShaderTranspiler transpiler; + auto transpileResult = transpiler.transpile (glsl, ShaderStage::compute, ShaderLanguage::glsl, shaderLanguageForApi (ctx->getPlatform())); + if (transpileResult.failed()) + return makeResultValueFail ("GLSL transpilation failed: " + transpileResult.getErrorMessage()); + + auto reflectionResult = transpiler.reflect (glsl, ShaderStage::compute, ShaderLanguage::glsl); + GpuWorkgroupSize wgs = workgroupSize; + if (wgs.x == 1 && wgs.y == 1 && wgs.z == 1 && reflectionResult.wasOk()) + { + const auto& reflWgs = reflectionResult.getReference().workgroupSize; + if (reflWgs.x > 0 && reflWgs.y > 0 && reflWgs.z > 0) + wgs = GpuWorkgroupSize { reflWgs.x, reflWgs.y, reflWgs.z }; + } + + const auto& nativeSource = transpileResult.getReference(); + + GpuShaderSource source; + source.language = targetLang; + source.code = static_cast (nativeSource.toRawUTF8()); + source.codeSize = static_cast (nativeSource.getNumBytesAsUTF8()); + + return compile (ctx, source, wgs); +} + +#endif // YUP_ENABLE_SHADER_TRANSPILER + +} // namespace yup diff --git a/modules/yup_rhi/rhi/yup_GpuComputePipeline.h b/modules/yup_rhi/rhi/yup_GpuComputePipeline.h new file mode 100644 index 000000000..4b44dbe2a --- /dev/null +++ b/modules/yup_rhi/rhi/yup_GpuComputePipeline.h @@ -0,0 +1,114 @@ +/* + ============================================================================== + + 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 compute pipeline. + + GpuComputePipeline holds a single compute shader stage compiled for the + current GPU backend. It is immutable once compiled — bind resources and + dispatch workgroups via GpuComputePass. + + Compile a pipeline once and reuse it across frames and compute passes. + + @warning Requires GpuDevice::isComputeAvailable() (Metal, D3D11, WebGPU, + or OpenGL ≥4.3 / GLES ≥3.1). + + @see GpuComputePass, GpuDevice, GpuWorkgroupSize +*/ +class YUP_API GpuComputePipeline : public ReferenceCountedObject +{ +public: + using Ptr = ReferenceCountedObjectPtr; + + //============================================================================== + /** Compiles a compute pipeline from a native shader source. + + Provide the source code in the shading language matching your target + platform (MSL for Metal, HLSL for Direct3D, WGSL for WebGPU, GLSL for + OpenGL). On failure the returned ResultValue holds a human-readable + description. + + @param ctx A GpuDevice with compute shader support. + @param source Compute shader source and language. + @param workgroupSize The local workgroup size declared in the shader. + + @returns A compiled compute pipeline, or a failure description. + + @warning Requires ctx->isComputeAvailable(). + */ + static ResultValue compile (GpuDevice::Ptr ctx, + const GpuShaderSource& source, + const GpuWorkgroupSize& workgroupSize); + + /** Compiles a compute pipeline from a pre-built shader bundle (.ysl). + + The bundle must contain a compute shader stage. Picks the native variant + matching the device's graphics API and compiles the pipeline. This is the + recommended path for shaders built offline — no transpiler needed at + runtime. + + @param ctx A GpuDevice with compute shader support. + @param bundle Bundle containing a compute shader stage. + @param workgroupSize Overrides the bundle's reflected workgroup size + when non-zero. + + @returns A compiled compute pipeline, or a failure description. + + @see ShaderBundle + */ + static ResultValue compileFromBundle (GpuDevice::Ptr ctx, + const ShaderBundle& bundle, + const GpuWorkgroupSize& workgroupSize = {}); + +#if YUP_ENABLE_SHADER_TRANSPILER + /** Compiles a compute pipeline from GLSL 450 source (online transpilation). + + Transpiles the GLSL to the native shading language, reflects the + workgroup size, and compiles the pipeline. Only available when the shader + transpiler is enabled at build time. + + @param ctx A GpuDevice with compute shader support. + @param glsl GLSL 450 compute shader source. + @param workgroupSize Overrides the reflected workgroup size when non-zero. + + @returns A compiled compute pipeline, or a failure description. + */ + static ResultValue compileFromGlsl (GpuDevice::Ptr ctx, + const String& glsl, + const GpuWorkgroupSize& workgroupSize = {}); +#endif + + //============================================================================== + /** Returns the local workgroup size this pipeline was compiled with. */ + virtual GpuWorkgroupSize getWorkgroupSize() const noexcept = 0; + +protected: + GpuComputePipeline() = default; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuComputePipeline) +}; + +} // namespace yup diff --git a/modules/yup_rhi/rhi/yup_GpuFrame.cpp b/modules/yup_rhi/rhi/yup_GpuFrame.cpp index 377baae84..21712b930 100644 --- a/modules/yup_rhi/rhi/yup_GpuFrame.cpp +++ b/modules/yup_rhi/rhi/yup_GpuFrame.cpp @@ -26,18 +26,46 @@ namespace yup struct GpuFrame::Impl { + GpuDevice::Ptr device; rive::ore::Context* oreCtx = nullptr; bool submitted = false; - - // Resources that must remain alive from a draw call until waitForGPU() - // completes or the frame is destroyed. + bool waited = false; std::vector> liveBuffers; std::vector> liveViews; std::vector> liveSamplers; + + /** Takes a uniform buffer from the device pool, fills it, and keeps it alive + for the rest of the frame. + + The encoded render pass references the buffer by raw pointer, so ownership + stays with the frame until it completes; the buffer then goes back to the + pool. Each call hands out a distinct buffer, so two draws in one frame never + share one. + + @returns The filled buffer, or nullptr if none could be obtained. + */ + rive::rcp acquireUniformBuffer (const void* data, size_t byteSize); }; //============================================================================== +rive::rcp GpuFrame::Impl::acquireUniformBuffer (const void* data, size_t byteSize) +{ + if (device == nullptr || oreCtx == nullptr || data == nullptr || byteSize == 0) + return nullptr; + + auto buffer = device->uniformBufferPool.acquire (*oreCtx, byteSize); + if (buffer == nullptr) + return nullptr; + + buffer->update (data, static_cast (byteSize), 0); + + liveBuffers.push_back (buffer); + return buffer; +} + +//============================================================================== + GpuFrame::Impl* GpuFrame::getImpl() noexcept { return impl.getPayload(); @@ -54,13 +82,14 @@ GpuFrame GpuFrame::begin (GpuDevice::Ptr ctx) { GpuFrame frame; - auto* oreCtx = ctx->gpuContext(); + auto* oreCtx = ctx->getGpuContext(); if (oreCtx == nullptr) return frame; frame.impl = TypeErasedObject (GpuFrame::Impl {}); auto* i = frame.getImpl(); + i->device = ctx; i->oreCtx = oreCtx; oreCtx->beginFrame ({}); @@ -75,9 +104,8 @@ GpuFrame& GpuFrame::operator= (GpuFrame&& other) noexcept { if (this != &other) { - // Submit any pending frame we currently own before taking over. - if (auto* i = getImpl(); i != nullptr && ! i->submitted && i->oreCtx != nullptr) - i->oreCtx->endFrame(); + submit(); + waitForGPU(); impl = std::move (other.impl); } @@ -88,6 +116,8 @@ GpuFrame& GpuFrame::operator= (GpuFrame&& other) noexcept GpuFrame::~GpuFrame() { submit(); + + waitForGPU(); } //============================================================================== @@ -106,18 +136,23 @@ bool GpuFrame::submit() i->oreCtx->endFrame(); i->submitted = true; + return true; } void GpuFrame::waitForGPU() { auto* i = getImpl(); - if (i == nullptr || i->oreCtx == nullptr) + if (i == nullptr || i->oreCtx == nullptr || i->waited) return; + i->waited = true; i->oreCtx->waitForGPU(); - // GPU has finished; safe to release all transient resources. + if (i->device != nullptr) + for (auto& buffer : i->liveBuffers) + i->device->uniformBufferPool.release (std::move (buffer)); + i->liveBuffers.clear(); i->liveViews.clear(); i->liveSamplers.clear(); diff --git a/modules/yup_rhi/rhi/yup_GpuFrame.h b/modules/yup_rhi/rhi/yup_GpuFrame.h index 6f8d901a2..43a1f9271 100644 --- a/modules/yup_rhi/rhi/yup_GpuFrame.h +++ b/modules/yup_rhi/rhi/yup_GpuFrame.h @@ -82,15 +82,23 @@ class YUP_API GpuFrame /** Submits all render passes recorded since begin(). Idempotent: a second call is a no-op and returns false. Does not block - the CPU - call waitForGPU() afterwards if you need results immediately. + the CPU. @return true on success; false if invalid or already submitted. */ bool submit(); - /** Blocks the calling thread until all submitted GPU work has completed. + /** Blocks the calling thread until all submitted GPU work has completed, + then releases the transient resources held for this frame. - Also releases the transient resources held for this frame. + Idempotent: a second call is a no-op, so waiting explicitly costs no more + than letting the frame go out of scope. + + Call this explicitly only when results are needed earlier than the end of + the frame's scope (e.g. before a CPU readback). The destructor already + waits, because the encoded render passes hold *raw* pointers to the + texture views, uniform buffers and samplers this frame keeps alive, so + releasing them while the GPU is still reading would corrupt the output. */ void waitForGPU(); @@ -108,7 +116,7 @@ class YUP_API GpuFrame static constexpr size_t ImplSizeBytes = 128; TypeErasedObject impl; - YUP_DECLARE_NON_COPYABLE (GpuFrame) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuFrame) }; } // namespace yup diff --git a/modules/yup_rhi/rhi/yup_GpuPipeline.cpp b/modules/yup_rhi/rhi/yup_GpuPipeline.cpp index 6a111b86c..f4a1d0f71 100644 --- a/modules/yup_rhi/rhi/yup_GpuPipeline.cpp +++ b/modules/yup_rhi/rhi/yup_GpuPipeline.cpp @@ -231,12 +231,25 @@ rive::ore::TextureFormat toOreTextureFormat (GpuTextureFormat f) struct GpuPipeline::Impl { + /** A sampler auto-created for one sampler binding declared by the layouts. */ + struct SamplerBinding + { + uint32_t binding; + rive::rcp sampler; + }; + rive::ore::Context* oreCtx = nullptr; rive::rcp vertModule; rive::rcp fragModule; rive::rcp pipeline; std::vector> layouts; // indexed by group; may contain null entries + // One sampler per sampler binding the layouts declare, indexed by group. + // GpuRenderPass fills every declared sampler slot with a linear/clamp-to-edge + // sampler; that descriptor never varies, so the samplers are created here once + // rather than per draw. The pipeline outlives the passes that reference them. + std::vector> samplersPerGroup; + // Vertex-layout storage backing PipelineDesc's raw pointers. The ore // Pipeline copies PipelineDesc by value but keeps the vertexBuffers / // attributes pointers, reading them at draw time - so this storage must @@ -268,7 +281,7 @@ ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, { using namespace GpuPipelineHelpers; - auto oreCtx = ctx->gpuContext(); + auto oreCtx = ctx->getGpuContext(); if (oreCtx == nullptr) return makeResultValueFail ("GpuDevice was not created with Options::enableOreContext = true"); @@ -619,6 +632,36 @@ ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, implRef->fragModule = std::move (fragModule); implRef->pipeline = std::move (pipeline); implRef->layouts = std::move (layouts); + + // Create the auto-samplers up front. Their descriptor is fixed, so one per + // declared binding serves every draw encoded with this pipeline. + implRef->samplersPerGroup.resize (implRef->layouts.size()); + + for (size_t g = 0; g < implRef->layouts.size(); ++g) + { + auto* layout = implRef->layouts[g].get(); + if (layout == nullptr) + continue; + + for (const auto& entry : layout->entries()) + { + if (entry.kind != rive::ore::BindingKind::sampler + && entry.kind != rive::ore::BindingKind::comparisonSampler) + { + continue; + } + + rive::ore::SamplerDesc sd; + sd.minFilter = rive::ore::Filter::linear; + sd.magFilter = rive::ore::Filter::linear; + sd.wrapU = rive::ore::WrapMode::clampToEdge; + sd.wrapV = rive::ore::WrapMode::clampToEdge; + + if (auto sampler = oreCtx->makeSampler (sd)) + implRef->samplersPerGroup[g].push_back ({ entry.binding, std::move (sampler) }); + } + } + return makeResultValueOk (pipe); } diff --git a/modules/yup_rhi/rhi/yup_GpuPipelineCache.h b/modules/yup_rhi/rhi/yup_GpuPipelineCache.h index 61230c276..f22dc1a64 100644 --- a/modules/yup_rhi/rhi/yup_GpuPipelineCache.h +++ b/modules/yup_rhi/rhi/yup_GpuPipelineCache.h @@ -145,12 +145,12 @@ class YUP_API GpuPipelineCache final void evictIfNeeded(); GpuDevice::Ptr context; - std::map cache; + std::unordered_map cache; size_t maxEntries = 256; uint64 accessCounter = 0; mutable CriticalSection lock; - YUP_DECLARE_NON_COPYABLE (GpuPipelineCache) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuPipelineCache) }; } // namespace yup diff --git a/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp b/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp index 8800e7b6b..a25af4f2b 100644 --- a/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp +++ b/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp @@ -60,6 +60,7 @@ struct GpuRenderPass::Impl GpuPipeline::Ptr pipelineRef; rive::ore::Pipeline* orePipeline = nullptr; const std::vector>* oreLayouts = nullptr; + const std::vector>* oreSamplers = nullptr; bool finished = false; @@ -143,12 +144,27 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) std::vector>> bindGroups; + // Fast path: skip bind-group creation when there are no UBOs, textures, + // or samplers to bind (common for simple vertex-only draws). + const bool hasAnyBindings = ! uboBindings.empty() || ! textureBindings.empty(); + for (uint32_t groupIdx = 0; groupIdx < layouts.size(); ++groupIdx) { auto* layout = layouts[groupIdx].get(); if (layout == nullptr) continue; + // The pipeline pre-created one sampler per sampler slot this layout + // declares, so their presence also decides whether a bind group is needed. + const std::vector* groupSamplers = nullptr; + if (oreSamplers != nullptr && groupIdx < oreSamplers->size()) + groupSamplers = &(*oreSamplers)[groupIdx]; + + const bool layoutHasSamplers = groupSamplers != nullptr && ! groupSamplers->empty(); + + if (! hasAnyBindings && ! layoutHasSamplers) + continue; + // UBO entries for this group. std::vector uboEntries; @@ -157,13 +173,9 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) if (ub.group != (int) groupIdx) continue; - rive::ore::BufferDesc bufDesc; - bufDesc.usage = rive::ore::BufferUsage::uniform; - bufDesc.size = (uint32_t) ub.data.size(); - bufDesc.data = ub.data.data(); - bufDesc.immutable = true; - - auto buf = oreCtx->makeBuffer (bufDesc); + // Recycled from the device pool and owned by the frame, so a steady + // stream of draws stops allocating GPU buffers after the first frames. + auto buf = framePools->acquireUniformBuffer (ub.data.data(), ub.data.size()); if (buf == nullptr) continue; @@ -173,7 +185,6 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) entry.offset = 0; entry.size = (uint32_t) ub.data.size(); uboEntries.push_back (entry); - framePools->liveBuffers.push_back (std::move (buf)); } // Texture entries for this group. @@ -195,32 +206,22 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) framePools->liveViews.push_back (std::move (view)); } - // Sampler entries - auto-create one linear+clamp sampler for each - // sampler binding declared in the layout. + // Sampler entries - one linear+clamp sampler per sampler binding declared + // in the layout, created once when the pipeline was compiled. The frame + // holds a reference too, since the encoded pass points at them raw and may + // outlive this pass object. std::vector sampEntries; - for (const auto& layoutEntry : layout->entries()) + if (groupSamplers != nullptr) { - if (layoutEntry.kind != rive::ore::BindingKind::sampler - && layoutEntry.kind != rive::ore::BindingKind::comparisonSampler) + for (const auto& sb : *groupSamplers) { - continue; + rive::ore::BindGroupDesc::SampEntry se; + se.slot = sb.binding; + se.sampler = sb.sampler.get(); + sampEntries.push_back (se); + framePools->liveSamplers.push_back (sb.sampler); } - rive::ore::SamplerDesc sd; - sd.minFilter = rive::ore::Filter::linear; - sd.magFilter = rive::ore::Filter::linear; - sd.wrapU = rive::ore::WrapMode::clampToEdge; - sd.wrapV = rive::ore::WrapMode::clampToEdge; - - auto samp = oreCtx->makeSampler (sd); - if (samp == nullptr) - continue; - - rive::ore::BindGroupDesc::SampEntry se; - se.slot = layoutEntry.binding; - se.sampler = samp.get(); - sampEntries.push_back (se); - framePools->liveSamplers.push_back (std::move (samp)); } rive::ore::BindGroupDesc bgDesc; @@ -329,11 +330,13 @@ void GpuRenderPass::setPipeline (GpuPipeline::Ptr pipeline) { i->orePipeline = pipeImpl->pipeline.get(); i->oreLayouts = &pipeImpl->layouts; + i->oreSamplers = &pipeImpl->samplersPerGroup; } else { i->orePipeline = nullptr; i->oreLayouts = nullptr; + i->oreSamplers = nullptr; } } @@ -387,7 +390,7 @@ void GpuRenderPass::setVertexBuffer (int slot, GpuBuffer::Ptr buffer) if (i == nullptr) return; - auto* ore = (buffer != nullptr && buffer->getImpl() != nullptr) ? buffer->getImpl()->buffer.get() : nullptr; + auto* ore = (buffer != nullptr && buffer->getImpl() != nullptr) ? buffer->getImpl()->oreBuffer.get() : nullptr; for (auto& vb : i->vertexBindings) { @@ -408,7 +411,7 @@ void GpuRenderPass::setIndexBuffer (GpuIndexFormat format, GpuBuffer::Ptr buffer if (i == nullptr) return; - i->indexOreBuffer = (buffer != nullptr && buffer->getImpl() != nullptr) ? buffer->getImpl()->buffer.get() : nullptr; + i->indexOreBuffer = (buffer != nullptr && buffer->getImpl() != nullptr) ? buffer->getImpl()->oreBuffer.get() : nullptr; i->indexBuffer = std::move (buffer); i->indexFormat = GpuPipelineHelpers::toOreIndexFormat (format); } @@ -439,6 +442,28 @@ bool GpuRenderPass::finish() if (i == nullptr || i->finished) return false; + // When a clear was requested but no draw was submitted (no pipeline set), + // encode a clear-only render pass so the framebuffer is actually cleared. + if (i->options.clear && i->orePipeline == nullptr && i->oreCtx != nullptr && i->outputTexture != nullptr) + { + auto outputView = Impl::createView (*i->oreCtx, *i->outputTexture, true); + if (outputView != nullptr) + { + rive::ore::RenderPassDesc rpDesc; + rpDesc.colorCount = 1; + rpDesc.colorAttachments[0].view = outputView.get(); + rpDesc.colorAttachments[0].loadOp = rive::ore::LoadOp::clear; + rpDesc.colorAttachments[0].storeOp = rive::ore::StoreOp::store; + rpDesc.colorAttachments[0].clearColor = { i->options.clearColor.red, + i->options.clearColor.green, + i->options.clearColor.blue, + i->options.clearColor.alpha }; + + auto renderPass = i->oreCtx->beginRenderPass (rpDesc); + renderPass->finish(); + } + } + i->finished = true; return true; } diff --git a/modules/yup_rhi/rhi/yup_GpuRenderPass.h b/modules/yup_rhi/rhi/yup_GpuRenderPass.h index adeb8df56..86cb645c8 100644 --- a/modules/yup_rhi/rhi/yup_GpuRenderPass.h +++ b/modules/yup_rhi/rhi/yup_GpuRenderPass.h @@ -163,7 +163,7 @@ class YUP_API GpuRenderPass static constexpr size_t ImplSizeBytes = 256; TypeErasedObject impl; - YUP_DECLARE_NON_COPYABLE (GpuRenderPass) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuRenderPass) }; } // namespace yup diff --git a/modules/yup_rhi/rhi/yup_GpuTarget.h b/modules/yup_rhi/rhi/yup_GpuTarget.h index 7eb6cca66..b19fce8cd 100644 --- a/modules/yup_rhi/rhi/yup_GpuTarget.h +++ b/modules/yup_rhi/rhi/yup_GpuTarget.h @@ -137,6 +137,8 @@ class YUP_API GpuTarget : public ReferenceCountedObject RenderableTarget* getRenderableTarget() const noexcept { return renderableTarget; } + void invalidateCachedTexture() noexcept { cachedTexture = nullptr; } + GpuDevice::Ptr ctx; std::unique_ptr offscreenTarget; RenderableTarget* renderableTarget = nullptr; diff --git a/modules/yup_rhi/yup_rhi.cpp b/modules/yup_rhi/yup_rhi.cpp index d387bb240..0a27688d7 100644 --- a/modules/yup_rhi/yup_rhi.cpp +++ b/modules/yup_rhi/yup_rhi.cpp @@ -26,6 +26,7 @@ #if YUP_RIVE_USE_D3D #include #include +#include #endif #if YUP_RIVE_USE_OPENGL #include @@ -55,6 +56,8 @@ #endif //============================================================================== +#include "rhi/yup_GpuBuffer.cpp" + #include "native/yup_GpuDevice_headless.cpp" #if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) @@ -76,11 +79,34 @@ #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_GpuComputePass.cpp" + +#if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) +#include "native/yup_GpuComputePipeline_metal.cpp" +#include "native/yup_GpuComputePass_metal.cpp" +#endif +#if YUP_RIVE_USE_D3D && YUP_WINDOWS +#include "native/yup_GpuComputePipeline_d3d.cpp" +#include "native/yup_GpuComputePass_d3d.cpp" +#endif +#if (YUP_EMSCRIPTEN && RIVE_WEBGPU) || YUP_RIVE_USE_DAWN +#include "native/yup_GpuComputePipeline_webgpu.cpp" +#include "native/yup_GpuComputePass_webgpu.cpp" +#endif +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID +#include "native/yup_GpuComputePipeline_opengl.cpp" +#include "native/yup_GpuComputePass_opengl.cpp" +#endif + +#include "rhi/yup_GpuComputePipeline.cpp" + +//============================================================================== #include "rhi/yup_GpuTarget.cpp" #include "rhi/yup_GpuTexture.cpp" #include "rhi/yup_ShaderBindingMap.cpp" diff --git a/modules/yup_rhi/yup_rhi.h b/modules/yup_rhi/yup_rhi.h index 4b6b1c3f2..ce37e3696 100644 --- a/modules/yup_rhi/yup_rhi.h +++ b/modules/yup_rhi/yup_rhi.h @@ -32,7 +32,7 @@ website: https://github.com/kunitoki/yup license: ISC - dependencies: yup_core yup_shading rive_renderer + dependencies: yup_core yup_shading yup_simd rive_renderer appleFrameworks: Metal END_YUP_MODULE_DECLARATION @@ -45,6 +45,7 @@ #include #include +#include //============================================================================== YUP_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") @@ -65,11 +66,13 @@ YUP_END_IGNORE_WARNINGS_GCC_LIKE #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_GpuComputePipeline.h" +#include "rhi/yup_GpuComputePass.h" #include "rhi/yup_GpuRenderPass.h" #include "rhi/yup_GpuTarget.h" #include "rhi/yup_GpuPipelineCache.h" +#include "rhi/yup_ShaderBindingMap.h" diff --git a/modules/yup_shading/shading/yup_ShaderTranspiler.cpp b/modules/yup_shading/shading/yup_ShaderTranspiler.cpp index 59ee0d3c6..0efcacf34 100644 --- a/modules/yup_shading/shading/yup_ShaderTranspiler.cpp +++ b/modules/yup_shading/shading/yup_ShaderTranspiler.cpp @@ -1352,6 +1352,11 @@ ResultValue ShaderTranspiler::decompileFromSPIRV (const MemoryBlock& spi spirv_cross::CompilerMSL::Options mslOpts; mslOpts.use_framebuffer_fetch_subpasses = options.mslUsesFramebufferFetch; + // Use the shader's declared `binding=N` directly as the MSL [[buffer(N)]] + // index, rather than spirv-cross's own auto-incrementing allocation. + // GpuComputePass binds native Metal buffer slots as group*16+binding + // with no reflection indirection, so it requires this to hold. + mslOpts.enable_decoration_binding = true; compiler.set_msl_options (mslOpts); if (! entryName.empty()) @@ -1479,6 +1484,11 @@ ResultValue ShaderTranspiler::reflectFromSPIRV (const MemoryBl spirv_cross::CompilerMSL::Options mslOpts; mslOpts.use_framebuffer_fetch_subpasses = options.mslUsesFramebufferFetch; + // Use the shader's declared `binding=N` directly as the MSL [[buffer(N)]] + // index, rather than spirv-cross's own auto-incrementing allocation. + // GpuComputePass binds native Metal buffer slots as group*16+binding + // with no reflection indirection, so it requires this to hold. + mslOpts.enable_decoration_binding = true; compiler.set_msl_options (mslOpts); if (! entryName.empty()) diff --git a/modules/yup_shading/wgsl/yup_GlslParser.cpp b/modules/yup_shading/wgsl/yup_GlslParser.cpp index 87a9e4e12..10896e301 100644 --- a/modules/yup_shading/wgsl/yup_GlslParser.cpp +++ b/modules/yup_shading/wgsl/yup_GlslParser.cpp @@ -895,15 +895,17 @@ class Parser ss.loc = l; if (lexer.peek().type == TokenType::identifier) + { ss.name = lexer.advance().text; + userStructNames.insert (ss.name); + } expect (TokenType::lBrace); while (lexer.peek().type != TokenType::rBrace && lexer.peek().type != TokenType::endOfFile) { - auto field = parseStructFieldSpecifier(); - if (! field) - return makeResultValueFail (field.getErrorMessage()); - ss.fields.push_back (std::move (field).getValue()); + auto result = parseStructFieldSpecifiers (ss.fields); + if (result.failed()) + return makeResultValueFail (result.getErrorMessage()); } expect (TokenType::rBrace); expect (TokenType::semicolon); @@ -963,6 +965,18 @@ class Parser // Fall through to regular declaration parsing below } } + + // Unnamed block (no instance name): layout(...) buffer BlockName { ... }; + if (blockStruct && lexer.peek().type == TokenType::semicolon) + { + lexer.advance(); // consume ; + Declaration decl; + decl.loc = l; + if (qualifier) + decl.qualifier = std::make_unique (std::move (*qualifier)); + decl.structSpecifier = std::move (blockStruct); + return makeResultValueOk (ExternalDeclaration { std::move (decl) }); + } } // Parse the type specifier (if not already set by block logic above) @@ -2110,7 +2124,8 @@ class Parser "subpassInputMS" }; - return typeNames.find (t) != typeNames.end(); + return typeNames.find (t) != typeNames.end() + || userStructNames.find (t) != userStructNames.end(); } ResultValue parseTypeSpecifier() @@ -2179,10 +2194,9 @@ class Parser // Parse fields while (lexer.peek().type != TokenType::rBrace && lexer.peek().type != TokenType::endOfFile) { - auto field = parseStructFieldSpecifier(); - if (! field) - return makeResultValueFail (field.getErrorMessage()); - ss.fields.push_back (std::move (field).getValue()); + auto result = parseStructFieldSpecifiers (ss.fields); + if (result.failed()) + return makeResultValueFail (result.getErrorMessage()); } expect (TokenType::rBrace); @@ -2195,29 +2209,50 @@ class Parser return makeResultValueOk (std::move (ts)); } - ResultValue parseStructFieldSpecifier() + /** Parses one struct or interface-block member declaration and appends the + members it declares to @p fields. + + A single declaration can introduce several members sharing a base type, + each with its own array specifiers - `uniform Params { float s, r, pad[2]; }` - + which is why the members are appended rather than returned one at a time. + */ + Result parseStructFieldSpecifiers (std::vector& fields) { SourceLocation l = loc(); - StructFieldSpecifier field; - field.loc = l; auto qualifier = parseTypeQualifier(); - if (qualifier) - field.qualifier = std::make_unique (std::move (*qualifier)); auto type = parseTypeSpecifier(); if (! type) - return makeResultValueFail (type.getErrorMessage()); - field.type = std::move (type).getValue(); + return Result::fail (type.getErrorMessage()); - field.name = expect (TokenType::identifier).text; + const auto& baseType = type.getReference(); - // Array specifiers on field - while (lexer.peek().type == TokenType::lBracket) - field.type.arraySpecifiers.push_back (parseArraySpecifier()); + for (bool isFirstDeclarator = true;; isFirstDeclarator = false) + { + StructFieldSpecifier field; + field.loc = l; + field.type = baseType; + field.name = expect (TokenType::identifier).text; + + // Array specifiers bind to the declarator, not to the shared base type. + while (lexer.peek().type == TokenType::lBracket) + field.type.arraySpecifiers.push_back (parseArraySpecifier()); + + // The qualifier belongs to the declaration rather than to any one + // member, so it stays on the first - which is where GLSL applies a + // layout offset. Nothing downstream reads it today. + if (isFirstDeclarator && qualifier) + field.qualifier = std::make_unique (std::move (*qualifier)); + + fields.push_back (std::move (field)); + + if (! match (TokenType::comma)) + break; + } expect (TokenType::semicolon); - return makeResultValueOk (std::move (field)); + return Result::ok(); } static TypeKind typeNameToKind (const std::string& name) @@ -2900,10 +2935,9 @@ class Parser while (lexer.peek().type != TokenType::rBrace && lexer.peek().type != TokenType::endOfFile) { - auto field = parseStructFieldSpecifier(); - if (! field) - return makeResultValueFail (field.getErrorMessage()); - ss.fields.push_back (std::move (field).getValue()); + auto result = parseStructFieldSpecifiers (ss.fields); + if (result.failed()) + return makeResultValueFail (result.getErrorMessage()); } expect (TokenType::rBrace); @@ -3038,6 +3072,7 @@ class Parser } Lexer& lexer; + std::unordered_set userStructNames; }; } // namespace diff --git a/modules/yup_shading/wgsl/yup_WgslEmitter.cpp b/modules/yup_shading/wgsl/yup_WgslEmitter.cpp index 4e30133d0..0abad20be 100644 --- a/modules/yup_shading/wgsl/yup_WgslEmitter.cpp +++ b/modules/yup_shading/wgsl/yup_WgslEmitter.cpp @@ -385,6 +385,13 @@ class Emitter if (! d.structSpecifier) continue; + // Skip structs from unnamed interface blocks — they are flattened + // into individual global variables. + if (! d.initDeclaratorList && d.qualifier + && (d.qualifier->hasStorage (StorageQualifier::uniform) + || d.qualifier->hasStorage (StorageQualifier::buffer))) + continue; + auto& ss = *d.structSpecifier; // Emit struct definition @@ -407,6 +414,39 @@ class Emitter continue; auto& d = std::get (decl); + + // Handle unnamed interface blocks (structSpecifier + qualifier but no initDeclaratorList). + // Each struct field becomes a separate global variable in WGSL. + if (! d.initDeclaratorList && d.structSpecifier && d.qualifier + && (d.qualifier->hasStorage (StorageQualifier::uniform) + || d.qualifier->hasStorage (StorageQualifier::buffer))) + { + bool isBuffer = d.qualifier->hasStorage (StorageQualifier::buffer); + std::string addrSpace = isBuffer ? "storage" : "uniform"; + + for (auto& field : d.structSpecifier->fields) + { + const LoweredProgram::ResourceAssignment* assign = nullptr; + for (auto& r : program.resources) + { + if (r.name == field.name) + { + assign = &r; + break; + } + } + + if (assign) + { + out += "@group(" + std::to_string (assign->group) + ") " + + "@binding(" + std::to_string (assign->binding) + ") "; + } + + out += "var<" + addrSpace + (isBuffer ? ", read_write" : "") + "> " + field.name + ": " + genericTypeName (field.type) + ";\n"; + } + continue; + } + if (! d.initDeclaratorList) continue; @@ -471,7 +511,7 @@ class Emitter } else { - out += "var<" + addrSpace + "> " + sd.name + ": " + genericTypeName (il.type) + ";\n"; + out += "var<" + addrSpace + (isBuffer ? ", read_write" : "") + "> " + sd.name + ": " + genericTypeName (il.type) + ";\n"; } } } @@ -509,6 +549,17 @@ class Emitter out += "var gl_FragCoord: vec4;\n"; out += "var gl_FrontFacing: bool;\n"; } + + // Compute builtins (gl_GlobalInvocationID, etc.) — declared as private + // so main_inner() can access them. + if (program.entryPoint.isCompute) + { + for (auto& io : program.entryPoint.inputs) + { + if (io.isBuiltin) + out += "var " + io.name + ": " + computeInputType (io.builtinName) + ";\n"; + } + } } //========================================================================== @@ -725,11 +776,17 @@ class Emitter out += ", "; first = false; - out += "@builtin(" + io.builtinName + ") " + io.name + ": " + computeInputType (io.builtinName); + out += "@builtin(" + io.builtinName + ") " + io.builtinName + ": " + computeInputType (io.builtinName); } } out += ") {\n"; + // Copy entry-point builtin params to private globals so main_inner() can access them + for (auto& io : program.entryPoint.inputs) + { + if (io.isBuiltin) + out += " " + io.name + " = " + io.builtinName + ";\n"; + } out += " main_inner();\n"; out += "}\n"; } @@ -787,17 +844,24 @@ class Emitter out += ind + "if ("; if (sel.condition) emitExpr (*sel.condition, out); - out += ") "; + out += ") {\n"; if (sel.thenBranch) - emitStatement (*sel.thenBranch, out, indent); - else - out += "{}\n"; + emitStatement (*sel.thenBranch, out, indent + 1); + out += ind + "}\n"; if (sel.elseBranch) { - // Check if the else branch is itself an 'if' -> merge as 'else if' out += ind + "else "; - emitStatement (*sel.elseBranch, out, indent); + // Preserve 'else if' — emit the inner selection without wrapping it in + // an extra layer, since emitStatement will handle its own braces. + if (sel.elseBranch->is()) + emitStatement (*sel.elseBranch, out, indent); + else + { + out += "{\n"; + emitStatement (*sel.elseBranch, out, indent + 1); + out += ind + "}\n"; + } } } else if (stmt.is()) @@ -831,11 +895,10 @@ class Emitter out += ind + "while ("; if (w.condition) emitExpr (*w.condition, out); - out += ") "; + out += ") {\n"; if (w.body) - emitStatement (*w.body, out, indent); - else - out += "{}\n"; + emitStatement (*w.body, out, indent + 1); + out += ind + "}\n"; } else if (stmt.is()) { @@ -884,11 +947,10 @@ class Emitter emitExpr (*update, out); } - out += ") "; + out += ") {\n"; if (f.body) - emitStatement (*f.body, out, indent); - else - out += "{}\n"; + emitStatement (*f.body, out, indent + 1); + out += ind + "}\n"; } else if (stmt.is()) { diff --git a/modules/yup_shading/wgsl/yup_WgslLowering.cpp b/modules/yup_shading/wgsl/yup_WgslLowering.cpp index 62b7cc3da..5ee6bfeea 100644 --- a/modules/yup_shading/wgsl/yup_WgslLowering.cpp +++ b/modules/yup_shading/wgsl/yup_WgslLowering.cpp @@ -465,6 +465,21 @@ class LoweringImpl info.isGlobal = true; symbolTable.declare (d.structSpecifier->name, info); } + + // For unnamed interface blocks (no initDeclaratorList), register + // each field as a global variable so they are accessible directly. + if (d.qualifier + && (d.qualifier->hasStorage (StorageQualifier::uniform) + || d.qualifier->hasStorage (StorageQualifier::buffer))) + { + for (auto& field : d.structSpecifier->fields) + { + SymbolInfo info; + info.type = field.type; + info.isGlobal = true; + symbolTable.declare (field.name, info); + } + } } } else if (std::holds_alternative (decl)) @@ -503,6 +518,32 @@ class LoweringImpl continue; auto& d = std::get (decl); + + // Handle unnamed interface blocks (structSpecifier + qualifier but no initDeclaratorList) + if (! d.initDeclaratorList && d.structSpecifier && d.qualifier + && (d.qualifier->hasStorage (StorageQualifier::uniform) + || d.qualifier->hasStorage (StorageQualifier::buffer))) + { + uint32_t set = ~0u; + uint32_t binding = ~0u; + + if (d.qualifier->layout) + { + for (auto& entry : d.qualifier->layout->entries) + { + if (entry.id == LayoutQualifierId::descriptorSet && entry.value && entry.value->is()) + set = static_cast (entry.value->as().value); + else if (entry.id == LayoutQualifierId::binding && entry.value && entry.value->is()) + binding = static_cast (entry.value->as().value); + } + } + + for (auto& field : d.structSpecifier->fields) + allocator.registerBinding (field.name, set, binding); + + continue; + } + if (! d.initDeclaratorList) continue; @@ -584,6 +625,10 @@ class LoweringImpl if (fd.body) markReassignedSymbols (*fd.body); + // Shadow reassigned parameters: WGSL params are immutable, so rename + // the param and insert a mutable local copy (Task 2.2). + shadowReassignedParams (fd); + // Process function body if (fd.body) { @@ -683,6 +728,86 @@ class LoweringImpl } } + //========================================================================== + // Shadow reassigned parameters (Task 2.2) + //========================================================================== + + void shadowReassignedParams (FunctionDefinition& fd) + { + // Collect reassigned parameter names (skip out/inout — those use + // WGSL pointer references which are already mutable) + std::vector> shadowed; + for (auto& param : fd.prototype.parameters) + { + auto* info = symbolTable.lookup (param.name); + if (info && info->isReassigned + && ! info->isOutParam && ! info->isInoutParam) + shadowed.push_back ({ param.name, param.type }); + } + + if (shadowed.empty()) + return; + + // Rename params in the prototype + for (auto& param : fd.prototype.parameters) + { + for (auto& [origName, origType] : shadowed) + { + if (param.name == origName) + { + param.name = "_" + origName; + break; + } + } + } + + // Insert shadow var declarations at the top of the function body + if (! fd.body || ! fd.body->is()) + return; + + auto& comp = fd.body->as(); + + // Insert in reverse order so they end up in the right order at the top + for (auto it = shadowed.rbegin(); it != shadowed.rend(); ++it) + { + auto& [origName, origType] = *it; + + // Build: var origName: type = _origName; + InitDeclaratorList il; + il.loc = fd.prototype.loc; + il.type = origType; + + SingleDeclaration sd; + sd.loc = fd.prototype.loc; + sd.name = origName; + + Initializer init; + init.loc = fd.prototype.loc; + { + Expr e; + e.loc = fd.prototype.loc; + e.value = ExprVariable { fd.prototype.loc, "_" + origName }; + init.expr = std::make_unique (std::move (e)); + } + + sd.initializer = std::make_unique (std::move (init)); + il.declarations.push_back (std::move (sd)); + + Declaration decl; + decl.loc = fd.prototype.loc; + decl.initDeclaratorList = std::make_unique (std::move (il)); + + StmtDeclaration sdStmt; + sdStmt.loc = fd.prototype.loc; + sdStmt.declaration = std::move (decl); + + Statement stmt; + stmt.loc = fd.prototype.loc; + stmt.value = std::move (sdStmt); + comp.statements.insert (comp.statements.begin(), std::move (stmt)); + } + } + //========================================================================== // Step 2.5: Collect stage IO for entry-point wrapping //========================================================================== diff --git a/modules/yup_simd/buffers/yup_AffineTransformOperations.cpp b/modules/yup_simd/buffers/yup_AffineTransformOperations.cpp index 6d88aa991..683b4897c 100644 --- a/modules/yup_simd/buffers/yup_AffineTransformOperations.cpp +++ b/modules/yup_simd/buffers/yup_AffineTransformOperations.cpp @@ -31,17 +31,17 @@ void YUP_CALLTYPE AffineTransformOperations::transformPoints (const float* srcXs { int i = 0; - const auto sx4 = Float4::broadcast (sx); - const auto shx4 = Float4::broadcast (shx); - const auto tx4 = Float4::broadcast (tx); - const auto shy4 = Float4::broadcast (shy); - const auto sy4 = Float4::broadcast (sy); - const auto ty4 = Float4::broadcast (ty); - - for (; i + Float4::size <= numPoints; i += Float4::size) + const auto sx4 = Float32x4::broadcast (sx); + const auto shx4 = Float32x4::broadcast (shx); + const auto tx4 = Float32x4::broadcast (tx); + const auto shy4 = Float32x4::broadcast (shy); + const auto sy4 = Float32x4::broadcast (sy); + const auto ty4 = Float32x4::broadcast (ty); + + for (; i + Float32x4::size <= numPoints; i += Float32x4::size) { - const auto x = Float4::loadUnaligned (srcXs + i); - const auto y = Float4::loadUnaligned (srcYs + i); + const auto x = Float32x4::loadUnaligned (srcXs + i); + const auto y = Float32x4::loadUnaligned (srcYs + i); const auto outX = tx4.mulAdd (sx4, x).mulAdd (shx4, y); const auto outY = ty4.mulAdd (shy4, x).mulAdd (sy4, y); diff --git a/modules/yup_simd/buffers/yup_ColorVectorOperations.cpp b/modules/yup_simd/buffers/yup_ColorVectorOperations.cpp index dbde342f1..ac3d133ff 100644 --- a/modules/yup_simd/buffers/yup_ColorVectorOperations.cpp +++ b/modules/yup_simd/buffers/yup_ColorVectorOperations.cpp @@ -24,6 +24,7 @@ namespace yup namespace { + static uint32 premultiplyComponent (uint32 component, uint32 alpha) noexcept { return (component * alpha + 127u) / 255u; @@ -38,7 +39,29 @@ static uint32 packRGBA (uint32 red, uint32 green, uint32 blue, uint32 alpha) noe //============================================================================== void YUP_CALLTYPE ColorVectorOperations::premultiplyARGB (uint32* pixels, int numPixels) noexcept { - for (int i = 0; i < numPixels; ++i) + const auto c127 = Uint32x4 (127u); + const auto c255 = Uint32x4 (255u); + const auto mask8 = Uint32x4 (0xFFu); + + int i = 0; + + for (; i + 4 <= numPixels; i += 4) + { + auto p = Uint32x4::loadUnaligned (pixels + i); + const auto a = p >> 24; + + auto r = ((p >> 16) & mask8) * a; + auto g = ((p >> 8) & mask8) * a; + auto b = (p & mask8) * a; + + r = (r + c127) / c255; + g = (g + c127) / c255; + b = (b + c127) / c255; + + ((a << 24) | (r << 16) | (g << 8) | b).storeUnaligned (pixels + i); + } + + for (; i < numPixels; ++i) { const uint32 pixel = pixels[i]; const uint32 alpha = (pixel >> 24) & 0xffu; @@ -50,62 +73,118 @@ void YUP_CALLTYPE ColorVectorOperations::premultiplyARGB (uint32* pixels, int nu } } -void YUP_CALLTYPE ColorVectorOperations::premultiplyRGBA (uint8* pixels, int numPixels) noexcept +void YUP_CALLTYPE ColorVectorOperations::premultiplyRGBA (uint32* pixels, int numPixels) noexcept { - auto* pixel = pixels; + const auto c127 = Uint32x4 (127u); + const auto c255 = Uint32x4 (255u); + const auto mask8 = Uint32x4 (0xFFu); - for (int i = 0; i < numPixels; ++i) + int i = 0; + + for (; i + 4 <= numPixels; i += 4) { - const uint32 alpha = pixel[3]; + auto p = Uint32x4::loadUnaligned (pixels + i); + const auto a = p >> 24; + + auto r = (p & mask8) * a; + auto g = ((p >> 8) & mask8) * a; + auto b = ((p >> 16) & mask8) * a; + + r = (r + c127) / c255; + g = (g + c127) / c255; + b = (b + c127) / c255; + + ((a << 24) | (b << 16) | (g << 8) | r).storeUnaligned (pixels + i); + } + + for (; i < numPixels; ++i) + { + const uint32 pixel = pixels[i]; + const uint32 alpha = pixel >> 24; + const uint32 r = premultiplyComponent (pixel & 0xFFu, alpha); + const uint32 g = premultiplyComponent ((pixel >> 8) & 0xFFu, alpha); + const uint32 b = premultiplyComponent ((pixel >> 16) & 0xFFu, alpha); - pixel[0] = static_cast (premultiplyComponent (pixel[0], alpha)); - pixel[1] = static_cast (premultiplyComponent (pixel[1], alpha)); - pixel[2] = static_cast (premultiplyComponent (pixel[2], alpha)); - pixel += 4; + pixels[i] = (alpha << 24) | (b << 16) | (g << 8) | r; } } void YUP_CALLTYPE ColorVectorOperations::convertARGBtoRGBA (const uint32* src, uint32* dst, int numPixels) noexcept { - for (int i = 0; i < numPixels; ++i) + int i = 0; + + for (; i + 4 <= numPixels; i += 4) + { + const auto p = Uint32x4::loadUnaligned (src + i); + ((p << 8) | (p >> 24)).storeUnaligned (dst + i); + } + + for (; i < numPixels; ++i) { const uint32 pixel = src[i]; dst[i] = ((pixel & 0x00ffffffu) << 8) | ((pixel >> 24) & 0xffu); } } -void YUP_CALLTYPE ColorVectorOperations::convertGrayscaleToRGBA (const uint8* src, uint8* dst, int numPixels) noexcept +//============================================================================== +void YUP_CALLTYPE ColorVectorOperations::convertBGRAtoRGBA (uint32* pixels, int numPixels) noexcept +{ + if (numPixels <= 0) + return; + + const auto maskAG = Uint32x4 (0xFF00FF00u); + const auto maskFF = Uint32x4 (0xFFu); + + const int simdPixels = numPixels & ~3; + int i = 0; + + for (; i < simdPixels; i += 4) + { + const auto p = Uint32x4::loadUnaligned (pixels + i); + + // BGRA (uint32 little-endian): bytes [B,G,R,A] + // RGBA: bytes [R,G,B,A] + // Transformation: swap byte 0 (B) ↔ byte 2 (R), keep bytes 1 (G) and 3 (A). + const auto result = (p & maskAG) | ((p & maskFF) << 16) | ((p >> 16) & maskFF); + result.storeUnaligned (pixels + i); + } + + // Scalar tail for remaining < 4 pixels. + for (; i < numPixels; ++i) + { + const auto p = pixels[i]; + pixels[i] = (p & 0xFF00FF00u) | ((p & 0xFFu) << 16) | ((p >> 16) & 0xFFu); + } +} + +void YUP_CALLTYPE ColorVectorOperations::convertGrayscaleToRGBA (const uint8* src, uint32* dst, int numPixels) noexcept { for (int i = 0; i < numPixels; ++i) { const uint32 value = *src++; - const auto rgba = packRGBA (value, value, value, 255u); - std::memcpy (dst, &rgba, sizeof (rgba)); - dst += 4; + dst[i] = packRGBA (value, value, value, 255u); } } -void YUP_CALLTYPE ColorVectorOperations::convertRGBToRGBA (const uint8* src, uint8* dst, int numPixels) noexcept +void YUP_CALLTYPE ColorVectorOperations::convertRGBToRGBA (const uint8* src, uint32* dst, int numPixels) noexcept { for (int i = 0; i < numPixels; ++i) { - const auto rgba = packRGBA (src[0], src[1], src[2], 255u); - std::memcpy (dst, &rgba, sizeof (rgba)); + dst[i] = packRGBA (src[0], src[1], src[2], 255u); src += 3; - dst += 4; } } void YUP_CALLTYPE ColorVectorOperations::lerpRows (const float* rowA, const float* rowB, float* dst, float t, int numPixels) noexcept { - const auto t4 = Float4::broadcast (t); - const auto minusOne = Float4::broadcast (-1.0f); + const auto t4 = Float32x4::broadcast (t); + const auto minusOne = Float32x4::broadcast (-1.0f); int i = 0; for (; i < numPixels; ++i) { - const auto a = Float4::loadUnaligned (rowA + i * 4); - const auto b = Float4::loadUnaligned (rowB + i * 4); + const auto a = Float32x4::loadUnaligned (rowA + i * 4); + const auto b = Float32x4::loadUnaligned (rowB + i * 4); a.mulAdd (b + (a * minusOne), t4).storeUnaligned (dst + i * 4); } } diff --git a/modules/yup_simd/buffers/yup_ColorVectorOperations.h b/modules/yup_simd/buffers/yup_ColorVectorOperations.h index aabc19982..c687a5fa4 100644 --- a/modules/yup_simd/buffers/yup_ColorVectorOperations.h +++ b/modules/yup_simd/buffers/yup_ColorVectorOperations.h @@ -31,8 +31,8 @@ class YUP_API ColorVectorOperations /** Premultiplies alpha in-place on a row of packed `0xAARRGGBB` pixels. */ static void YUP_CALLTYPE premultiplyARGB (uint32* pixels, int numPixels) noexcept; - /** Premultiplies alpha in-place on a row of RGBA byte pixels. */ - static void YUP_CALLTYPE premultiplyRGBA (uint8* pixels, int numPixels) noexcept; + /** Premultiplies alpha in-place on a row of packed RGBA pixels. */ + static void YUP_CALLTYPE premultiplyRGBA (uint32* pixels, int numPixels) noexcept; /** Converts packed `0xAARRGGBB` pixels to packed `0xRRGGBBAA` pixels. @@ -40,11 +40,20 @@ class YUP_API ColorVectorOperations */ static void YUP_CALLTYPE convertARGBtoRGBA (const uint32* src, uint32* dst, int numPixels) noexcept; - /** Expands 8-bit grayscale pixels to RGBA byte pixels with opaque alpha. */ - static void YUP_CALLTYPE convertGrayscaleToRGBA (const uint8* src, uint8* dst, int numPixels) noexcept; + /** Swaps the R and B channels of packed BGRA pixels to RGBA in place. - /** Expands RGB byte pixels to RGBA byte pixels with opaque alpha. */ - static void YUP_CALLTYPE convertRGBToRGBA (const uint8* src, uint8* dst, int numPixels) noexcept; + Processes 4 pixels per iteration using xsimd; falls back to a + scalar R ↔ B swap loop for the tail. + + The pixel count must accurately reflect the number of pixels in `pixels`. + */ + static void YUP_CALLTYPE convertBGRAtoRGBA (uint32* pixels, int numPixels) noexcept; + + /** Expands 8-bit grayscale pixels to packed RGBA pixels with opaque alpha. */ + static void YUP_CALLTYPE convertGrayscaleToRGBA (const uint8* src, uint32* dst, int numPixels) noexcept; + + /** Expands RGB byte pixels to packed RGBA pixels with opaque alpha. */ + static void YUP_CALLTYPE convertRGBToRGBA (const uint8* src, uint32* dst, int numPixels) noexcept; /** Blends rows of float RGBA pixels using `dst = rowA + (rowB - rowA) * t`. */ static void YUP_CALLTYPE lerpRows (const float* rowA, const float* rowB, float* dst, float t, int numPixels) noexcept; diff --git a/modules/yup_simd/types/yup_SIMDRegister.h b/modules/yup_simd/types/yup_SIMDRegister.h index fa187270f..237631023 100644 --- a/modules/yup_simd/types/yup_SIMDRegister.h +++ b/modules/yup_simd/types/yup_SIMDRegister.h @@ -238,6 +238,63 @@ class SIMDRegister return *this; } + //============================================================================== + /** Bitwise AND of two SIMD registers element-wise. + + @param other The SIMD register to AND with. + + @returns A SIMD register containing the element-wise bitwise AND. + */ + forcedinline SIMDRegister operator& (SIMDRegister other) const noexcept + { + SIMDRegister result; + for (std::size_t i = 0; i < numBatches; ++i) + result.data[i] = data[i] & other.data[i]; + return result; + } + + /** Bitwise OR of two SIMD registers element-wise. + + @param other The SIMD register to OR with. + + @returns A SIMD register containing the element-wise bitwise OR. + */ + forcedinline SIMDRegister operator| (SIMDRegister other) const noexcept + { + SIMDRegister result; + for (std::size_t i = 0; i < numBatches; ++i) + result.data[i] = data[i] | other.data[i]; + return result; + } + + /** Left shift each element by a scalar amount. + + @param shift The number of bits to shift left. + + @returns A SIMD register containing the shifted elements. + */ + forcedinline SIMDRegister operator<< (int shift) const noexcept + { + SIMDRegister result; + for (std::size_t i = 0; i < numBatches; ++i) + result.data[i] = data[i] << shift; + return result; + } + + /** Right shift each element by a scalar amount. + + @param shift The number of bits to shift right. + + @returns A SIMD register containing the shifted elements. + */ + forcedinline SIMDRegister operator>> (int shift) const noexcept + { + SIMDRegister result; + for (std::size_t i = 0; i < numBatches; ++i) + result.data[i] = data[i] >> shift; + return result; + } + //============================================================================== /** Performs a fused multiply-add operation: this + (a * b). @@ -382,9 +439,10 @@ class SIMDRegister std::array data; }; -using Float4 = SIMDRegister; -using Float8 = SIMDRegister; -using Double2 = SIMDRegister; -using Double4 = SIMDRegister; +using Float32x4 = SIMDRegister; +using Float32x8 = SIMDRegister; +using Float64x2 = SIMDRegister; +using Float64x4 = SIMDRegister; +using Uint32x4 = SIMDRegister; } // namespace yup diff --git a/modules/yup_simd/types/yup_Vec.h b/modules/yup_simd/types/yup_Vec.h index 6e6157fc8..735ff8ebc 100644 --- a/modules/yup_simd/types/yup_Vec.h +++ b/modules/yup_simd/types/yup_Vec.h @@ -86,21 +86,21 @@ struct alignas (16) Vec4f Vec4f operator+ (Vec4f other) const noexcept { alignas (16) float result[4]; - (Float4::loadUnaligned (data()) + Float4::loadUnaligned (other.data())).storeUnaligned (result); + (Float32x4::loadUnaligned (data()) + Float32x4::loadUnaligned (other.data())).storeUnaligned (result); return load (result); } Vec4f operator* (float scalar) const noexcept { alignas (16) float result[4]; - (Float4::loadUnaligned (data()) * Float4::broadcast (scalar)).storeUnaligned (result); + (Float32x4::loadUnaligned (data()) * Float32x4::broadcast (scalar)).storeUnaligned (result); return load (result); } Vec4f operator* (Vec4f other) const noexcept { alignas (16) float result[4]; - (Float4::loadUnaligned (data()) * Float4::loadUnaligned (other.data())).storeUnaligned (result); + (Float32x4::loadUnaligned (data()) * Float32x4::loadUnaligned (other.data())).storeUnaligned (result); return load (result); } diff --git a/modules/yup_simd/yup_simd.h b/modules/yup_simd/yup_simd.h index e34364d1e..69cd47f96 100644 --- a/modules/yup_simd/yup_simd.h +++ b/modules/yup_simd/yup_simd.h @@ -65,6 +65,8 @@ #include #include +//============================================================================== +// x86 / x64 SIMD feature detection //============================================================================== #ifndef YUP_USE_SSE_INTRINSICS #if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) @@ -72,12 +74,48 @@ #endif #endif +#ifndef YUP_USE_SSE2_INTRINSICS +#if defined(__SSE2__) || defined(YUP_USE_SSE_INTRINSICS) +#define YUP_USE_SSE2_INTRINSICS 1 +#endif +#endif + +#ifndef YUP_USE_SSE3_INTRINSICS +#if defined(__SSE3__) +#define YUP_USE_SSE3_INTRINSICS 1 +#endif +#endif + +#ifndef YUP_USE_SSSE3_INTRINSICS +#if defined(__SSSE3__) +#define YUP_USE_SSSE3_INTRINSICS 1 +#endif +#endif + +#ifndef YUP_USE_SSE4_1_INTRINSICS +#if defined(__SSE4_1__) +#define YUP_USE_SSE4_1_INTRINSICS 1 +#endif +#endif + +#ifndef YUP_USE_SSE4_2_INTRINSICS +#if defined(__SSE4_2__) +#define YUP_USE_SSE4_2_INTRINSICS 1 +#endif +#endif + #ifndef YUP_USE_AVX_INTRINSICS -#if defined(__AVX2__) +#if defined(__AVX__) #define YUP_USE_AVX_INTRINSICS 1 #endif #endif +#ifndef YUP_USE_AVX2_INTRINSICS +#if defined(__AVX2__) +#define YUP_USE_AVX2_INTRINSICS 1 +#endif +#endif + #ifndef YUP_USE_FMA_INTRINSICS #if defined(__FMA__) #define YUP_USE_FMA_INTRINSICS 1 @@ -86,10 +124,19 @@ #if ! YUP_INTEL #undef YUP_USE_SSE_INTRINSICS +#undef YUP_USE_SSE2_INTRINSICS +#undef YUP_USE_SSE3_INTRINSICS +#undef YUP_USE_SSSE3_INTRINSICS +#undef YUP_USE_SSE4_1_INTRINSICS +#undef YUP_USE_SSE4_2_INTRINSICS #undef YUP_USE_AVX_INTRINSICS +#undef YUP_USE_AVX2_INTRINSICS #undef YUP_USE_FMA_INTRINSICS #endif +//============================================================================== +// ARM SIMD feature detection +//============================================================================== #if __ARM_NEON__ && ! (YUP_USE_VDSP_FRAMEWORK || defined(YUP_USE_ARM_NEON)) #define YUP_USE_ARM_NEON 1 #endif @@ -101,6 +148,18 @@ #define YUP_USE_ARM_NEON 0 #endif +#ifndef YUP_USE_NEON64_INTRINSICS +#if defined(__aarch64__) +#define YUP_USE_NEON64_INTRINSICS 1 +#endif +#endif + +#ifndef YUP_USE_SVE_INTRINSICS +#if defined(__ARM_FEATURE_SVE) +#define YUP_USE_SVE_INTRINSICS 1 +#endif +#endif + //============================================================================== #if (YUP_MAC || YUP_IOS) && __has_include() #ifndef YUP_USE_VDSP_FRAMEWORK @@ -112,11 +171,21 @@ #endif //============================================================================== -#if YUP_USE_AVX_INTRINSICS || YUP_USE_FMA_INTRINSICS +// Intrinsics headers +//============================================================================== +#if YUP_USE_AVX_INTRINSICS || YUP_USE_AVX2_INTRINSICS || YUP_USE_FMA_INTRINSICS #include #endif -#if YUP_USE_SSE_INTRINSICS +#if YUP_USE_SSE4_2_INTRINSICS && ! (YUP_USE_AVX_INTRINSICS || YUP_USE_AVX2_INTRINSICS || YUP_USE_FMA_INTRINSICS) +#include +#elif YUP_USE_SSE4_1_INTRINSICS && ! (YUP_USE_AVX_INTRINSICS || YUP_USE_AVX2_INTRINSICS || YUP_USE_FMA_INTRINSICS) +#include +#elif YUP_USE_SSSE3_INTRINSICS && ! (YUP_USE_AVX_INTRINSICS || YUP_USE_AVX2_INTRINSICS || YUP_USE_FMA_INTRINSICS) +#include +#elif YUP_USE_SSE3_INTRINSICS && ! (YUP_USE_AVX_INTRINSICS || YUP_USE_AVX2_INTRINSICS || YUP_USE_FMA_INTRINSICS) +#include +#elif YUP_USE_SSE2_INTRINSICS && ! (YUP_USE_AVX_INTRINSICS || YUP_USE_AVX2_INTRINSICS || YUP_USE_FMA_INTRINSICS) #include #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index afdc5af72..56dece477 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -215,7 +215,7 @@ if (YUP_PLATFORM_DESKTOP) endif() target_compile_options (${target_name} PRIVATE - $<$:-Wno-subobject-linkage>) + $<$,$>:-Wno-subobject-linkage>) set_target_properties (${target_name} PROPERTIES XCODE_SCHEME_ARGUMENTS "--gtest_filter=*" diff --git a/tests/main.cpp b/tests/main.cpp index da9861a0d..71d7304ad 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -106,6 +106,13 @@ struct TestApplication : yup::YUPApplication void initialise (const yup::String& commandLineParameters) override { + yup::SystemStats::setApplicationCrashHandler ([] (void*) + { + auto trace = yup::SystemStats::getStackBacktrace(); + std::fprintf (stderr, "\n=== CRASH ===\n%s\n=== END CRASH ===\n", trace.toRawUTF8()); + std::fflush (stderr); + }); + yup::Array argv; auto applicationName = getApplicationName(); diff --git a/tests/mocks/rive_ore.h b/tests/mocks/rive_ore.h index 835aa4713..481ee90c5 100644 --- a/tests/mocks/rive_ore.h +++ b/tests/mocks/rive_ore.h @@ -91,8 +91,10 @@ class MockOreRenderPass : public rive::ore::RenderPass class MockOreBuffer : public rive::ore::Buffer { public: - MockOreBuffer() - : rive::ore::Buffer (0, rive::ore::BufferUsage::uniform) + /** Reports @p size from Buffer::size(), which callers use to bucket buffers. */ + explicit MockOreBuffer (uint32_t size = 0, + rive::ore::BufferUsage usage = rive::ore::BufferUsage::uniform) + : rive::ore::Buffer (size, usage) { } @@ -142,6 +144,15 @@ struct TestOreBindGroup : public rive::ore::BindGroup struct TestOreBindGroupLayout : public rive::ore::BindGroupLayout { TestOreBindGroupLayout() = default; + + /** Declares a binding, so tests can exercise layout-driven code paths. */ + void addEntry (uint32_t binding, rive::ore::BindingKind kind) + { + rive::ore::BindGroupLayoutEntry entry; + entry.binding = binding; + entry.kind = kind; + m_entries.push_back (entry); + } }; struct TestOreSampler : public rive::ore::Sampler diff --git a/tests/mocks/yup_graphics.h b/tests/mocks/yup_graphics.h index 99d80ca8d..cce50de72 100644 --- a/tests/mocks/yup_graphics.h +++ b/tests/mocks/yup_graphics.h @@ -31,7 +31,7 @@ // Test helper: Delegate GraphicsContext that allows injecting a mock ore context. // // Wraps a real (headless) GraphicsContext and delegates all methods to it except -// gpuContext(), which returns the supplied rive::ore::Context*. +// getGpuContext(), which returns the supplied rive::ore::Context*. // ============================================================================== class OreInjectedGraphicsContext : public yup::GraphicsContext @@ -47,11 +47,11 @@ class OreInjectedGraphicsContext : public yup::GraphicsContext yup::GpuDevice::Ptr getGpuDevice() const noexcept override { return real->getGpuDevice(); } - rive::Factory* factory() override { return real->factory(); } + rive::Factory* getFactory() override { return real->getFactory(); } - rive::gpu::RenderContext* renderContext() override { return real->renderContext(); } + rive::gpu::RenderContext* getRenderContext() override { return real->getRenderContext(); } - rive::gpu::RenderTarget* renderTarget() override { return real->renderTarget(); } + rive::gpu::RenderTarget* getRenderTarget() override { return real->getRenderTarget(); } std::unique_ptr makeRenderer (int width, int height) override { return real->makeRenderer (width, height); } diff --git a/tests/mocks/yup_rhi.h b/tests/mocks/yup_rhi.h index 87843908c..424622fa8 100644 --- a/tests/mocks/yup_rhi.h +++ b/tests/mocks/yup_rhi.h @@ -29,7 +29,7 @@ // 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*. +// getGpuContext(), which returns the supplied rive::ore::Context*. // ============================================================================== class OreInjectedGpuDevice : public yup::GpuDevice @@ -41,9 +41,11 @@ class OreInjectedGpuDevice : public yup::GpuDevice { } + ~OreInjectedGpuDevice() override { releasePooledResources(); } + yup::GpuPlatform getPlatform() const noexcept override { return real->getPlatform(); } - rive::ore::Context* gpuContext() const noexcept override { return injectedOreContext; } + rive::ore::Context* getGpuContext() const noexcept override { return injectedOreContext; } std::unique_ptr createOffscreenTarget (int width, int height) override { return real->createOffscreenTarget (width, height); } diff --git a/tests/yup_animation/yup_AnimationRenderer.cpp b/tests/yup_animation/yup_AnimationRenderer.cpp index 4e7075527..5d97c6d78 100644 --- a/tests/yup_animation/yup_AnimationRenderer.cpp +++ b/tests/yup_animation/yup_AnimationRenderer.cpp @@ -1291,6 +1291,64 @@ constexpr const char* kFillEffectJson = R"json({ ] })json"; +// A paint-less nested group carrying the modifier that defines the outline, with the +// paint on the parent group. This is how RubberHose rigs draw a limb: a 4-point star +// trimmed down to an arc, stroked by the enclosing group. The nested group's trim has +// to survive being handed up to that stroke, otherwise the whole star gets painted. +constexpr const char* kNestedTrimmedGroupJson = R"json({ + "v": "5.7.0", "fr": 25, "ip": 0, "op": 50, "w": 100, "h": 100, "nm": "nested trim", + "layers": [ + { + "ddd": 0, "ind": 1, "ty": 4, "nm": "hose", "sr": 1, "ao": 0, + "ip": 0, "op": 50, "st": 0, "bm": 0, + "ks": { + "o": { "a": 0, "k": 100 }, "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [50, 50, 0] }, "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] } + }, + "shapes": [ + { + "ty": "gr", "nm": "BaseHose", "hd": false, + "it": [ + { + "ty": "gr", "nm": "Arc", "hd": false, + "it": [ + { + "ty": "sr", "nm": "LineForCurve", "hd": false, "sy": 1, + "pt": { "a": 0, "k": 4 }, "p": { "a": 0, "k": [0, 0] }, + "r": { "a": 0, "k": 0 }, "ir": { "a": 0, "k": 20 }, + "is": { "a": 0, "k": 0 }, "or": { "a": 0, "k": 40 }, + "os": { "a": 0, "k": 0 } + }, + { + "ty": "tm", "nm": "Line Halfer", "hd": false, "m": 1, + "s": { "a": 0, "k": 0 }, "e": { "a": 0, "k": 25 }, + "o": { "a": 0, "k": 0 } + }, + { + "ty": "tr", "p": { "a": 0, "k": [0, 0] }, "a": { "a": 0, "k": [0, 0] }, + "s": { "a": 0, "k": [100, 100] }, "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + } + ] + }, + { + "ty": "st", "nm": "Stroke 1", "hd": false, "lc": 2, "lj": 2, + "c": { "a": 0, "k": [0, 0, 0, 1] }, "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 4 } + }, + { + "ty": "tr", "p": { "a": 0, "k": [0, 0] }, "a": { "a": 0, "k": [0, 0] }, + "s": { "a": 0, "k": [100, 100] }, "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + } + ] + } + ] + } + ] +})json"; + } // namespace class AnimationRendererTests : public ::testing::Test @@ -1331,6 +1389,43 @@ TEST_F (AnimationRendererTests, RenderShapeLayerCompositionDoesNotCrash) }); } +TEST_F (AnimationRendererTests, RenderNestedTrimmedGroupParsesAndRenders) +{ + auto comp = LottieReader::parseData (kNestedTrimmedGroupJson).valueOr (nullptr); + ASSERT_NE (comp, nullptr); + ASSERT_EQ (comp->layers.size(), 1u); + + // The trim must be parsed onto the nested group rather than the layer's outer + // group, since that is what the geometry hand-off has to carry up to the stroke. + const auto* shapeLayer = dynamic_cast (comp->layers[0].get()); + ASSERT_NE (shapeLayer, nullptr); + ASSERT_EQ (shapeLayer->groups.size(), 1u); + + const auto* baseHose = shapeLayer->groups[0].get(); + ASSERT_NE (baseHose, nullptr); + + const AnimationGroup* arc = nullptr; + bool baseHoseHasStroke = false; + for (const auto& child : baseHose->children) + { + if (child.kind == AnimationGroup::ChildKind::Group && child.group != nullptr) + arc = child.group.get(); + else if (child.kind == AnimationGroup::ChildKind::Stroke) + baseHoseHasStroke = true; + } + + EXPECT_TRUE (baseHoseHasStroke); + ASSERT_NE (arc, nullptr); + EXPECT_TRUE (arc->hasAnyModifier); + + auto renderer = context->makeRenderer (100, 100); + Graphics g (*context, *renderer); + + EXPECT_NO_THROW ({ + AnimationRenderer::renderComposition (g, *comp, 0.0f, Rectangle (0, 0, 100, 100)); + }); +} + TEST_F (AnimationRendererTests, RenderSolidLayerCompositionDoesNotCrash) { auto comp = LottieReader::parseData (kSolidLayerJson).valueOr (nullptr); diff --git a/tests/yup_core/yup_MathFunctions.cpp b/tests/yup_core/yup_MathFunctions.cpp index 2c79e5bb6..69041dcc0 100644 --- a/tests/yup_core/yup_MathFunctions.cpp +++ b/tests/yup_core/yup_MathFunctions.cpp @@ -86,10 +86,135 @@ TEST (MathFunctionsTests, YupAbs_Runtime) EXPECT_DOUBLE_EQ (yup_abs (2.71828182845904523536), 2.71828182845904523536); } +//============================================================================== +// yup_sqrt Tests +//============================================================================== + +TEST (MathFunctionsTests, YupSqrt_ConstexprVsRuntime) +{ + // Float: constexpr path vs runtime path + { + constexpr float x0 = yup_sqrt (0.0f); + constexpr float x1 = yup_sqrt (1.0f); + constexpr float x4 = yup_sqrt (4.0f); + constexpr float x9 = yup_sqrt (9.0f); + constexpr float x16 = yup_sqrt (16.0f); + constexpr float x100 = yup_sqrt (100.0f); + constexpr float x2 = yup_sqrt (2.0f); + + EXPECT_FLOAT_EQ (yup_sqrt (0.0f), x0); + EXPECT_FLOAT_EQ (yup_sqrt (1.0f), x1); + EXPECT_FLOAT_EQ (yup_sqrt (4.0f), x4); + EXPECT_FLOAT_EQ (yup_sqrt (9.0f), x9); + EXPECT_FLOAT_EQ (yup_sqrt (16.0f), x16); + EXPECT_FLOAT_EQ (yup_sqrt (100.0f), x100); + EXPECT_FLOAT_EQ (yup_sqrt (2.0f), x2); + } + + // Double: constexpr path vs runtime path + { + constexpr double x0 = yup_sqrt (0.0); + constexpr double x1 = yup_sqrt (1.0); + constexpr double x4 = yup_sqrt (4.0); + constexpr double x9 = yup_sqrt (9.0); + constexpr double x16 = yup_sqrt (16.0); + constexpr double x100 = yup_sqrt (100.0); + constexpr double x2 = yup_sqrt (2.0); + + EXPECT_DOUBLE_EQ (yup_sqrt (0.0), x0); + EXPECT_DOUBLE_EQ (yup_sqrt (1.0), x1); + EXPECT_DOUBLE_EQ (yup_sqrt (4.0), x4); + EXPECT_DOUBLE_EQ (yup_sqrt (9.0), x9); + EXPECT_DOUBLE_EQ (yup_sqrt (16.0), x16); + EXPECT_DOUBLE_EQ (yup_sqrt (100.0), x100); + EXPECT_DOUBLE_EQ (yup_sqrt (2.0), x2); + } +} + +TEST (MathFunctionsTests, YupSqrt_Runtime) +{ + // Integer sqrt via implicit conversion + EXPECT_EQ (yup_sqrt (0), 0); + EXPECT_EQ (yup_sqrt (1), 1); + EXPECT_EQ (yup_sqrt (4), 2); + EXPECT_EQ (yup_sqrt (9), 3); + EXPECT_EQ (yup_sqrt (16), 4); + EXPECT_EQ (yup_sqrt (25), 5); + EXPECT_EQ (yup_sqrt (100), 10); + + // Float tests + EXPECT_FLOAT_EQ (yup_sqrt (0.0f), 0.0f); + EXPECT_FLOAT_EQ (yup_sqrt (1.0f), 1.0f); + EXPECT_FLOAT_EQ (yup_sqrt (4.0f), 2.0f); + EXPECT_FLOAT_EQ (yup_sqrt (9.0f), 3.0f); + EXPECT_FLOAT_EQ (yup_sqrt (16.0f), 4.0f); + EXPECT_FLOAT_EQ (yup_sqrt (2.0f), 1.41421356f); + EXPECT_FLOAT_EQ (yup_sqrt (3.0f), 1.73205081f); + EXPECT_FLOAT_EQ (yup_sqrt (100.0f), 10.0f); + + // Negative input should return NaN + EXPECT_TRUE (std::isnan (yup_sqrt (-1.0f))); + EXPECT_TRUE (std::isnan (yup_sqrt (-4.0f))); + + // Double tests + EXPECT_DOUBLE_EQ (yup_sqrt (0.0), 0.0); + EXPECT_DOUBLE_EQ (yup_sqrt (1.0), 1.0); + EXPECT_DOUBLE_EQ (yup_sqrt (4.0), 2.0); + EXPECT_DOUBLE_EQ (yup_sqrt (9.0), 3.0); + EXPECT_DOUBLE_EQ (yup_sqrt (16.0), 4.0); + EXPECT_DOUBLE_EQ (yup_sqrt (2.0), 1.4142135623730951); + EXPECT_DOUBLE_EQ (yup_sqrt (100.0), 10.0); + + // Negative input should return NaN + EXPECT_TRUE (std::isnan (yup_sqrt (-1.0))); + EXPECT_TRUE (std::isnan (yup_sqrt (-4.0))); +} + //============================================================================== // yup_hypot Tests //============================================================================== +TEST (MathFunctionsTests, YupHypot_ConstexprVsRuntime) +{ + // Float: constexpr path vs runtime path + { + constexpr float h0 = yup_hypot (0.0f, 0.0f); + constexpr float h1 = yup_hypot (1.0f, 0.0f); + constexpr float h2 = yup_hypot (0.0f, 1.0f); + constexpr float h3 = yup_hypot (3.0f, 4.0f); + constexpr float h4 = yup_hypot (5.0f, 12.0f); + constexpr float h5 = yup_hypot (8.0f, 6.0f); + constexpr float h6 = yup_hypot (-3.0f, 4.0f); + + EXPECT_FLOAT_EQ (yup_hypot (0.0f, 0.0f), h0); + EXPECT_FLOAT_EQ (yup_hypot (1.0f, 0.0f), h1); + EXPECT_FLOAT_EQ (yup_hypot (0.0f, 1.0f), h2); + EXPECT_FLOAT_EQ (yup_hypot (3.0f, 4.0f), h3); + EXPECT_FLOAT_EQ (yup_hypot (5.0f, 12.0f), h4); + EXPECT_FLOAT_EQ (yup_hypot (8.0f, 6.0f), h5); + EXPECT_FLOAT_EQ (yup_hypot (-3.0f, 4.0f), h6); + } + + // Double: constexpr path vs runtime path + { + constexpr double h0 = yup_hypot (0.0, 0.0); + constexpr double h1 = yup_hypot (1.0, 0.0); + constexpr double h2 = yup_hypot (0.0, 1.0); + constexpr double h3 = yup_hypot (3.0, 4.0); + constexpr double h4 = yup_hypot (5.0, 12.0); + constexpr double h5 = yup_hypot (8.0, 6.0); + constexpr double h6 = yup_hypot (-3.0, 4.0); + + EXPECT_DOUBLE_EQ (yup_hypot (0.0, 0.0), h0); + EXPECT_DOUBLE_EQ (yup_hypot (1.0, 0.0), h1); + EXPECT_DOUBLE_EQ (yup_hypot (0.0, 1.0), h2); + EXPECT_DOUBLE_EQ (yup_hypot (3.0, 4.0), h3); + EXPECT_DOUBLE_EQ (yup_hypot (5.0, 12.0), h4); + EXPECT_DOUBLE_EQ (yup_hypot (8.0, 6.0), h5); + EXPECT_DOUBLE_EQ (yup_hypot (-3.0, 4.0), h6); + } +} + TEST (MathFunctionsTests, YupHypot_Float) { EXPECT_FLOAT_EQ (yup_hypot (3.0f, 4.0f), 5.0f); diff --git a/tests/yup_graphics/yup_AffineTransform.cpp b/tests/yup_graphics/yup_AffineTransform.cpp index a6d464ef0..3f4b24666 100644 --- a/tests/yup_graphics/yup_AffineTransform.cpp +++ b/tests/yup_graphics/yup_AffineTransform.cpp @@ -289,6 +289,29 @@ TEST (AffineTransformTests, ScaleFactor) EXPECT_FLOAT_EQ (t.getScaleFactor(), 4.0f); } +TEST (AffineTransformTests, ScaleFactorIsIndependentOfRotation) +{ + // Rotating does not resize anything, so the reported scale must not change with + // the angle. Reading only the matrix diagonal would report scale * cos(angle), + // which reaches zero at 90 degrees. + for (const float degrees : { 0.0f, 30.0f, 45.0f, 60.0f, 90.0f, 137.0f, 180.0f, 270.0f }) + { + const auto rotated = AffineTransform::scaling (4.0f).rotated (degreesToRadians (degrees)); + + EXPECT_NEAR (rotated.getScaleFactor(), 4.0f, 1.0e-4f) << "at " << degrees << " degrees"; + } +} + +TEST (AffineTransformTests, ScaleFactorAveragesNonUniformScaling) +{ + const auto t = AffineTransform::scaling (2.0f, 6.0f); + EXPECT_FLOAT_EQ (t.getScaleFactor(), 4.0f); + + // Still the average of the two axis lengths once rotated. + const auto rotated = t.rotated (degreesToRadians (90.0f)); + EXPECT_NEAR (rotated.getScaleFactor(), 4.0f, 1.0e-4f); +} + TEST (AffineTransformTests, MatrixPoints) { AffineTransform t (2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f); diff --git a/tests/yup_graphics/yup_GifImageFormat.cpp b/tests/yup_graphics/yup_GifImageFormat.cpp index 6cc0dbd74..70d67d73a 100644 --- a/tests/yup_graphics/yup_GifImageFormat.cpp +++ b/tests/yup_graphics/yup_GifImageFormat.cpp @@ -23,8 +23,6 @@ #include "yup_ImageFormatTools.h" -#if YUP_MODULE_AVAILABLE_libgif && YUP_IMAGE_FORMAT_GIF - //============================================================================== // Helpers //============================================================================== @@ -1162,5 +1160,3 @@ TEST (GifImageFormatTests, WriteImageReturnsFalseForInvalidImage) Image invalid; EXPECT_FALSE (writer.writeImage (invalid)); } - -#endif // YUP_MODULE_AVAILABLE_libgif && YUP_IMAGE_FORMAT_GIF diff --git a/tests/yup_graphics/yup_GraphicsContext.cpp b/tests/yup_graphics/yup_GraphicsContext.cpp new file mode 100644 index 000000000..c0a46233d --- /dev/null +++ b/tests/yup_graphics/yup_GraphicsContext.cpp @@ -0,0 +1,102 @@ +/* + ============================================================================== + + 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 + +#include + +using namespace yup; + +class GraphicsContextTests : public ::testing::Test +{ +protected: + void SetUp() override + { + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); + ASSERT_NE (context, nullptr); + } + + std::unique_ptr context; +}; + +TEST_F (GraphicsContextTests, IsGpuAvailableReturnsFalseWhenRenderContextIsNull) +{ + // Headless context returns nullptr for getRenderContext, + // so isGpuAvailable should return false. + EXPECT_FALSE (context->isGpuAvailable()); +} + +TEST_F (GraphicsContextTests, GetPlatformReturnsHeadless) +{ + EXPECT_EQ (context->getPlatform(), GpuPlatform::Headless); +} + +TEST_F (GraphicsContextTests, GetFactoryReturnsNonNull) +{ + EXPECT_NE (context->getFactory(), nullptr); +} + +TEST_F (GraphicsContextTests, GetRenderContextReturnsNullForHeadless) +{ + EXPECT_EQ (context->getRenderContext(), nullptr); +} + +TEST_F (GraphicsContextTests, GetRenderTargetReturnsNullForHeadless) +{ + EXPECT_EQ (context->getRenderTarget(), nullptr); +} + +TEST_F (GraphicsContextTests, MakeRendererReturnsNonNull) +{ + auto renderer = context->makeRenderer (200, 200); + EXPECT_NE (renderer, nullptr); +} + +TEST_F (GraphicsContextTests, TickDoesNotCrash) +{ + EXPECT_NO_THROW (context->tick()); +} + +TEST_F (GraphicsContextTests, CreateContextWithNullExistingDeviceSucceeds) +{ + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}, nullptr); + EXPECT_NE (ctx, nullptr); + EXPECT_EQ (ctx->getPlatform(), GpuPlatform::Headless); +} + +TEST_F (GraphicsContextTests, CreateContextWithExistingDeviceSucceeds) +{ + auto existingDevice = GpuDevice::create (GpuPlatform::Headless, {}); + ASSERT_NE (existingDevice, nullptr); + + auto ctx = GraphicsContext::createContext (GpuPlatform::Headless, {}, existingDevice); + EXPECT_NE (ctx, nullptr); + EXPECT_EQ (ctx->getPlatform(), GpuPlatform::Headless); +} + +TEST (GraphicsContextStaticTests, CreateContextReturnsNullForInvalidApi) +{ + // Cast an out-of-range value to GpuPlatform to hit the default case. + // Use a high value that won't match any valid enum member. + const auto invalidApi = static_cast (9999); + auto ctx = GraphicsContext::createContext (invalidApi, {}); + EXPECT_EQ (ctx, nullptr); +} diff --git a/tests/yup_graphics/yup_GraphicsOffscreen.cpp b/tests/yup_graphics/yup_GraphicsOffscreen.cpp index d70870ee5..cf98fb1e4 100644 --- a/tests/yup_graphics/yup_GraphicsOffscreen.cpp +++ b/tests/yup_graphics/yup_GraphicsOffscreen.cpp @@ -28,6 +28,9 @@ using namespace yup; namespace { +//============================================================================== +// A minimal RenderableTarget for testing offscreen Graphics constructors. +//============================================================================== class TrackingOffscreenTarget : public RenderableTarget { public: @@ -53,3 +56,692 @@ class TrackingOffscreenTarget : public RenderableTarget }; } // namespace + +//============================================================================== +// Offscreen Graphics construction tests +//============================================================================== + +class GraphicsOffscreenTests : public ::testing::Test +{ +protected: + void SetUp() override + { + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); + ASSERT_NE (context, nullptr); + } + + std::unique_ptr context; +}; + +TEST_F (GraphicsOffscreenTests, ConstructWithOwnedTargetDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + EXPECT_NO_THROW ({ + Graphics g (*context, std::move (target), 0xFF000000u); + }); +} + +TEST_F (GraphicsOffscreenTests, ConstructWithReferencedTargetDoesNotCrash) +{ + TrackingOffscreenTarget target (128, 64); + EXPECT_NO_THROW ({ + Graphics g (*context, target, 0xFF000000u); + }); +} + +TEST_F (GraphicsOffscreenTests, IsOffscreenReturnsTrueForOffscreenConstructed) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_TRUE (g.isOffscreen()); +} + +TEST_F (GraphicsOffscreenTests, IsOffscreenReturnsFalseForRendererConstructed) +{ + auto renderer = context->makeRenderer (200, 200); + ASSERT_NE (renderer, nullptr); + Graphics g (*context, *renderer); + + EXPECT_FALSE (g.isOffscreen()); +} + +TEST_F (GraphicsOffscreenTests, CommitOffscreenTargetSucceeds) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_TRUE (g.commitOffscreenTarget()); +} + +TEST_F (GraphicsOffscreenTests, CommitOffscreenTargetIsIdempotent) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_TRUE (g.commitOffscreenTarget()); + EXPECT_FALSE (g.commitOffscreenTarget()); // Already committed. +} + +TEST_F (GraphicsOffscreenTests, CommitToImageReturnsFalseWhenNoImageTarget) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + // No associated image, so commitToImage should fail. + EXPECT_FALSE (g.commitToImage()); +} + +TEST_F (GraphicsOffscreenTests, ReadPixelsToImageReturnsFalseWhenNoImageTarget) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_FALSE (g.readPixelsToImage()); +} + +TEST_F (GraphicsOffscreenTests, DrawingAreaDefaultsToTargetSizeForOffscreen) +{ + TrackingOffscreenTarget target (128, 64); + Graphics g (*context, target, 0xFF000000u); + + auto area = g.getDrawingArea(); + EXPECT_FLOAT_EQ (area.getWidth(), 128.0f); + EXPECT_FLOAT_EQ (area.getHeight(), 64.0f); +} + +//============================================================================== +// Offscreen drawing operations +//============================================================================== + +TEST_F (GraphicsOffscreenTests, FillAllOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_NO_THROW (g.fillAll()); +} + +TEST_F (GraphicsOffscreenTests, FillRectOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_NO_THROW ({ + g.setFillColor (Color (0xFFFF0000)); + g.fillRect (10.0f, 10.0f, 50.0f, 30.0f); + }); +} + +TEST_F (GraphicsOffscreenTests, StrokeRectOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_NO_THROW ({ + g.setStrokeColor (Color (0xFF00FF00)); + g.setStrokeWidth (2.0f); + g.strokeRect (5.0f, 5.0f, 100.0f, 50.0f); + }); +} + +TEST_F (GraphicsOffscreenTests, PathDrawingOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + Path path; + path.moveTo (10.0f, 10.0f); + path.lineTo (50.0f, 10.0f); + path.lineTo (30.0f, 50.0f); + path.close(); + + EXPECT_NO_THROW ({ + g.setFillColor (Color (0xFF0000FF)); + g.fillPath (path); + g.setStrokeColor (Color (0xFFFF0000)); + g.strokePath (path); + }); +} + +TEST_F (GraphicsOffscreenTests, EllipseDrawingOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_NO_THROW ({ + g.setFillColor (Color (0xFFFF00FF)); + g.fillEllipse (20.0f, 10.0f, 60.0f, 40.0f); + g.strokeEllipse (Rectangle (20.0f, 10.0f, 60.0f, 40.0f)); + }); +} + +TEST_F (GraphicsOffscreenTests, RoundedRectDrawingOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_NO_THROW ({ + g.fillRoundedRect (5.0f, 5.0f, 100.0f, 50.0f, 3.0f, 5.0f, 7.0f, 9.0f); + g.strokeRoundedRect (5.0f, 5.0f, 100.0f, 50.0f, 4.0f); + g.fillRoundedRect (Rectangle (10.0f, 10.0f, 80.0f, 40.0f), 6.0f); + g.strokeRoundedRect (Rectangle (10.0f, 10.0f, 80.0f, 40.0f), 2.0f, 3.0f, 4.0f, 5.0f); + }); +} + +TEST_F (GraphicsOffscreenTests, LineDrawingOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_NO_THROW ({ + g.setStrokeColor (Color (0xFFFFFFFF)); + g.strokeLine (0.0f, 0.0f, 127.0f, 63.0f); + g.strokeLine (Point (10.0f, 10.0f), Point (100.0f, 50.0f)); + }); +} + +TEST_F (GraphicsOffscreenTests, SaveRestoreStateOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + g.setFillColor (Color (0xFFFF0000)); + { + auto state = g.saveState(); + g.setFillColor (Color (0xFF00FF00)); + EXPECT_EQ (g.getFillColor(), Color (0xFF00FF00)); + } + EXPECT_EQ (g.getFillColor(), Color (0xFFFF0000)); +} + +TEST_F (GraphicsOffscreenTests, ClipPathOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_NO_THROW ({ + g.setClipPath (Rectangle (10.0f, 10.0f, 50.0f, 40.0f)); + }); + + Path clipPath; + clipPath.addEllipse (20.0f, 10.0f, 80.0f, 40.0f); + EXPECT_NO_THROW ({ + g.setClipPath (clipPath); + }); +} + +TEST_F (GraphicsOffscreenTests, FittedTextOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + StyledText styledText; + { + auto modifier = styledText.startUpdate(); + modifier.setMaxSize (Size (120.0f, 60.0f)); + modifier.appendText ("Test", Font()); + } + + Rectangle textRect (4.0f, 4.0f, 120.0f, 56.0f); + + EXPECT_NO_THROW ({ + g.fillFittedText (styledText, textRect); + g.strokeFittedText (styledText, textRect); + }); +} + +TEST_F (GraphicsOffscreenTests, FittedTextConvenienceOverloadsDoNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + Rectangle textRect (4.0f, 4.0f, 120.0f, 56.0f); + + EXPECT_NO_THROW ({ + g.fillFittedText ("Hello world", Font().withHeight (14.0f), textRect, Justification::center); + g.strokeFittedText ("Hello world", Font().withHeight (14.0f), textRect, Justification::topLeft); + g.fillFittedText ("Hello world", Font().withHeight (14.0f), textRect, Justification::bottomRight); + g.strokeFittedText ("Hello world", Font().withHeight (14.0f), textRect, Justification::right); + }); +} + +TEST_F (GraphicsOffscreenTests, EmptyFittedTextReturnsEarly) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + Rectangle textRect (4.0f, 4.0f, 120.0f, 56.0f); + + EXPECT_NO_THROW ({ + g.fillFittedText ("", Font(), textRect); + g.strokeFittedText ("", Font(), textRect); + }); +} + +TEST_F (GraphicsOffscreenTests, ImageDrawingOnOffscreenReturnsEarly) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + Image testImage (32, 32, PixelFormat::RGBA); + testImage.fill (0xFFFF0000u); + + // drawImage / drawImageAt should not crash (though texture creation fails on headless). + EXPECT_NO_THROW ({ + g.drawImage (testImage, Rectangle (0.0f, 0.0f, 32.0f, 32.0f)); + g.drawImageAt (testImage, Point (10.0f, 10.0f)); + }); +} + +TEST_F (GraphicsOffscreenTests, DrawTextureWithNullDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + EXPECT_NO_THROW ({ + g.drawTexture (nullptr, Rectangle (0.0f, 0.0f, 32.0f, 32.0f)); + }); +} + +TEST_F (GraphicsOffscreenTests, GradientFillOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + ColorGradient linearGrad ( + Color (0xFFFF0000), 0.0f, 0.0f, Color (0xFF0000FF), 100.0f, 100.0f, ColorGradient::Linear); + + ColorGradient radialGrad ( + Color (0xFF00FF00), 50.0f, 50.0f, Color (0xFFFFFF00), 0.0f, 0.0f, ColorGradient::Radial); + + EXPECT_NO_THROW ({ + g.setFillColorGradient (linearGrad); + g.fillRect (10.0f, 10.0f, 50.0f, 30.0f); + + g.setStrokeColorGradient (radialGrad); + g.setStrokeWidth (2.0f); + g.strokeRect (70.0f, 10.0f, 50.0f, 30.0f); + }); +} + +TEST_F (GraphicsOffscreenTests, SingleStopGradientDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + ColorGradient singleStop ( + Color (0xFFFF0000), 0.0f, 0.0f, Color (0xFFFF0000), 0.0f, 0.0f, ColorGradient::Linear); + // Add only one stop to force the single-stop path. + singleStop.clearStops(); + singleStop.addColorStop (Color (0xFF00FF00), 0.0f); + + EXPECT_NO_THROW ({ + g.setFillColorGradient (singleStop); + g.fillRect (10.0f, 10.0f, 50.0f, 30.0f); + }); +} + +TEST_F (GraphicsOffscreenTests, EmptyGradientDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + ColorGradient emptyGrad ( + Color (0xFFFF0000), 0.0f, 0.0f, Color (0xFFFF0000), 0.0f, 0.0f, ColorGradient::Linear); + emptyGrad.clearStops(); + + EXPECT_NO_THROW ({ + g.setFillColorGradient (emptyGrad); + g.fillRect (10.0f, 10.0f, 50.0f, 30.0f); + }); +} + +//============================================================================== +// TransparencyLayer tests +//============================================================================== + +class TransparencyLayerTests : public ::testing::Test +{ +protected: + void SetUp() override + { + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); + ASSERT_NE (context, nullptr); + renderer = context->makeRenderer (200, 200); + ASSERT_NE (renderer, nullptr); + graphics = std::make_unique (*context, *renderer); + } + + std::unique_ptr context; + std::unique_ptr renderer; + std::unique_ptr graphics; +}; + +TEST_F (TransparencyLayerTests, BeginTransparencyLayerWithTinyAreaReturnsInvalidLayer) +{ + // Width or height of 0 produces an invalid layer. + auto layer = graphics->beginTransparencyLayer (Rectangle (0.0f, 0.0f, 0.0f, 10.0f), 0.5f); + EXPECT_FALSE (layer.isValid()); +} + +TEST_F (TransparencyLayerTests, BeginTransparencyLayerWithNegativeAreaReturnsInvalidLayer) +{ + auto layer = graphics->beginTransparencyLayer (Rectangle (0.0f, 0.0f, -10.0f, 10.0f), 0.5f); + EXPECT_FALSE (layer.isValid()); +} + +TEST_F (TransparencyLayerTests, InvalidLayerCommitReturnsFalse) +{ + auto layer = graphics->beginTransparencyLayer (Rectangle (0.0f, 0.0f, 0.0f, 10.0f), 0.5f); + EXPECT_FALSE (layer.isValid()); + EXPECT_FALSE (layer.commit()); +} + +TEST_F (TransparencyLayerTests, BeginTransparencyLayerWithValidAreaDoesNotCrash) +{ + EXPECT_NO_THROW ({ + auto layer = graphics->beginTransparencyLayer (Rectangle (0.0f, 0.0f, 100.0f, 100.0f), 0.5f); + }); +} + +TEST_F (TransparencyLayerTests, MoveConstructedLayer) +{ + auto layer1 = graphics->beginTransparencyLayer (Rectangle (0.0f, 0.0f, 100.0f, 100.0f), 0.5f); + Graphics::TransparencyLayer layer2 (std::move (layer1)); + + // layer1 should be committed/finished after move. + EXPECT_FALSE (layer1.isValid()); + + // layer2 should be valid or invalid depending on headless support. + EXPECT_NO_THROW (layer2.commit()); +} + +TEST_F (TransparencyLayerTests, MoveAssignedLayer) +{ + auto layer1 = graphics->beginTransparencyLayer (Rectangle (0.0f, 0.0f, 100.0f, 100.0f), 0.5f); + auto layer2 = graphics->beginTransparencyLayer (Rectangle (0.0f, 0.0f, 100.0f, 100.0f), 0.3f); + + layer2 = std::move (layer1); + + EXPECT_FALSE (layer1.isValid()); + EXPECT_NO_THROW (layer2.commit()); +} + +//============================================================================== +// Blend mode coverage +//============================================================================== + +TEST_F (TransparencyLayerTests, AllBlendModesSetWithoutCrash) +{ + std::array blendModes = { + BlendMode::SrcOver, + BlendMode::Screen, + BlendMode::Multiply, + BlendMode::Overlay, + BlendMode::Darken, + BlendMode::Lighten, + BlendMode::ColorDodge, + BlendMode::ColorBurn, + BlendMode::HardLight, + BlendMode::SoftLight, + BlendMode::Difference, + BlendMode::Exclusion, + BlendMode::Hue, + BlendMode::Saturation, + BlendMode::Color, + BlendMode::Luminosity + }; + + for (const auto& mode : blendModes) + { + EXPECT_NO_THROW (graphics->setBlendMode (mode)); + EXPECT_EQ (graphics->getBlendMode(), mode); + } +} + +TEST_F (TransparencyLayerTests, BlendModeDefaultCoverage) +{ + // Verify that each blend mode round-trips without triggering the default case. + graphics->setBlendMode (BlendMode::SrcOver); + EXPECT_EQ (graphics->getBlendMode(), BlendMode::SrcOver); + + graphics->setBlendMode (BlendMode::Color); + EXPECT_EQ (graphics->getBlendMode(), BlendMode::Color); + + graphics->setBlendMode (BlendMode::Luminosity); + EXPECT_EQ (graphics->getBlendMode(), BlendMode::Luminosity); +} + +//============================================================================== +// Justification conversion (toHorizontalAlign / toVerticalAlign) +//============================================================================== + +TEST_F (TransparencyLayerTests, JustificationConversionsDoNotCrash) +{ + Rectangle textRect (4.0f, 4.0f, 120.0f, 56.0f); + + EXPECT_NO_THROW ({ + graphics->fillFittedText ("left", Font().withHeight (14.0f), textRect, Justification::left); + graphics->fillFittedText ("right", Font().withHeight (14.0f), textRect, Justification::right); + graphics->fillFittedText ("hCenter", Font().withHeight (14.0f), textRect, Justification::horizontalCenter); + graphics->fillFittedText ("top", Font().withHeight (14.0f), textRect, Justification::top); + graphics->fillFittedText ("bottom", Font().withHeight (14.0f), textRect, Justification::bottom); + graphics->fillFittedText ("vCenter", Font().withHeight (14.0f), textRect, Justification::verticalCenter); + graphics->fillFittedText ("centered", Font().withHeight (14.0f), textRect, Justification::center); + }); +} + +//============================================================================== +// Graphics renderTexture / drawTexture coverage +//============================================================================== + +class GraphicsTextureRenderingTests : public ::testing::Test +{ +protected: + void SetUp() override + { + context = GraphicsContext::createContext (GpuPlatform::Headless, {}); + ASSERT_NE (context, nullptr); + renderer = context->makeRenderer (200, 200); + ASSERT_NE (renderer, nullptr); + graphics = std::make_unique (*context, *renderer); + } + + std::unique_ptr context; + std::unique_ptr renderer; + std::unique_ptr graphics; +}; + +TEST_F (GraphicsTextureRenderingTests, DrawImageWithValidImageDoesNotCrash) +{ + // drawImage calls createTextureIfNotPresent which returns false on headless, + // but should not crash. + Image testImage (32, 32, PixelFormat::RGBA); + testImage.fill (0xFF112233u); + + EXPECT_NO_THROW ({ + graphics->drawImage (testImage, Rectangle (10.0f, 10.0f, 32.0f, 32.0f)); + graphics->drawImageAt (testImage, Point (5.0f, 5.0f)); + }); +} + +TEST_F (GraphicsTextureRenderingTests, DrawImageWithInvalidImageDoesNotCrash) +{ + Image invalid; + EXPECT_NO_THROW ({ + graphics->drawImage (invalid, Rectangle (10.0f, 10.0f, 32.0f, 32.0f)); + }); +} + +TEST_F (GraphicsTextureRenderingTests, DrawTextureWithNullDoesNotCrash) +{ + EXPECT_NO_THROW ({ + graphics->drawTexture (nullptr, Rectangle (0.0f, 0.0f, 64.0f, 64.0f)); + }); +} + +TEST_F (GraphicsTextureRenderingTests, DrawTextureOnOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + Graphics g (*context, std::move (target), 0xFF000000u); + + Image img (16, 16, PixelFormat::RGBA); + img.fill (0xFFAABBCCu); + + EXPECT_NO_THROW ({ + g.drawImage (img, Rectangle (0.0f, 0.0f, 16.0f, 16.0f)); + g.drawTexture (nullptr, Rectangle (0.0f, 0.0f, 16.0f, 16.0f)); + }); +} + +//============================================================================== +// StrokeJoin / StrokeCap all values +//============================================================================== + +TEST_F (TransparencyLayerTests, AllStrokeJoinsRoundTrip) +{ + std::array joins = { StrokeJoin::Miter, StrokeJoin::Round, StrokeJoin::Bevel }; + + for (const auto& join : joins) + { + EXPECT_NO_THROW (graphics->setStrokeJoin (join)); + EXPECT_EQ (graphics->getStrokeJoin(), join); + } +} + +TEST_F (TransparencyLayerTests, AllStrokeCapsRoundTrip) +{ + std::array caps = { StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square }; + + for (const auto& cap : caps) + { + EXPECT_NO_THROW (graphics->setStrokeCap (cap)); + EXPECT_EQ (graphics->getStrokeCap(), cap); + } +} + +//============================================================================== +// Graphics::convertRawPathToRenderPath variants +//============================================================================== + +namespace +{ + +// Internal functions declared in yup_Graphics but accessible via the module header. +// Exercise the non-identity transform path by using an actual transform. + +} // namespace + +TEST_F (TransparencyLayerTests, FillAndStrokePathWithNonIdentityTransform) +{ + graphics->setDrawingArea (Rectangle (0.0f, 0.0f, 200.0f, 200.0f)); + graphics->setTransform (AffineTransform::translation (50.0f, 30.0f).scaled (2.0f, 2.0f)); + + Path path; + path.addRectangle (0.0f, 0.0f, 50.0f, 50.0f); + + EXPECT_NO_THROW ({ + graphics->fillPath (path); + graphics->strokePath (path); + }); +} + +TEST_F (TransparencyLayerTests, FillPathWithGradientAndNonIdentityTransform) +{ + graphics->setDrawingArea (Rectangle (0.0f, 0.0f, 200.0f, 200.0f)); + + ColorGradient grad ( + Color (0xFFFF0000), 0.0f, 0.0f, Color (0xFF00FF00), 50.0f, 50.0f, ColorGradient::Radial); + graphics->setFillColorGradient (grad); + graphics->setTransform (AffineTransform::rotation (MathConstants::halfPi / 2.0f, 0.0f, 0.0f)); + + Path path; + path.addEllipse (0.0f, 0.0f, 100.0f, 100.0f); + + EXPECT_NO_THROW ({ + graphics->fillPath (path); + }); +} + +//============================================================================== +// Graphics destructor with committed offscreen — no double endOffscreen +//============================================================================== + +TEST_F (GraphicsOffscreenTests, DestructorWithCommittedOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + { + Graphics g (*context, std::move (target), 0xFF000000u); + EXPECT_TRUE (g.commitOffscreenTarget()); + } + // Destructor should not call endOffscreen since committed is true. +} + +TEST_F (GraphicsOffscreenTests, DestructorWithUncommittedOffscreenDoesNotCrash) +{ + auto target = std::make_unique (128, 64); + { + Graphics g (*context, std::move (target), 0xFF000000u); + // Leave uncommitted — destructor calls endOffscreen. + } +} + +//============================================================================== +// Graphics with nullptr offscreenTarget in owned-target constructor +//============================================================================== + +TEST_F (GraphicsOffscreenTests, ConstructWithNullOwnedTargetDoesNotCrash) +{ + EXPECT_NO_THROW ({ + Graphics g (*context, std::unique_ptr (nullptr), 0xFF000000u); + EXPECT_FALSE (g.isOffscreen()); + }); +} + +//============================================================================== +// Graphics complex text rendering (fitted text with gradient) +//============================================================================== + +TEST_F (GraphicsOffscreenTests, FillFittedTextWithGradientDoesNotCrash) +{ + auto target = std::make_unique (200, 100); + Graphics g (*context, std::move (target), 0xFF000000u); + + ColorGradient textGrad ( + Color (0xFFFF0000), 0.0f, 0.0f, Color (0xFF0000FF), 150.0f, 0.0f, ColorGradient::Linear); + g.setFillColorGradient (textGrad); + + StyledText styled; + { + auto mod = styled.startUpdate(); + mod.setMaxSize (Size (180.0f, 80.0f)); + mod.appendText ("Gradient Text", Font().withHeight (20.0f)); + } + + EXPECT_NO_THROW ({ + g.fillFittedText (styled, Rectangle (10.0f, 10.0f, 180.0f, 80.0f)); + }); +} + +TEST_F (GraphicsOffscreenTests, StrokeFittedTextWithGradientDoesNotCrash) +{ + auto target = std::make_unique (200, 100); + Graphics g (*context, std::move (target), 0xFF000000u); + + ColorGradient textGrad ( + Color (0xFF00FF00), 0.0f, 0.0f, Color (0xFFFF0000), 100.0f, 50.0f, ColorGradient::Radial); + g.setStrokeColorGradient (textGrad); + g.setStrokeWidth (2.0f); + + StyledText styled; + { + auto mod = styled.startUpdate(); + mod.setMaxSize (Size (180.0f, 80.0f)); + mod.appendText ("Stroke Grad", Font().withHeight (18.0f)); + } + + EXPECT_NO_THROW ({ + g.strokeFittedText (styled, Rectangle (10.0f, 10.0f, 180.0f, 80.0f)); + }); +} diff --git a/tests/yup_graphics/yup_Image.cpp b/tests/yup_graphics/yup_Image.cpp index 9ba1e3939..106062adc 100644 --- a/tests/yup_graphics/yup_Image.cpp +++ b/tests/yup_graphics/yup_Image.cpp @@ -88,13 +88,12 @@ TEST (ImageTests, GrayscaleBitmapConvertsToOpaqueRGBATextureBytes) raw[1] = 127; raw[2] = 255; - uint8 textureBytes[12] = {}; + uint32_t textureBytes[3] = {}; ColorVectorOperations::convertGrayscaleToRGBA (raw.data(), textureBytes, 3); - const uint8 expected[] = { 0, 0, 0, 255, 127, 127, 127, 255, 255, 255, 255, 255 }; - - for (size_t i = 0; i < std::size (expected); ++i) - EXPECT_EQ (textureBytes[i], expected[i]); + EXPECT_EQ (textureBytes[0], 0xff000000u); + EXPECT_EQ (textureBytes[1], 0xff7f7f7fu); + EXPECT_EQ (textureBytes[2], 0xffffffffu); } TEST (ImageTests, RgbBitmapConvertsToOpaqueRGBATextureBytes) @@ -105,13 +104,11 @@ TEST (ImageTests, RgbBitmapConvertsToOpaqueRGBATextureBytes) image.setPixel (1, 0, 0xffabcdef); const auto raw = image.getRawData(); - uint8 textureBytes[8] = {}; + uint32_t textureBytes[2] = {}; ColorVectorOperations::convertRGBToRGBA (raw.data(), textureBytes, 2); - const uint8 expected[] = { 0x12, 0x34, 0x56, 0xff, 0xab, 0xcd, 0xef, 0xff }; - - for (size_t i = 0; i < std::size (expected); ++i) - EXPECT_EQ (textureBytes[i], expected[i]); + EXPECT_EQ (textureBytes[0], 0xff563412u); + EXPECT_EQ (textureBytes[1], 0xffefcdabu); } TEST (ImageTests, RgbaBitmapConvertsToPremultipliedRGBATextureBytes) @@ -122,14 +119,12 @@ TEST (ImageTests, RgbaBitmapConvertsToPremultipliedRGBATextureBytes) image.setPixel (1, 0, 0xff010203); const auto raw = image.getRawData(); - uint8 textureBytes[8] = {}; + uint32_t textureBytes[2] = {}; std::memcpy (textureBytes, raw.data(), raw.size()); ColorVectorOperations::premultiplyRGBA (textureBytes, 2); - const uint8 expected[] = { 8, 16, 32, 128, 1, 2, 3, 255 }; - - for (size_t i = 0; i < std::size (expected); ++i) - EXPECT_EQ (textureBytes[i], expected[i]); + EXPECT_EQ (textureBytes[0], 0x80201008u); + EXPECT_EQ (textureBytes[1], 0xff030201u); } TEST (ImageTests, ColorCanConvertToExplicitPackedByteOrders) diff --git a/tests/yup_graphics/yup_ImageFormatMetadataExtended.cpp b/tests/yup_graphics/yup_ImageFormatMetadataExtended.cpp new file mode 100644 index 000000000..744bf96e3 --- /dev/null +++ b/tests/yup_graphics/yup_ImageFormatMetadataExtended.cpp @@ -0,0 +1,309 @@ +/* + ============================================================================== + + 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 + +#include + +#include "yup_ImageFormatTools.h" + +using namespace yup; + +namespace +{ + +// Helper: load image from memory block with options +static Image loadFromBlock (const MemoryBlock& block, const ImageFormat::Options& opts) +{ + auto data = block.asBytes(); + auto result = Image::loadFromData (data, opts); + if (! result.wasOk()) + return {}; + return std::move (result).getValue(); +} + +} // namespace + +//============================================================================== +// JPEG extended metadata tests +//============================================================================== + +#if YUP_MODULE_AVAILABLE_libjpeg && YUP_IMAGE_FORMAT_JPEG + +TEST (JpegMetadataExtendedTest, DpiInDotsPerCmSurvivesRoundTrip) +{ + // When DPI is stored in dots/cm in the JPEG, it must be converted to dots/inch. + // Write a JPEG with DPI that triggers density_unit == 2 (dots/cm). + Image img = generateSolidImage (16, 16, PixelFormat::RGB, 0xFF336699u); + + auto meta = ImageMetadata::create(); + // 150 DPI = ~59 dots/cm. When written and read back, the reader + // converts from dots/cm to DPI by multiplying by 2.54. + meta->dpiX = 150.0; + meta->dpiY = 150.0; + img.setMetadata (meta); + + auto block = writeImageToBlock (img, 0); + + ImageFormat::Options opts = ImageFormat::Options().withMetadata (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + // DPI should survive the round-trip (JPEG uses dots/inch internally) + EXPECT_NEAR (150.0, reloaded.getMetadata()->dpiX, 1.0); + EXPECT_NEAR (150.0, reloaded.getMetadata()->dpiY, 1.0); +} + +TEST (JpegMetadataExtendedTest, XmpRawChunkSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGB, 0xFF336699u); + + // Build a minimal XMP-like blob for the "jpeg/xmp" chunk. + const char xmpData[] = "http://ns.adobe.com/xap/1.0/\x00setRawChunk ("jpeg/xmp", MemoryBlock (xmpData, sizeof (xmpData))); + img.setMetadata (meta); + + auto block = writeImageToBlock (img, 0); + + ImageFormat::Options opts = ImageFormat::Options().withRawChunks (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + + auto* xmpChunk = reloaded.getMetadata()->getRawChunk ("jpeg/xmp"); + ASSERT_NE (nullptr, xmpChunk); + EXPECT_EQ (sizeof (xmpData), xmpChunk->getSize()); +} + +TEST (JpegMetadataExtendedTest, JfifRawChunkSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGB, 0xFF336699u); + + // A minimal JFIF APP0 marker body (without the "JFIF\0" header). + const uint8 jfifBody[] = { 0x01, 0x02, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0x00 }; + + auto meta = ImageMetadata::create(); + meta->setRawChunk ("jpeg/jfif", MemoryBlock (jfifBody, sizeof (jfifBody))); + img.setMetadata (meta); + + auto block = writeImageToBlock (img, 0); + + ImageFormat::Options opts = ImageFormat::Options().withRawChunks (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + + auto* jfifChunk = reloaded.getMetadata()->getRawChunk ("jpeg/jfif"); + ASSERT_NE (nullptr, jfifChunk); + EXPECT_EQ (sizeof (jfifBody), jfifChunk->getSize()); +} + +TEST (JpegMetadataExtendedTest, CommentTextSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGB, 0xFF336699u); + + auto meta = ImageMetadata::create(); + meta->textEntries.set ("Comment", "JPEG COM marker round-trip test."); + img.setMetadata (meta); + + auto block = writeImageToBlock (img, 0); + + ImageFormat::Options opts = ImageFormat::Options().withMetadata (true).withRawChunks (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + EXPECT_EQ ("JPEG COM marker round-trip test.", reloaded.getMetadata()->textEntries.getValue ("Comment", {})); +} + +TEST (JpegMetadataExtendedTest, IccProfileRawChunkSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGB, 0xFF336699u); + + const uint8 iccData[] = { 'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', 0x00, 0x01, 0x02, 0x03, 0x04 }; + + auto meta = ImageMetadata::create(); + meta->setRawChunk ("jpeg/icc", MemoryBlock (iccData, sizeof (iccData))); + img.setMetadata (meta); + + auto block = writeImageToBlock (img, 0); + + ImageFormat::Options opts = ImageFormat::Options().withRawChunks (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + + auto* iccChunk = reloaded.getMetadata()->getRawChunk ("jpeg/icc"); + ASSERT_NE (nullptr, iccChunk); + EXPECT_EQ (sizeof (iccData), iccChunk->getSize()); +} + +TEST (JpegMetadataExtendedTest, LoadInvalidDataReturnsEmptyImage) +{ + // Completely random bytes should fail to decode. + const uint8 garbage[] = { 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + auto result = Image::loadFromData (Span (garbage, sizeof (garbage))); + + // Should gracefully return a failure, not crash. + EXPECT_FALSE (result.wasOk()); +} + +TEST (JpegMetadataExtendedTest, GrayScaleRoundTrip) +{ + Image img (8, 8, PixelFormat::Grayscale); + for (int y = 0; y < 8; ++y) + for (int x = 0; x < 8; ++x) + img.setPixelColor (x, y, Color (0xFF, (uint8) (x * 32), (uint8) (x * 32), (uint8) (x * 32))); + + auto block = writeImageToBlock (img, 0); + + auto data = block.asBytes(); + auto result = Image::loadFromData (data); + ASSERT_TRUE (result.wasOk()); + + auto reloaded = std::move (result).getValue(); + ASSERT_TRUE (reloaded.isValid()); + EXPECT_EQ (reloaded.getWidth(), 8); + EXPECT_EQ (reloaded.getHeight(), 8); +} + +#endif // YUP_IMAGE_FORMAT_JPEG + +//============================================================================== +// PNG extended metadata tests +//============================================================================== + +#if YUP_MODULE_AVAILABLE_libpng && YUP_IMAGE_FORMAT_PNG + +TEST (PngMetadataExtendedTest, TimeChunkSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGBA, 0x8044AA88u); + + auto meta = ImageMetadata::create(); + meta->textEntries.set ("png/time", "2024:06:15 14:30:00"); + img.setMetadata (meta); + + auto block = writeImageToBlock (img); + + ImageFormat::Options opts = ImageFormat::Options().withMetadata (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + EXPECT_EQ ("2024:06:15 14:30:00", reloaded.getMetadata()->textEntries.getValue ("png/time", {})); +} + +TEST (PngMetadataExtendedTest, SRgbChunkSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGBA, 0x8044AA88u); + + auto meta = ImageMetadata::create(); + meta->textEntries.set ("png/sRGB", "0"); + img.setMetadata (meta); + + auto block = writeImageToBlock (img); + + ImageFormat::Options opts = ImageFormat::Options().withMetadata (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + EXPECT_EQ ("0", reloaded.getMetadata()->textEntries.getValue ("png/sRGB", {})); +} + +TEST (PngMetadataExtendedTest, GammaChunkSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGBA, 0x8044AA88u); + + auto meta = ImageMetadata::create(); + meta->textEntries.set ("png/gamma", "2.2"); + img.setMetadata (meta); + + auto block = writeImageToBlock (img); + + ImageFormat::Options opts = ImageFormat::Options().withMetadata (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + EXPECT_EQ ("2.2", reloaded.getMetadata()->textEntries.getValue ("png/gamma", {})); +} + +TEST (PngMetadataExtendedTest, IccpRawChunkSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGBA, 0x8044AA88u); + + const uint8 iccpData[] = { 0x49, 0x43, 0x43, 0x50, 0x00, 0x01, 0x02, 0x03 }; + + auto meta = ImageMetadata::create(); + meta->setRawChunk ("png/iCCP", MemoryBlock (iccpData, sizeof (iccpData))); + img.setMetadata (meta); + + auto block = writeImageToBlock (img); + + ImageFormat::Options opts = ImageFormat::Options().withRawChunks (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + + auto* iccpChunk = reloaded.getMetadata()->getRawChunk ("png/iCCP"); + ASSERT_NE (nullptr, iccpChunk); + EXPECT_EQ (sizeof (iccpData), iccpChunk->getSize()); +} + +TEST (PngMetadataExtendedTest, ChrmRawChunkSurvivesRoundTrip) +{ + Image img = generateSolidImage (16, 16, PixelFormat::RGBA, 0x8044AA88u); + + const uint8 chrmData[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F }; + + auto meta = ImageMetadata::create(); + meta->setRawChunk ("png/cHRM", MemoryBlock (chrmData, sizeof (chrmData))); + img.setMetadata (meta); + + auto block = writeImageToBlock (img); + + ImageFormat::Options opts = ImageFormat::Options().withRawChunks (true); + auto reloaded = loadFromBlock (block, opts); + + ASSERT_TRUE (reloaded.isValid()); + ASSERT_TRUE (reloaded.hasMetadata()); + + auto* chrmChunk = reloaded.getMetadata()->getRawChunk ("png/cHRM"); + ASSERT_NE (nullptr, chrmChunk); + EXPECT_EQ (sizeof (chrmData), chrmChunk->getSize()); +} + +TEST (PngMetadataExtendedTest, LoadInvalidDataReturnsEmptyImage) +{ + const uint8 garbage[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + auto result = Image::loadFromData (Span (garbage, sizeof (garbage))); + EXPECT_FALSE (result.wasOk()); +} + +#endif // YUP_IMAGE_FORMAT_PNG diff --git a/tests/yup_graphics/yup_JpegImageFormat.cpp b/tests/yup_graphics/yup_JpegImageFormat.cpp index 63059840a..342ac9c14 100644 --- a/tests/yup_graphics/yup_JpegImageFormat.cpp +++ b/tests/yup_graphics/yup_JpegImageFormat.cpp @@ -23,8 +23,6 @@ #include "yup_ImageFormatTools.h" -#if YUP_MODULE_AVAILABLE_libjpeg && YUP_IMAGE_FORMAT_JPEG - // ====================================================================== // Reader dimension and header tests // ====================================================================== @@ -756,5 +754,3 @@ TEST (JpegImageFormatTests, MetadataExtractsExifOrientation) ASSERT_NE (reader.metadata, nullptr); EXPECT_EQ (reader.metadata->getOrientation(), 6); } - -#endif // YUP_MODULE_AVAILABLE_libjpeg && YUP_IMAGE_FORMAT_JPEG diff --git a/tests/yup_graphics/yup_PngImageFormat.cpp b/tests/yup_graphics/yup_PngImageFormat.cpp index b6daeb6a8..b1bad33b0 100644 --- a/tests/yup_graphics/yup_PngImageFormat.cpp +++ b/tests/yup_graphics/yup_PngImageFormat.cpp @@ -23,8 +23,6 @@ #include "yup_ImageFormatTools.h" -#if YUP_MODULE_AVAILABLE_libpng && YUP_IMAGE_FORMAT_PNG - // ====================================================================== // Reader dimension and header tests // ====================================================================== @@ -766,5 +764,3 @@ TEST (PngImageFormatTests, ParseMetadataExtractsTextChunks) EXPECT_EQ (reader.metadata->textEntries.getValue ("Title", {}), String ("Test PNG")); EXPECT_EQ (reader.metadata->textEntries.getValue ("Author", {}), String ("YUP")); } - -#endif // YUP_MODULE_AVAILABLE_libpng && YUP_IMAGE_FORMAT_PNG diff --git a/tests/yup_graphics/yup_TiffImageFormat.cpp b/tests/yup_graphics/yup_TiffImageFormat.cpp index 150a416f1..aece94045 100644 --- a/tests/yup_graphics/yup_TiffImageFormat.cpp +++ b/tests/yup_graphics/yup_TiffImageFormat.cpp @@ -23,8 +23,6 @@ #include "yup_ImageFormatTools.h" -#if YUP_MODULE_AVAILABLE_libtiff && YUP_IMAGE_FORMAT_TIFF - using namespace yup; // ====================================================================== @@ -871,5 +869,3 @@ TEST (TiffImageFormatTests, ParseRawChunksExtractsExifWhenPresent) ASSERT_NE (reader.metadata, nullptr); EXPECT_FALSE (reader.metadata->hasRawChunk ("tiff/exif")); } - -#endif // YUP_MODULE_AVAILABLE_libtiff && YUP_IMAGE_FORMAT_TIFF diff --git a/tests/yup_graphics/yup_WebPImageFormat.cpp b/tests/yup_graphics/yup_WebPImageFormat.cpp index fa073b8e8..c3aa5fb0e 100644 --- a/tests/yup_graphics/yup_WebPImageFormat.cpp +++ b/tests/yup_graphics/yup_WebPImageFormat.cpp @@ -23,8 +23,6 @@ #include "yup_ImageFormatTools.h" -#if YUP_MODULE_AVAILABLE_libwebp && YUP_IMAGE_FORMAT_WEBP - // ====================================================================== // Reader dimension and header tests // ====================================================================== @@ -852,5 +850,3 @@ TEST (WebPImageFormatTests, ParseRawChunksCreatesMetadata) EXPECT_DOUBLE_EQ (reader.metadata->dpiX, 0.0); EXPECT_DOUBLE_EQ (reader.metadata->dpiY, 0.0); } - -#endif // YUP_MODULE_AVAILABLE_libwebp && YUP_IMAGE_FORMAT_WEBP diff --git a/tests/yup_gui.cpp b/tests/yup_gui.cpp index 9d3c7a6e1..43fb36ada 100644 --- a/tests/yup_gui.cpp +++ b/tests/yup_gui.cpp @@ -26,6 +26,7 @@ #include "yup_gui/yup_Artboard.cpp" #include "yup_gui/yup_ComboBox.cpp" #include "yup_gui/yup_Component.cpp" +#include "yup_gui/yup_ComponentNative.cpp" #include "yup_gui/yup_ComponentEffect.cpp" #include "yup_gui/yup_Desktop.cpp" #include "yup_gui/yup_DragAndDropData.cpp" diff --git a/tests/yup_gui/yup_ComponentEffect.cpp b/tests/yup_gui/yup_ComponentEffect.cpp index 1ecf0e250..a5ed03510 100644 --- a/tests/yup_gui/yup_ComponentEffect.cpp +++ b/tests/yup_gui/yup_ComponentEffect.cpp @@ -91,6 +91,11 @@ class ComponentTestHelper comp.cachedTextureCanvas = canvas; } + static GpuCanvas::Ptr getEffectOffscreenCanvas (const Component& comp) + { + return comp.effectOffscreenCanvas; + } + static void triggerPaint (Component& comp, Graphics& g, const Rectangle& repaintArea, bool renderContinuous = false) { comp.internalPaint (g, repaintArea, renderContinuous); @@ -811,15 +816,53 @@ TEST_F (ComponentEffectGpuTest, EffectPlusCacheRendersEffectEveryFrame) auto firstCanvas = ComponentHelper::getCachedTextureCanvas (*comp); ASSERT_NE (firstCanvas, nullptr); - // Effect path always re-renders the full subtree (so cached canvas - // is recreated), because a child repaint doesn't invalidate the - // parent's cache. Effect must track live child content. + // The effect path always re-renders the full subtree, because a child repaint + // doesn't invalidate the parent's cache and the effect must track live child + // content. Caching is therefore ineffective here, which is what applyCount + // rising on every paint shows. triggerPaintOnCanvas (*comp, 128, 128); EXPECT_GT (effect->applyCount, applyCountAfterFirst); + // The canvas itself is reused while the component size is unchanged - only its + // contents are redrawn - so no GPU render target is reallocated per frame. auto secondCanvas = ComponentHelper::getCachedTextureCanvas (*comp); ASSERT_NE (secondCanvas, nullptr); - EXPECT_NE (firstCanvas.get(), secondCanvas.get()); + EXPECT_EQ (firstCanvas.get(), secondCanvas.get()); +} + +TEST_F (ComponentEffectGpuTest, EffectCanvasIsReusedUntilTheComponentIsResized) +{ + if (! gpuContext) + return; + + auto comp = makeComp ("test", 128, 128); + comp->setVisible (true); + + auto effect = ReferenceCountedObjectPtr (new CountingEffect()); + comp->setComponentEffect (effect); + + triggerPaintOnCanvas (*comp, 128, 128); + + // Holding this reference also keeps the assertions below honest: the replaced + // canvas stays alive, so a new one cannot be allocated at the same address. + auto firstCanvas = ComponentHelper::getEffectOffscreenCanvas (*comp); + ASSERT_NE (firstCanvas, nullptr); + EXPECT_EQ (128, firstCanvas->getWidth()); + EXPECT_EQ (128, firstCanvas->getHeight()); + + // Unchanged size: the same canvas is drawn into again. + triggerPaintOnCanvas (*comp, 128, 128); + EXPECT_EQ (firstCanvas.get(), ComponentHelper::getEffectOffscreenCanvas (*comp).get()); + + // A resize leaves the cached canvas the wrong size, so it has to be replaced. + comp->setBounds (0, 0, 64, 64); + triggerPaintOnCanvas (*comp, 64, 64); + + auto resizedCanvas = ComponentHelper::getEffectOffscreenCanvas (*comp); + ASSERT_NE (resizedCanvas, nullptr); + EXPECT_NE (firstCanvas.get(), resizedCanvas.get()); + EXPECT_EQ (64, resizedCanvas->getWidth()); + EXPECT_EQ (64, resizedCanvas->getHeight()); } #endif // YUP_MAC diff --git a/tests/yup_gui/yup_ComponentNative.cpp b/tests/yup_gui/yup_ComponentNative.cpp new file mode 100644 index 000000000..b16778816 --- /dev/null +++ b/tests/yup_gui/yup_ComponentNative.cpp @@ -0,0 +1,382 @@ +/* + ============================================================================== + + 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 + +#include + +using namespace yup; + +namespace +{ + +// ============================================================================== +// Minimal concrete ComponentNative for testing the constructor. +// ============================================================================== + +class StubComponentNative final : public ComponentNative +{ +public: + StubComponentNative (Component& comp, const Flags& f) + : ComponentNative (comp, f) + { + } + + void setTitle (const String&) override {} + + String getTitle() const override { return {}; } + + void setVisible (bool) override {} + + bool isVisible() const override { return false; } + + void toFront() override {} + + void setSize (const Size&) override {} + + Size getSize() const override { return {}; } + + Size getContentSize() const override { return {}; } + + Point getPosition() const override { return {}; } + + void setPosition (const Point&) override {} + + Rectangle getBounds() const override { return {}; } + + void setBounds (const Rectangle&) override {} + + Rectangle getSafeAreaBounds() const override { return {}; } + + void setFullScreen (bool) override {} + + bool isFullScreen() const override { return false; } + + bool isDecorated() const override { return false; } + + void setOpacity (float) override {} + + float getOpacity() const override { return 1.0f; } + + void setFocusedComponent (Component*) override {} + + Component* getFocusedComponent() const override { return nullptr; } + + bool isContinuousRepaintingEnabled() const override { return false; } + + void enableContinuousRepainting (bool) override {} + + bool isAtomicModeEnabled() const override { return false; } + + void enableAtomicMode (bool) override {} + + bool isWireframeEnabled() const override { return false; } + + void enableWireframe (bool) override {} + + void repaint() override {} + + void repaint (const Rectangle&) override {} + + const RectangleList& getRepaintAreas() const override + { + static RectangleList r; + return r; + } + + void startTextInput (Component&) override {} + + void stopTextInput (Component&) override {} + + void updateTextInputRect (Component&) override {} + + float getScaleDpi() const override { return 1.0f; } + + float getCurrentFrameRate() const override { return 60.0f; } + + float getDesiredFrameRate() const override { return 60.0f; } + + void* getNativeHandle() const override { return nullptr; } + + rive::Factory* getFactory() override { return nullptr; } + + GraphicsContext* getGraphicsContext() override { return nullptr; } + + Component& getComponent() const { return component; } + + Flags getFlags() const { return flags; } + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StubComponentNative) +}; + +} // namespace + +// ============================================================================== +// ComponentNative::Options — builder pattern tests +// ============================================================================== + +class ComponentNativeOptionsTests : public ::testing::Test +{ +protected: + ComponentNative::Options opts; +}; + +TEST_F (ComponentNativeOptionsTests, DefaultOptionsHaveDefaultFlags) +{ + EXPECT_EQ (opts.flags, ComponentNative::defaultFlags); + EXPECT_EQ (opts.graphicsApi, std::nullopt); + EXPECT_EQ (opts.framerateRedraw, std::nullopt); + EXPECT_EQ (opts.clearColor, std::nullopt); + EXPECT_EQ (opts.doubleClickTime, std::nullopt); + EXPECT_FALSE (opts.updateOnlyWhenFocused); +} + +TEST_F (ComponentNativeOptionsTests, WithFlagsOverridesAllFlags) +{ + auto& result = opts.withFlags (ComponentNative::noFlags); + EXPECT_EQ (&result, &opts); + EXPECT_EQ (opts.flags, ComponentNative::noFlags); +} + +TEST_F (ComponentNativeOptionsTests, WithDecorationTrueEnablesFlag) +{ + opts.withDecoration (true); + EXPECT_TRUE (opts.flags.test (ComponentNative::decoratedWindow)); +} + +TEST_F (ComponentNativeOptionsTests, WithDecorationFalseDisablesFlag) +{ + opts.withDecoration (true); + opts.withDecoration (false); + EXPECT_FALSE (opts.flags.test (ComponentNative::decoratedWindow)); +} + +TEST_F (ComponentNativeOptionsTests, WithResizableWindowTrueEnablesFlag) +{ + opts.withResizableWindow (true); + EXPECT_TRUE (opts.flags.test (ComponentNative::resizableWindow)); +} + +TEST_F (ComponentNativeOptionsTests, WithResizableWindowFalseDisablesFlag) +{ + opts.withResizableWindow (true); + opts.withResizableWindow (false); + EXPECT_FALSE (opts.flags.test (ComponentNative::resizableWindow)); +} + +TEST_F (ComponentNativeOptionsTests, WithRenderContinuousTrueEnablesFlag) +{ + opts.withRenderContinuous (true); + EXPECT_TRUE (opts.flags.test (ComponentNative::renderContinuous)); +} + +TEST_F (ComponentNativeOptionsTests, WithRenderContinuousFalseDisablesFlag) +{ + opts.withRenderContinuous (true); + opts.withRenderContinuous (false); + EXPECT_FALSE (opts.flags.test (ComponentNative::renderContinuous)); +} + +TEST_F (ComponentNativeOptionsTests, WithAllowedHighDensityDisplayTrueEnablesFlag) +{ + opts.withAllowedHighDensityDisplay (true); + EXPECT_TRUE (opts.flags.test (ComponentNative::allowHighDensityDisplay)); +} + +TEST_F (ComponentNativeOptionsTests, WithAllowedHighDensityDisplayFalseDisablesFlag) +{ + opts.withAllowedHighDensityDisplay (true); + opts.withAllowedHighDensityDisplay (false); + EXPECT_FALSE (opts.flags.test (ComponentNative::allowHighDensityDisplay)); +} + +TEST_F (ComponentNativeOptionsTests, WithMouseCaptureTrueEnablesFlag) +{ + opts.withMouseCapture (true); + EXPECT_TRUE (opts.flags.test (ComponentNative::captureMouse)); +} + +TEST_F (ComponentNativeOptionsTests, WithMouseCaptureFalseDisablesFlag) +{ + opts.withMouseCapture (true); + opts.withMouseCapture (false); + EXPECT_FALSE (opts.flags.test (ComponentNative::captureMouse)); +} + +TEST_F (ComponentNativeOptionsTests, WithTemporaryWindowTrueEnablesFlag) +{ + opts.withTemporaryWindow (true); + EXPECT_TRUE (opts.flags.test (ComponentNative::temporaryWindow)); +} + +TEST_F (ComponentNativeOptionsTests, WithTemporaryWindowFalseDisablesFlag) +{ + opts.withTemporaryWindow (true); + opts.withTemporaryWindow (false); + EXPECT_FALSE (opts.flags.test (ComponentNative::temporaryWindow)); +} + +TEST_F (ComponentNativeOptionsTests, WithGraphicsApiSetsValue) +{ + opts.withGraphicsApi (GpuPlatform::Metal); + ASSERT_TRUE (opts.graphicsApi.has_value()); + EXPECT_EQ (*opts.graphicsApi, GpuPlatform::Metal); +} + +TEST_F (ComponentNativeOptionsTests, WithGraphicsApiNulloptClearsValue) +{ + opts.withGraphicsApi (GpuPlatform::Metal); + opts.withGraphicsApi (std::nullopt); + EXPECT_FALSE (opts.graphicsApi.has_value()); +} + +TEST_F (ComponentNativeOptionsTests, WithFramerateRedrawSetsValue) +{ + opts.withFramerateRedraw (30.0f); + ASSERT_TRUE (opts.framerateRedraw.has_value()); + EXPECT_FLOAT_EQ (*opts.framerateRedraw, 30.0f); +} + +TEST_F (ComponentNativeOptionsTests, WithFramerateRedrawNulloptClearsValue) +{ + opts.withFramerateRedraw (30.0f); + opts.withFramerateRedraw (std::nullopt); + EXPECT_FALSE (opts.framerateRedraw.has_value()); +} + +TEST_F (ComponentNativeOptionsTests, WithClearColorSetsValue) +{ + const Color col (0xff112233); + opts.withClearColor (col); + ASSERT_TRUE (opts.clearColor.has_value()); + EXPECT_EQ (*opts.clearColor, col); +} + +TEST_F (ComponentNativeOptionsTests, WithClearColorNulloptClearsValue) +{ + opts.withClearColor (Color (0xff112233)); + opts.withClearColor (std::nullopt); + EXPECT_FALSE (opts.clearColor.has_value()); +} + +TEST_F (ComponentNativeOptionsTests, WithDoubleClickTimeSetsValue) +{ + const auto t = RelativeTime::milliseconds (400); + opts.withDoubleClickTime (t); + ASSERT_TRUE (opts.doubleClickTime.has_value()); + EXPECT_EQ (*opts.doubleClickTime, t); +} + +TEST_F (ComponentNativeOptionsTests, WithDoubleClickTimeNulloptClearsValue) +{ + opts.withDoubleClickTime (RelativeTime::milliseconds (400)); + opts.withDoubleClickTime (std::nullopt); + EXPECT_FALSE (opts.doubleClickTime.has_value()); +} + +TEST_F (ComponentNativeOptionsTests, WithUpdateOnlyFocusedTrue) +{ + auto& result = opts.withUpdateOnlyFocused (true); + EXPECT_EQ (&result, &opts); + EXPECT_TRUE (opts.updateOnlyWhenFocused); +} + +TEST_F (ComponentNativeOptionsTests, WithUpdateOnlyFocusedFalse) +{ + opts.withUpdateOnlyFocused (true); + opts.withUpdateOnlyFocused (false); + EXPECT_FALSE (opts.updateOnlyWhenFocused); +} + +TEST_F (ComponentNativeOptionsTests, ChainedOptionsAllApply) +{ + opts.withFlags (ComponentNative::noFlags) + .withDecoration (true) + .withResizableWindow (false) + .withRenderContinuous (true) + .withAllowedHighDensityDisplay (true) + .withMouseCapture (true) + .withTemporaryWindow (true) + .withGraphicsApi (GpuPlatform::Headless) + .withFramerateRedraw (60.0f) + .withClearColor (Color (0xff000000)) + .withDoubleClickTime (RelativeTime::milliseconds (500)) + .withUpdateOnlyFocused (true); + + EXPECT_TRUE (opts.flags.test (ComponentNative::decoratedWindow)); + EXPECT_FALSE (opts.flags.test (ComponentNative::resizableWindow)); + EXPECT_TRUE (opts.flags.test (ComponentNative::renderContinuous)); + EXPECT_TRUE (opts.flags.test (ComponentNative::allowHighDensityDisplay)); + EXPECT_TRUE (opts.flags.test (ComponentNative::captureMouse)); + EXPECT_TRUE (opts.flags.test (ComponentNative::temporaryWindow)); + ASSERT_TRUE (opts.graphicsApi.has_value()); + EXPECT_EQ (*opts.graphicsApi, GpuPlatform::Headless); + ASSERT_TRUE (opts.framerateRedraw.has_value()); + EXPECT_FLOAT_EQ (*opts.framerateRedraw, 60.0f); + ASSERT_TRUE (opts.clearColor.has_value()); + EXPECT_TRUE (opts.updateOnlyWhenFocused); +} + +// ============================================================================== +// ComponentNative — construction / destruction +// ============================================================================== + +class ComponentNativeConstructionTests : public ::testing::Test +{ +protected: + void SetUp() override + { + comp.setBounds (0, 0, 100, 100); + } + + Component comp; +}; + +TEST_F (ComponentNativeConstructionTests, ConstructWithDefaultFlags) +{ + StubComponentNative native (comp, ComponentNative::defaultFlags); + EXPECT_EQ (&native.getComponent(), &comp); + EXPECT_EQ (native.getFlags(), ComponentNative::defaultFlags); +} + +TEST_F (ComponentNativeConstructionTests, ConstructWithNoFlags) +{ + StubComponentNative native (comp, ComponentNative::noFlags); + EXPECT_EQ (&native.getComponent(), &comp); + EXPECT_EQ (native.getFlags(), ComponentNative::noFlags); +} + +TEST_F (ComponentNativeConstructionTests, ConstructWithCustomFlags) +{ + auto flags = ComponentNative::decoratedWindow | ComponentNative::renderContinuous; + StubComponentNative native (comp, flags); + EXPECT_EQ (&native.getComponent(), &comp); + EXPECT_EQ (native.getFlags(), flags); +} + +TEST_F (ComponentNativeConstructionTests, DestructorDoesNotCrash) +{ + { + StubComponentNative native (comp, ComponentNative::defaultFlags); + EXPECT_NO_THROW ({ /* destructor called here */ }); + } + SUCCEED(); +} diff --git a/tests/yup_gui/yup_PopupMenu.cpp b/tests/yup_gui/yup_PopupMenu.cpp index ec8c47b6f..7480bd7a6 100644 --- a/tests/yup_gui/yup_PopupMenu.cpp +++ b/tests/yup_gui/yup_PopupMenu.cpp @@ -59,6 +59,8 @@ class PopupMenuTest : public ::testing::Test void TearDown() override { + PopupMenu::dismissAllPopups(); + ApplicationTheme::setGlobalTheme (oldTheme.get()); theme = nullptr; oldTheme = nullptr; diff --git a/tests/yup_rhi.cpp b/tests/yup_rhi.cpp index f45c0d407..13ec6e121 100644 --- a/tests/yup_rhi.cpp +++ b/tests/yup_rhi.cpp @@ -27,3 +27,7 @@ #include "yup_rhi/yup_GpuTarget.cpp" #include "yup_rhi/yup_GpuPipeline.cpp" #include "yup_rhi/yup_GpuPipelineMocked.cpp" + +#if YUP_LINUX +#include "yup_rhi/native/yup_GpuDevice_linux.cpp" +#endif diff --git a/tests/yup_rhi/native/yup_GpuDevice_linux.cpp b/tests/yup_rhi/native/yup_GpuDevice_linux.cpp new file mode 100644 index 000000000..011f02077 --- /dev/null +++ b/tests/yup_rhi/native/yup_GpuDevice_linux.cpp @@ -0,0 +1,805 @@ +/* + ============================================================================== + + 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 + +#include +#include +#include + +#include + +#include + +using namespace yup; + +namespace +{ + +// ============================================================================== +// Helper: create a GpuDevice backed by a real OpenGL context via SDL. +// +// Creates a hidden SDL window + GL context, makes it current, and uses +// SDL_GL_GetProcAddress as the loader function for GpuDevice::Options. +// ============================================================================== + +struct GLContext +{ + SDL_Window* window = nullptr; + SDL_GLContext glContext = nullptr; + + bool init() + { +#if YUP_LINUX + // Force Mesa's llvmpipe software rasterizer for CI environments. + // Real GPU drivers take precedence when available. + setenv ("LIBGL_ALWAYS_SOFTWARE", "1", 0); + setenv ("GALLIUM_DRIVER", "llvmpipe", 0); +#endif + + SDL_SetHint (SDL_HINT_RENDER_DRIVER, "opengl"); + + SDL_GL_SetAttribute (SDL_GL_CONTEXT_MAJOR_VERSION, YUP_RIVE_OPENGL_MAJOR); + SDL_GL_SetAttribute (SDL_GL_CONTEXT_MINOR_VERSION, YUP_RIVE_OPENGL_MINOR); + SDL_GL_SetAttribute (SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + + window = SDL_CreateWindow ("yup_rhi_gl_test", + 64, + 64, + SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); + if (window == nullptr) + { + fprintf (stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); + return false; + } + + glContext = SDL_GL_CreateContext (window); + if (glContext == nullptr) + { + fprintf (stderr, "SDL_GL_CreateContext failed: %s\n", SDL_GetError()); + SDL_DestroyWindow (window); + window = nullptr; + return false; + } + + SDL_GL_MakeCurrent (window, glContext); + return true; + } + + void shutdown() + { + if (glContext != nullptr) + { + SDL_GL_DestroyContext (glContext); + glContext = nullptr; + } + if (window != nullptr) + { + SDL_DestroyWindow (window); + window = nullptr; + } + } + + GpuDevice::Ptr createDevice() const + { + GpuDevice::Options opts; + opts.loaderFunction = (GpuDevice::LoaderFunction) SDL_GL_GetProcAddress; + opts.readableFramebuffer = true; + + auto device = GpuDevice::create (GpuPlatform::OpenGL, opts); + if (device == nullptr) + fprintf (stderr, "GpuDevice::create(OpenGL) returned null\n"); + + return device; + } +}; + +} // namespace + +// ============================================================================== +// GpuDeviceOpenGL — real GPU device tests (Linux, OpenGL via SDL) +// ============================================================================== + +class GpuDeviceOpenGLTests : public ::testing::Test +{ +protected: + void SetUp() override + { + if (! gl.init()) + GTEST_SKIP() << "Cannot create OpenGL context — is a display available?"; + device = gl.createDevice(); + if (device == nullptr) + GTEST_SKIP() << "Cannot create OpenGL GpuDevice — is GL 4.5 supported?"; + + GpuDevice::Options ctxOpts; + ctxOpts.loaderFunction = (GpuDevice::LoaderFunction) SDL_GL_GetProcAddress; + graphicsContext = GraphicsContext::createContext (GpuPlatform::OpenGL, ctxOpts, device); + if (graphicsContext == nullptr) + GTEST_SKIP() << "Cannot create OpenGL GraphicsContext"; + } + + void TearDown() override + { + graphicsContext = nullptr; + device = nullptr; + gl.shutdown(); + } + + GLContext gl; + GpuDevice::Ptr device; + std::unique_ptr graphicsContext; +}; + +// -------------------------------------------------------------------------- +// Basic device creation +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, DeviceIsNotNull) +{ + ASSERT_NE (device, nullptr); +} + +TEST_F (GpuDeviceOpenGLTests, PlatformIsOpenGL) +{ + EXPECT_EQ (device->getPlatform(), GpuPlatform::OpenGL); +} + +TEST_F (GpuDeviceOpenGLTests, GpuContextIsNotNull) +{ + EXPECT_NE (device->getGpuContext(), nullptr); +} + +// -------------------------------------------------------------------------- +// Buffer creation +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, CreateVertexBuffer) +{ + const float verts[] = { 0.0f, 1.0f, 2.0f, 3.0f }; + auto buf = device->createBuffer (GpuBufferType::vertex, verts, sizeof (verts)); + ASSERT_NE (buf, nullptr); + EXPECT_EQ (buf->getType(), GpuBufferType::vertex); + EXPECT_EQ (buf->getSizeInBytes(), sizeof (verts)); + EXPECT_TRUE (buf->isValid()); +} + +TEST_F (GpuDeviceOpenGLTests, CreateIndexBuffer) +{ + const uint16_t indices[] = { 0, 1, 2, 3 }; + auto buf = device->createBuffer (GpuBufferType::index, indices, sizeof (indices)); + ASSERT_NE (buf, nullptr); + EXPECT_EQ (buf->getType(), GpuBufferType::index); + EXPECT_TRUE (buf->isValid()); +} + +TEST_F (GpuDeviceOpenGLTests, CreateUniformBuffer) +{ + const int data[] = { 42, 43, 44 }; + auto buf = device->createBuffer (GpuBufferType::uniform, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + EXPECT_EQ (buf->getType(), GpuBufferType::uniform); + EXPECT_TRUE (buf->isValid()); +} + +TEST_F (GpuDeviceOpenGLTests, CreateBufferWithNullDataReturnsNull) +{ + EXPECT_EQ (device->createBuffer (GpuBufferType::vertex, nullptr, 16), nullptr); +} + +TEST_F (GpuDeviceOpenGLTests, CreateBufferWithZeroSizeReturnsNull) +{ + const float data[] = { 1.0f }; + EXPECT_EQ (device->createBuffer (GpuBufferType::vertex, data, 0), nullptr); +} + +// -------------------------------------------------------------------------- +// Buffer update +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, UpdateVertexBuffer) +{ + const float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + auto buf = device->createBuffer (GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + + const float newData[] = { 5.0f, 6.0f, 7.0f, 8.0f }; + EXPECT_TRUE (device->updateBuffer (buf, newData, sizeof (newData))); +} + +TEST_F (GpuDeviceOpenGLTests, UpdateBufferLargerThanOriginalReturnsFalse) +{ + const float data[] = { 1.0f }; + auto buf = device->createBuffer (GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + + const float larger[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + EXPECT_FALSE (device->updateBuffer (buf, larger, sizeof (larger))); +} + +// -------------------------------------------------------------------------- +// Offscreen target creation +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, CreateOffscreenTarget) +{ + auto target = device->createOffscreenTarget (256, 256); + ASSERT_NE (target, nullptr); + EXPECT_EQ (target->getWidth(), 256); + EXPECT_EQ (target->getHeight(), 256); +} + +TEST_F (GpuDeviceOpenGLTests, CreateOffscreenTargetZeroSize) +{ + EXPECT_EQ (device->createOffscreenTarget (0, 256), nullptr); + EXPECT_EQ (device->createOffscreenTarget (256, 0), nullptr); +} + +// -------------------------------------------------------------------------- +// GpuTarget +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, GpuTargetCreate) +{ + auto target = GpuTarget::create (device, 128, 128); + ASSERT_NE (target, nullptr); + EXPECT_EQ (target->getWidth(), 128); + EXPECT_EQ (target->getHeight(), 128); +} + +TEST_F (GpuDeviceOpenGLTests, GpuTargetAsTexture) +{ + auto target = GpuTarget::create (device, 128, 128); + ASSERT_NE (target, nullptr); + + auto tex = target->asTexture(); + ASSERT_NE (tex, nullptr); + EXPECT_TRUE (tex->isValid()); + EXPECT_EQ (tex->getWidth(), 128); + EXPECT_EQ (tex->getHeight(), 128); + EXPECT_TRUE (tex->isRenderTarget()); +} + +TEST_F (GpuDeviceOpenGLTests, GpuTargetReadPixels) +{ + auto target = GpuTarget::create (device, 128, 128); + ASSERT_NE (target, nullptr); + + std::vector pixels (128 * 128 * 4); + EXPECT_TRUE (target->readPixels (pixels.data(), pixels.size())); +} + +TEST_F (GpuDeviceOpenGLTests, GpuTargetReadPixelsTooSmallBufferReturnsFalse) +{ + auto target = GpuTarget::create (device, 128, 128); + ASSERT_NE (target, nullptr); + + std::vector pixels (16); + EXPECT_FALSE (target->readPixels (pixels.data(), pixels.size())); +} + +// -------------------------------------------------------------------------- +// GpuFrame +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, GpuFrameBeginReturnsValidFrame) +{ + auto frame = GpuFrame::begin (device); + EXPECT_TRUE (frame.isValid()); +} + +TEST_F (GpuDeviceOpenGLTests, GpuFrameSubmit) +{ + auto frame = GpuFrame::begin (device); + ASSERT_TRUE (frame.isValid()); + EXPECT_TRUE (frame.submit()); + EXPECT_FALSE (frame.submit()); // Idempotent after first submit. +} + +TEST_F (GpuDeviceOpenGLTests, GpuFrameWaitForGPU) +{ + auto frame = GpuFrame::begin (device); + ASSERT_TRUE (frame.isValid()); + frame.submit(); + EXPECT_NO_THROW (frame.waitForGPU()); +} + +// -------------------------------------------------------------------------- +// Pipeline compilation +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, CompilePipelineWithMinimalShaders) +{ +#if ! YUP_ENABLE_SHADER_TRANSPILER + GTEST_SKIP() << "Shader transpiler unavailable — cannot compile GLSL sources inline"; +#else + const char* vsSrc = R"( + #version 450 + layout(set = 0, binding = 0) uniform Uniforms { mat4 mvp; } ubo; + layout(location = 0) in vec3 aPos; + void main() { gl_Position = ubo.mvp * vec4(aPos, 1.0); } + )"; + + const char* fsSrc = R"( + #version 450 + layout(location = 0) out vec4 fragColor; + void main() { fragColor = vec4(1.0, 0.0, 0.0, 1.0); } + )"; + + auto result = GpuPipeline::compileFromGlsl (device, vsSrc, fsSrc); + ASSERT_TRUE (result.wasOk()); + ASSERT_NE (result.getValue(), nullptr); +#endif +} + +TEST_F (GpuDeviceOpenGLTests, CompilePipelineFailsWithEmptyVertexCode) +{ + GpuShaderSource vs; + vs.language = GpuShaderLanguage::glsl; + vs.code = nullptr; + vs.codeSize = 0; + + GpuShaderSource fs; + fs.language = GpuShaderLanguage::glsl; + fs.code = "void main() {}"; + fs.codeSize = (uint32_t) strlen ("void main() {}"); + + auto result = GpuPipeline::compile (device, vs, fs); + EXPECT_TRUE (result.failed()); +} + +// -------------------------------------------------------------------------- +// Render pass (end-to-end) +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, RenderPassDrawTriangle) +{ + // Create a render target. + auto target = GpuTarget::create (device, 256, 256); + ASSERT_NE (target, nullptr); + +#if ! YUP_ENABLE_SHADER_TRANSPILER + GTEST_SKIP() << "Shader transpiler unavailable — cannot compile GLSL sources inline"; +#else + // Compile a minimal pipeline. + const char* vsSrc = R"( + #version 450 + layout(set = 0, binding = 0) uniform Uniforms { mat4 mvp; } ubo; + layout(location = 0) in vec3 aPos; + void main() { gl_Position = ubo.mvp * vec4(aPos, 1.0); } + )"; + + const char* fsSrc = R"( + #version 450 + layout(location = 0) out vec4 fragColor; + void main() { fragColor = vec4(1.0, 0.0, 0.0, 1.0); } + )"; + + auto compileResult = GpuPipeline::compileFromGlsl (device, vsSrc, fsSrc); + ASSERT_TRUE (compileResult.wasOk()); + auto* pipeline = compileResult.getValue().get(); + ASSERT_NE (pipeline, nullptr); + + // Begin a frame and render pass. + auto frame = GpuFrame::begin (device); + ASSERT_TRUE (frame.isValid()); + + auto pass = target->beginRenderPass (frame, { true, Color (0, 0, 0, 0) }); + ASSERT_TRUE (pass.isValid()); + + pass.setPipeline (*pipeline); + EXPECT_TRUE (pass.draw (3)); + + pass.finish(); + frame.submit(); +#endif +} + +TEST_F (GpuDeviceOpenGLTests, RenderPassClearColor) +{ + auto target = GpuTarget::create (device, 64, 64); + ASSERT_NE (target, nullptr); + + // Render with a green clear color. + auto frame = GpuFrame::begin (device); + ASSERT_TRUE (frame.isValid()); + + GpuRenderOptions opts; + opts.clear = true; + opts.clearColor = Color (255, 0, 255, 0); // ARGB: Green, fully opaque. + + auto pass = target->beginRenderPass (frame, opts); + ASSERT_TRUE (pass.isValid()); + pass.finish(); + frame.submit(); + frame.waitForGPU(); + + // Read back the pixels — they should be green. + std::vector pixels (64 * 64 * 4); + ASSERT_TRUE (target->readPixels (pixels.data(), pixels.size())); + + // Check a few pixels in the center are green. + const size_t center = (32 * 64 + 32) * 4; + EXPECT_EQ (pixels[center + 0], 0u); // R + EXPECT_EQ (pixels[center + 1], 255u); // G + EXPECT_EQ (pixels[center + 2], 0u); // B + EXPECT_EQ (pixels[center + 3], 255u); // A +} + +TEST_F (GpuDeviceOpenGLTests, GpuFrameMovePreservesState) +{ + auto src = GpuFrame::begin (device); + ASSERT_TRUE (src.isValid()); + + GpuFrame dst (std::move (src)); + EXPECT_TRUE (dst.isValid()); + EXPECT_FALSE (src.isValid()); + + dst.submit(); +} + +// -------------------------------------------------------------------------- +// GpuCanvas integration (via GpuCanvas, which wraps GpuTarget) +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, GpuCanvasCreate) +{ + auto canvas = GpuCanvas::create (*graphicsContext, 256, 256); + ASSERT_NE (canvas, nullptr); +} + +TEST_F (GpuDeviceOpenGLTests, GpuCanvasCreateClearsToTransparentBlackByDefault) +{ + // A new canvas must be safe to sample before anything is drawn into it, so its + // backing texture cannot be left holding uninitialized GPU memory. + auto canvas = GpuCanvas::create (*graphicsContext, 64, 64); + ASSERT_NE (canvas, nullptr); + + std::vector pixels (64 * 64 * 4, 0xab); + ASSERT_TRUE (canvas->readPixels (pixels.data(), pixels.size())); + + size_t nonZeroBytes = 0; + for (const auto value : pixels) + { + if (value != 0u) + ++nonZeroBytes; + } + + EXPECT_EQ (nonZeroBytes, 0u); +} + +TEST_F (GpuDeviceOpenGLTests, GpuCanvasCreateClearsToRequestedColor) +{ + auto canvas = GpuCanvas::create (*graphicsContext, 64, 64, Color (255, 0, 128, 255)); + ASSERT_NE (canvas, nullptr); + + std::vector pixels (64 * 64 * 4, 0); + ASSERT_TRUE (canvas->readPixels (pixels.data(), pixels.size())); + + const size_t centerIdx = (32 * 64 + 32) * 4; + EXPECT_EQ (pixels[centerIdx + 0], 0u); // R + EXPECT_EQ (pixels[centerIdx + 1], 128u); // G + EXPECT_EQ (pixels[centerIdx + 2], 255u); // B + EXPECT_EQ (pixels[centerIdx + 3], 255u); // A +} + +TEST_F (GpuDeviceOpenGLTests, GpuCanvasCreateWithoutClearStillSucceeds) +{ + // std::nullopt skips the clear; the contents are then undefined by contract, so + // only creation itself is asserted here. + auto canvas = GpuCanvas::create (*graphicsContext, 64, 64, std::nullopt); + ASSERT_NE (canvas, nullptr); + EXPECT_EQ (canvas->getWidth(), 64); + EXPECT_EQ (canvas->getHeight(), 64); +} + +TEST_F (GpuDeviceOpenGLTests, ClearOffscreenFillsRenderableTarget) +{ + // clearOffscreen needs neither an active frame nor a render pass, so it can run + // straight after the target is created. + auto target = device->createRenderableTarget (64, 64); + ASSERT_NE (target, nullptr); + + ASSERT_TRUE (device->clearOffscreen (*target, GpuColor (0.0f, 1.0f, 0.0f, 1.0f))); + + std::vector pixels (64 * 64 * 4, 0); + ASSERT_TRUE (device->readOffscreenPixels (*target, pixels.data(), pixels.size())); + + const size_t centerIdx = (32 * 64 + 32) * 4; + EXPECT_EQ (pixels[centerIdx + 0], 0u); // R + EXPECT_EQ (pixels[centerIdx + 1], 255u); // G + EXPECT_EQ (pixels[centerIdx + 2], 0u); // B + EXPECT_EQ (pixels[centerIdx + 3], 255u); // A +} + +TEST_F (GpuDeviceOpenGLTests, ClearOffscreenFillsRenderPassOnlyTarget) +{ + // The same call must also work on a plain offscreen target, which has no + // dedicated render context. + auto target = device->createOffscreenTarget (64, 64); + ASSERT_NE (target, nullptr); + + ASSERT_TRUE (device->clearOffscreen (*target, GpuColor (1.0f, 0.0f, 0.0f, 1.0f))); + + std::vector pixels (64 * 64 * 4, 0); + ASSERT_TRUE (device->readOffscreenPixels (*target, pixels.data(), pixels.size())); + + const size_t centerIdx = (32 * 64 + 32) * 4; + EXPECT_EQ (pixels[centerIdx + 0], 255u); // R + EXPECT_EQ (pixels[centerIdx + 1], 0u); // G + EXPECT_EQ (pixels[centerIdx + 2], 0u); // B + EXPECT_EQ (pixels[centerIdx + 3], 255u); // A +} + +TEST_F (GpuDeviceOpenGLTests, GpuCanvasBeginDrawAndCommit) +{ + auto canvas = GpuCanvas::create (*graphicsContext, 256, 256); + ASSERT_NE (canvas, nullptr); + + canvas->beginDraw(); + canvas->commit(); +} + +TEST_F (GpuDeviceOpenGLTests, GpuCanvasAsImage) +{ + auto canvas = GpuCanvas::create (*graphicsContext, 256, 256); + ASSERT_NE (canvas, nullptr); + + auto& g = canvas->beginDraw(); + + // Draw a filled rectangle. + g.setFillColor (Color (255, 255, 0, 0)); // ARGB: Red, fully opaque. + g.fillRect (0, 0, 256, 256); + + canvas->commit(); + + auto img = canvas->asImage(); + EXPECT_TRUE (img.isValid()); + EXPECT_EQ (img.getWidth(), 256); + EXPECT_EQ (img.getHeight(), 256); +} + +TEST_F (GpuDeviceOpenGLTests, GpuCanvasReadPixelsAfterDraw) +{ + auto canvas = GpuCanvas::create (*graphicsContext, 128, 128); + ASSERT_NE (canvas, nullptr); + + auto& g = canvas->beginDraw(); + + // Fill the entire canvas with red. + g.setFillColor (Color (255, 255, 0, 0)); // ARGB: Red, fully opaque. + g.fillRect (0, 0, 128, 128); + + canvas->commit(); + + std::vector pixels (128 * 128 * 4); + EXPECT_TRUE (canvas->readPixels (pixels.data(), pixels.size())); + + // Center pixel should be red. + const size_t centerIdx = (64 * 128 + 64) * 4; + EXPECT_EQ (pixels[centerIdx + 0], 255u); // R + EXPECT_EQ (pixels[centerIdx + 1], 0u); // G + EXPECT_EQ (pixels[centerIdx + 2], 0u); // B + EXPECT_EQ (pixels[centerIdx + 3], 255u); // A +} + +// -------------------------------------------------------------------------- +// Compute shader support +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, IsComputeAvailable) +{ + // GL 4.3+ should support compute shaders. + EXPECT_TRUE (device->isComputeAvailable()); +} + +// -------------------------------------------------------------------------- +// GpuComputePipeline +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, ComputePipelineCompileFailsWithNullDevice) +{ + GpuShaderSource src; + src.language = GpuShaderLanguage::glsl; + src.code = "void main() {}"; + src.codeSize = static_cast (strlen (static_cast (src.code))); + + auto result = GpuComputePipeline::compile (nullptr, src, GpuWorkgroupSize { 16, 1, 1 }); + EXPECT_TRUE (result.failed()); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePipelineCompileFromGlslMinimalShader) +{ +#if ! YUP_ENABLE_SHADER_TRANSPILER + GTEST_SKIP() << "Shader transpiler unavailable — cannot compile GLSL sources inline"; +#else + const char* glsl = R"( + #version 450 + layout(local_size_x = 8, local_size_y = 1, local_size_z = 1) in; + layout(std430, binding = 0) buffer OutputBuf { float values[]; } outputBuf; + void main() + { + uint idx = gl_GlobalInvocationID.x; + outputBuf.values[idx] = float(idx) * 2.0; + } + )"; + + auto result = GpuComputePipeline::compileFromGlsl (device, glsl); + ASSERT_TRUE (result.wasOk()); + ASSERT_NE (result.getValue(), nullptr); + + auto wgs = result.getValue()->getWorkgroupSize(); + EXPECT_EQ (wgs.x, 8u); + EXPECT_EQ (wgs.y, 1u); + EXPECT_EQ (wgs.z, 1u); +#endif +} + +TEST_F (GpuDeviceOpenGLTests, ComputePipelineCompileFromGlslFailsWithNullDevice) +{ +#if ! YUP_ENABLE_SHADER_TRANSPILER + GTEST_SKIP() << "Shader transpiler unavailable — cannot compile GLSL sources inline"; +#else + auto result = GpuComputePipeline::compileFromGlsl (nullptr, "#version 450\nvoid main() {}"); + EXPECT_TRUE (result.failed()); +#endif +} + +TEST_F (GpuDeviceOpenGLTests, ComputePipelineCompileFromBundleFailsWithNullDevice) +{ + ShaderBundle bundle; + auto result = GpuComputePipeline::compileFromBundle (nullptr, bundle); + EXPECT_TRUE (result.failed()); +} + +// -------------------------------------------------------------------------- +// GpuComputePass +// -------------------------------------------------------------------------- + +TEST_F (GpuDeviceOpenGLTests, ComputePassBeginReturnsValidPass) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_TRUE (pass.isValid()); + + if (pass.isValid()) + pass.finish(); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassIsValid) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_TRUE (pass.isValid()); + + // After finish, isValid should return false. + pass.finish(); + EXPECT_FALSE (pass.isValid()); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassFinishIsIdempotent) +{ + auto pass = GpuComputePass::begin (device); + ASSERT_TRUE (pass.isValid()); + + EXPECT_TRUE (pass.finish()); + EXPECT_FALSE (pass.finish()); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassDispatchWithoutPipelineReturnsFalse) +{ + auto pass = GpuComputePass::begin (device); + ASSERT_TRUE (pass.isValid()); + + EXPECT_FALSE (pass.dispatch (1, 1, 1)); + pass.finish(); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassMoveConstruction) +{ + auto src = GpuComputePass::begin (device); + ASSERT_TRUE (src.isValid()); + + GpuComputePass dst (std::move (src)); + EXPECT_TRUE (dst.isValid()); + EXPECT_FALSE (src.isValid()); + + dst.finish(); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassMoveAssignment) +{ + auto src = GpuComputePass::begin (device); + ASSERT_TRUE (src.isValid()); + + auto dst = GpuComputePass::begin (GpuDevice::create (GpuPlatform::Headless, {})); + EXPECT_FALSE (dst.isValid()); + + dst = std::move (src); + EXPECT_TRUE (dst.isValid()); + EXPECT_FALSE (src.isValid()); + + dst.finish(); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassSetPipelineDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + ASSERT_TRUE (pass.isValid()); + + EXPECT_NO_THROW (pass.setPipeline (nullptr)); + + pass.finish(); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassSetStorageBufferDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + ASSERT_TRUE (pass.isValid()); + + EXPECT_NO_THROW (pass.setStorageBuffer (0, 0, nullptr)); + + pass.finish(); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassSetUniformBufferDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + ASSERT_TRUE (pass.isValid()); + + float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + EXPECT_NO_THROW (pass.setUniformBuffer (0, 0, data, sizeof (data))); + + pass.finish(); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassSetTextureDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + ASSERT_TRUE (pass.isValid()); + + EXPECT_NO_THROW (pass.setTexture (0, 0, nullptr)); + + pass.finish(); +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassDestructorCallsFinish) +{ + auto pass = GpuComputePass::begin (device); + ASSERT_TRUE (pass.isValid()); + // Let destructor call finish — should not crash. +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassDestructorAfterFinishDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + ASSERT_TRUE (pass.isValid()); + pass.finish(); + // Destructor after explicit finish — should not double-finish. +} + +TEST_F (GpuDeviceOpenGLTests, ComputePassWithHeadlessDeviceIsInvalid) +{ + auto headless = GpuDevice::create (GpuPlatform::Headless, {}); + ASSERT_NE (headless, nullptr); + + auto pass = GpuComputePass::begin (headless); + EXPECT_FALSE (pass.isValid()); + EXPECT_FALSE (pass.dispatch (1, 1, 1)); + EXPECT_FALSE (pass.finish()); +} diff --git a/tests/yup_rhi/yup_GpuComputePass.cpp b/tests/yup_rhi/yup_GpuComputePass.cpp new file mode 100644 index 000000000..8c65ecb9e --- /dev/null +++ b/tests/yup_rhi/yup_GpuComputePass.cpp @@ -0,0 +1,273 @@ +/* + ============================================================================== + + 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 + +#include + +using namespace yup; + +//============================================================================== +// GpuComputePass — headless path (compute not available) +//============================================================================== + +class GpuComputePassHeadlessTests : public ::testing::Test +{ +protected: + void SetUp() override + { + device = GpuDevice::create (GpuPlatform::Headless, {}); + ASSERT_NE (device, nullptr); + } + + GpuDevice::Ptr device; +}; + +TEST_F (GpuComputePassHeadlessTests, BeginWithNullDeviceReturnsInvalidPass) +{ + auto pass = GpuComputePass::begin (nullptr); + EXPECT_FALSE (pass.isValid()); +} + +TEST_F (GpuComputePassHeadlessTests, BeginWithHeadlessDeviceReturnsInvalidPass) +{ + // Headless backend does not support compute shaders. + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.isValid()); +} + +TEST_F (GpuComputePassHeadlessTests, IsValidReturnsFalseForDefaultConstructed) +{ + GpuComputePass pass; + EXPECT_FALSE (pass.isValid()); +} + +TEST_F (GpuComputePassHeadlessTests, SetPipelineOnInvalidPassDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.isValid()); + EXPECT_NO_THROW (pass.setPipeline (nullptr)); +} + +TEST_F (GpuComputePassHeadlessTests, SetStorageBufferOnInvalidPassDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.isValid()); + EXPECT_NO_THROW (pass.setStorageBuffer (0, 0, nullptr)); +} + +TEST_F (GpuComputePassHeadlessTests, SetUniformBufferOnInvalidPassDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.isValid()); + + float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + EXPECT_NO_THROW (pass.setUniformBuffer (0, 0, data, sizeof (data))); +} + +TEST_F (GpuComputePassHeadlessTests, SetTextureOnInvalidPassDoesNotCrash) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.isValid()); + EXPECT_NO_THROW (pass.setTexture (0, 0, nullptr)); +} + +TEST_F (GpuComputePassHeadlessTests, DispatchOnInvalidPassReturnsFalse) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.isValid()); + EXPECT_FALSE (pass.dispatch (1, 1, 1)); + EXPECT_FALSE (pass.dispatch (16, 8, 4)); +} + +TEST_F (GpuComputePassHeadlessTests, FinishOnInvalidPassReturnsFalse) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.isValid()); + EXPECT_FALSE (pass.finish()); +} + +TEST_F (GpuComputePassHeadlessTests, FinishIsIdempotentOnInvalidPass) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.finish()); + EXPECT_FALSE (pass.finish()); +} + +TEST_F (GpuComputePassHeadlessTests, MoveConstructionFromInvalidPass) +{ + auto src = GpuComputePass::begin (device); + EXPECT_FALSE (src.isValid()); + + GpuComputePass dst (std::move (src)); + EXPECT_FALSE (dst.isValid()); + EXPECT_FALSE (src.isValid()); + + EXPECT_FALSE (dst.finish()); +} + +TEST_F (GpuComputePassHeadlessTests, MoveAssignmentFromInvalidPass) +{ + auto src = GpuComputePass::begin (device); + auto dst = GpuComputePass::begin (device); + + dst = std::move (src); + EXPECT_FALSE (dst.isValid()); + EXPECT_FALSE (src.isValid()); +} + +TEST_F (GpuComputePassHeadlessTests, DestructorOnInvalidPassDoesNotCrash) +{ + { + auto pass = GpuComputePass::begin (device); + EXPECT_FALSE (pass.isValid()); + // Destructor should not crash. + } + EXPECT_TRUE (true); +} + +TEST_F (GpuComputePassHeadlessTests, SetPipelineWithNonNullDoesNotCrash) +{ + // Even though pipeline is null (no compute support), the call should not crash. + auto pass = GpuComputePass::begin (device); + EXPECT_NO_THROW (pass.setPipeline (GpuComputePipeline::Ptr (nullptr))); +} + +TEST_F (GpuComputePassHeadlessTests, SetStorageBufferCoversMultipleGroups) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_NO_THROW ({ + pass.setStorageBuffer (0, 0, nullptr); + pass.setStorageBuffer (0, 1, nullptr); + pass.setStorageBuffer (1, 0, nullptr); + pass.setStorageBuffer (1, 1, nullptr); + }); +} + +TEST_F (GpuComputePassHeadlessTests, SetUniformBufferCoversMultipleGroups) +{ + auto pass = GpuComputePass::begin (device); + float data = 42.0f; + + EXPECT_NO_THROW ({ + pass.setUniformBuffer (0, 0, &data, sizeof (data)); + pass.setUniformBuffer (0, 1, &data, sizeof (data)); + pass.setUniformBuffer (1, 0, &data, sizeof (data)); + }); +} + +TEST_F (GpuComputePassHeadlessTests, SetTextureCoversMultipleGroups) +{ + auto pass = GpuComputePass::begin (device); + EXPECT_NO_THROW ({ + pass.setTexture (0, 0, nullptr); + pass.setTexture (0, 1, nullptr); + pass.setTexture (1, 0, nullptr); + }); +} + +//============================================================================== +// GpuComputePipeline — headless path (compute not available) +//============================================================================== + +class GpuComputePipelineHeadlessTests : public ::testing::Test +{ +protected: + void SetUp() override + { + device = GpuDevice::create (GpuPlatform::Headless, {}); + ASSERT_NE (device, nullptr); + } + + GpuDevice::Ptr device; +}; + +TEST_F (GpuComputePipelineHeadlessTests, CompileWithNullDeviceReturnsFailure) +{ + GpuShaderSource source; + source.language = GpuShaderLanguage::glsl; + source.code = "void main() {}"; + source.codeSize = static_cast (strlen (static_cast (source.code))); + + GpuWorkgroupSize wgs { 16, 1, 1 }; + auto result = GpuComputePipeline::compile (nullptr, source, wgs); + EXPECT_TRUE (result.failed()); + EXPECT_FALSE (result.getErrorMessage().isEmpty()); +} + +TEST_F (GpuComputePipelineHeadlessTests, CompileWithHeadlessDeviceReturnsFailure) +{ + GpuShaderSource source; + source.language = GpuShaderLanguage::glsl; + source.code = "void main() {}"; + source.codeSize = static_cast (strlen (static_cast (source.code))); + + GpuWorkgroupSize wgs { 16, 1, 1 }; + auto result = GpuComputePipeline::compile (device, source, wgs); + EXPECT_TRUE (result.failed()); + EXPECT_STRNE (result.getErrorMessage().toRawUTF8(), ""); +} + +TEST_F (GpuComputePipelineHeadlessTests, CompileFromBundleWithNullDeviceReturnsFailure) +{ + ShaderBundle bundle; + auto result = GpuComputePipeline::compileFromBundle (nullptr, bundle); + EXPECT_TRUE (result.failed()); +} + +TEST_F (GpuComputePipelineHeadlessTests, CompileFromBundleWithHeadlessDeviceReturnsFailure) +{ + ShaderBundle bundle; + auto result = GpuComputePipeline::compileFromBundle (device, bundle); + EXPECT_TRUE (result.failed()); +} + +TEST_F (GpuComputePipelineHeadlessTests, CompileFromBundleWithDefaultWorkgroupSize) +{ + ShaderBundle bundle; + auto result = GpuComputePipeline::compileFromBundle (nullptr, bundle, GpuWorkgroupSize { 8, 8, 1 }); + EXPECT_TRUE (result.failed()); +} + +#if YUP_ENABLE_SHADER_TRANSPILER + +TEST_F (GpuComputePipelineHeadlessTests, CompileFromGlslWithNullDeviceReturnsFailure) +{ + auto result = GpuComputePipeline::compileFromGlsl (nullptr, "#version 450\nvoid main() {}"); + EXPECT_TRUE (result.failed()); +} + +TEST_F (GpuComputePipelineHeadlessTests, CompileFromGlslWithHeadlessDeviceReturnsFailure) +{ + auto result = GpuComputePipeline::compileFromGlsl (device, "#version 450\nvoid main() {}"); + EXPECT_TRUE (result.failed()); +} + +TEST_F (GpuComputePipelineHeadlessTests, CompileFromGlslWithWorkgroupSize) +{ + auto result = GpuComputePipeline::compileFromGlsl ( + nullptr, + "#version 450\nlayout(local_size_x = 8) in; void main() {}", + GpuWorkgroupSize { 8, 1, 1 }); + EXPECT_TRUE (result.failed()); +} + +#endif // YUP_ENABLE_SHADER_TRANSPILER diff --git a/tests/yup_rhi/yup_GpuDevice.cpp b/tests/yup_rhi/yup_GpuDevice.cpp new file mode 100644 index 000000000..9d13c63a7 --- /dev/null +++ b/tests/yup_rhi/yup_GpuDevice.cpp @@ -0,0 +1,228 @@ +/* + ============================================================================== + + 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 + +#include + +using namespace yup; + +//============================================================================== +// GpuDevice — error path tests +//============================================================================== + +class GpuDeviceErrorTests : public ::testing::Test +{ +protected: + void SetUp() override + { + device = GpuDevice::create (GpuPlatform::Headless, {}); + ASSERT_NE (device, nullptr); + } + + GpuDevice::Ptr device; +}; + +// --------------------------------------------------------------------------- +// create — invalid API +// --------------------------------------------------------------------------- + +TEST_F (GpuDeviceErrorTests, CreateWithInvalidApiReturnsNull) +{ + const auto invalidApi = static_cast (9999); + auto ctx = GpuDevice::create (invalidApi, {}); + EXPECT_EQ (ctx, nullptr); +} + +// --------------------------------------------------------------------------- +// readBuffer — default returns false +// --------------------------------------------------------------------------- + +TEST_F (GpuDeviceErrorTests, ReadBufferReturnsFalse) +{ + // readBuffer is a no-op in the base class; always returns false. + uint8 buf[64] = {}; + EXPECT_FALSE (device->readBuffer (nullptr, buf, sizeof (buf))); +} + +TEST_F (GpuDeviceErrorTests, ReadBufferWithNonNullBufferReturnsFalse) +{ + const float data[] = { 1.0f, 2.0f, 3.0f }; + auto buffer = GpuBuffer::create (device, GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buffer, nullptr); + + uint8 readback[sizeof (data)] = {}; + EXPECT_FALSE (device->readBuffer (buffer, readback, sizeof (readback))); +} + +TEST_F (GpuDeviceErrorTests, ReadBufferWithNullDestinationReturnsFalse) +{ + const float data[] = { 1.0f, 2.0f, 3.0f }; + auto buffer = GpuBuffer::create (device, GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buffer, nullptr); + + EXPECT_FALSE (device->readBuffer (buffer, nullptr, sizeof (data))); +} + +TEST_F (GpuDeviceErrorTests, ReadBufferLeavesDestinationUntouchedOnFailure) +{ + // A false return means "no new data yet", not "the destination is now junk": + // callers own it across calls and must be able to keep using the last snapshot. + const float data[] = { 1.0f, 2.0f, 3.0f }; + auto buffer = GpuBuffer::create (device, GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buffer, nullptr); + + float readback[] = { 7.0f, 8.0f, 9.0f }; + ASSERT_FALSE (device->readBuffer (buffer, readback, sizeof (readback))); + + EXPECT_FLOAT_EQ (readback[0], 7.0f); + EXPECT_FLOAT_EQ (readback[1], 8.0f); + EXPECT_FLOAT_EQ (readback[2], 9.0f); +} + +// --------------------------------------------------------------------------- +// updateBuffer — error paths +// --------------------------------------------------------------------------- + +TEST_F (GpuDeviceErrorTests, UpdateBufferWithNullBufferReturnsFalse) +{ + const float data[] = { 1.0f, 2.0f }; + EXPECT_FALSE (device->updateBuffer (nullptr, data, sizeof (data))); +} + +TEST_F (GpuDeviceErrorTests, UpdateBufferWithNullDataReturnsFalse) +{ + const float data[] = { 1.0f, 2.0f, 3.0f }; + auto buffer = GpuBuffer::create (device, GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buffer, nullptr); + + EXPECT_FALSE (device->updateBuffer (buffer, nullptr, sizeof (data))); +} + +TEST_F (GpuDeviceErrorTests, UpdateBufferWithZeroSizeReturnsFalse) +{ + const float data[] = { 1.0f }; + auto buffer = GpuBuffer::create (device, GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buffer, nullptr); + + EXPECT_FALSE (device->updateBuffer (buffer, data, 0)); +} + +TEST_F (GpuDeviceErrorTests, UpdateBufferSucceedsForValidOreBuffer) +{ + const float initial[] = { 1.0f, 2.0f, 3.0f }; + auto buffer = GpuBuffer::create (device, GpuBufferType::vertex, initial, sizeof (initial)); + ASSERT_NE (buffer, nullptr); + + const float updated[] = { 4.0f, 5.0f }; + // Update fewer bytes than the buffer size — should succeed. + EXPECT_TRUE (device->updateBuffer (buffer, updated, sizeof (updated))); +} + +TEST_F (GpuDeviceErrorTests, UpdateBufferFailsForStorageBuffer) +{ + // Storage buffers are not supported by the base GpuDevice::createBuffer. + const float data[] = { 1.0f, 2.0f }; + auto buffer = GpuBuffer::create (device, GpuBufferType::storage, data, sizeof (data)); + // Storage buffer creation returns null from the base implementation. + EXPECT_EQ (buffer, nullptr); +} + +// --------------------------------------------------------------------------- +// createBuffer — error paths +// --------------------------------------------------------------------------- + +TEST_F (GpuDeviceErrorTests, CreateBufferWithNullDataReturnsNull) +{ + EXPECT_EQ (device->createBuffer (GpuBufferType::vertex, nullptr, 16), nullptr); +} + +TEST_F (GpuDeviceErrorTests, CreateBufferWithZeroSizeReturnsNull) +{ + const float data[] = { 1.0f }; + EXPECT_EQ (device->createBuffer (GpuBufferType::vertex, data, 0), nullptr); +} + +TEST_F (GpuDeviceErrorTests, CreateBufferWithStorageTypeReturnsNull) +{ + const float data[] = { 1.0f }; + // Storage buffers not supported by base implementation. + EXPECT_EQ (device->createBuffer (GpuBufferType::storage, data, sizeof (data)), nullptr); +} + +// --------------------------------------------------------------------------- +// isComputeAvailable +// --------------------------------------------------------------------------- + +TEST_F (GpuDeviceErrorTests, IsComputeAvailableOnHeadlessReturnsFalse) +{ + EXPECT_FALSE (device->isComputeAvailable()); +} + +// --------------------------------------------------------------------------- +// getPlatform +// --------------------------------------------------------------------------- + +TEST_F (GpuDeviceErrorTests, GetPlatformOnHeadlessReturnsHeadless) +{ + EXPECT_EQ (device->getPlatform(), GpuPlatform::Headless); +} + +// --------------------------------------------------------------------------- +// GpuBuffer — additional coverage +// --------------------------------------------------------------------------- + +class GpuBufferErrorTests : public ::testing::Test +{ +protected: + void SetUp() override + { + device = GpuDevice::create (GpuPlatform::Headless, {}); + ASSERT_NE (device, nullptr); + } + + GpuDevice::Ptr device; +}; + +TEST_F (GpuBufferErrorTests, CreateWithUniformTypeSucceeds) +{ + const float data[] = { 1.0f, 2.0f }; + auto buf = GpuBuffer::create (device, GpuBufferType::uniform, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + EXPECT_EQ (buf->getType(), GpuBufferType::uniform); + EXPECT_EQ (buf->getSizeInBytes(), sizeof (data)); + EXPECT_TRUE (buf->isValid()); +} + +TEST_F (GpuBufferErrorTests, CreateWithIndexTypeSucceeds) +{ + const uint16_t data[] = { 0, 1, 2, 3, 4, 5 }; + auto buf = GpuBuffer::create (device, GpuBufferType::index, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + EXPECT_EQ (buf->getType(), GpuBufferType::index); + EXPECT_EQ (buf->getSizeInBytes(), sizeof (data)); +} + +TEST_F (GpuBufferErrorTests, DefaultConstructedBufferIsInvalid) +{ + GpuBuffer::Ptr nullBuf; + EXPECT_EQ (nullBuf, nullptr); +} diff --git a/tests/yup_rhi/yup_GpuPipeline.cpp b/tests/yup_rhi/yup_GpuPipeline.cpp index 202abe63a..e9646bd28 100644 --- a/tests/yup_rhi/yup_GpuPipeline.cpp +++ b/tests/yup_rhi/yup_GpuPipeline.cpp @@ -103,6 +103,126 @@ TEST (ShaderBindingMapTests, TextureAndSamplerAreEncoded) EXPECT_FALSE (blob.empty()); } +TEST (ShaderBindingMapTests, StorageBufferIsEncoded) +{ + ShaderReflection refl; + + ShaderReflection::ResourceBinding sb; + sb.name = "OutputBuffer"; + sb.set = 0; + sb.binding = 0; + sb.backendSlot = 0; + refl.storageBuffers.push_back (sb); + + auto blob = makeShaderBindingMapBlob (refl, ShaderStage::vertex); + EXPECT_FALSE (blob.empty()); +} + +TEST (ShaderBindingMapTests, StorageImageIsEncoded) +{ + ShaderReflection refl; + + ShaderReflection::ResourceBinding si; + si.name = "OutputImage"; + si.set = 0; + si.binding = 0; + si.backendSlot = 0; + refl.storageImages.push_back (si); + + auto blob = makeShaderBindingMapBlob (refl, ShaderStage::fragment); + EXPECT_FALSE (blob.empty()); +} + +TEST (ShaderBindingMapTests, ComputeStageUsesCorrectStageMask) +{ + ShaderReflection refl; + + ShaderReflection::ResourceBinding ub; + ub.name = "Uniforms"; + ub.set = 0; + ub.binding = 0; + ub.backendSlot = 0; + refl.uniformBuffers.push_back (ub); + + auto blob = makeShaderBindingMapBlob (refl, ShaderStage::compute); + EXPECT_FALSE (blob.empty()); +} + +TEST (ShaderBindingMapTests, GLFixupBlobEmptyReflectionReturnsEmpty) +{ + ShaderReflection refl; + auto blob = makeGLFixupBlob (refl); + EXPECT_TRUE (blob.empty()); +} + +TEST (ShaderBindingMapTests, GLFixupBlobEncodesUniformBuffers) +{ + ShaderReflection refl; + + ShaderReflection::ResourceBinding ub; + ub.name = "Uniforms"; + ub.set = 0; + ub.binding = 0; + ub.backendSlot = 0; + refl.uniformBuffers.push_back (ub); + + auto blob = makeGLFixupBlob (refl); + EXPECT_FALSE (blob.empty()); + + // Version byte + EXPECT_EQ (blob[0], 1u); + // Entry count (uint16 LE) = 1 + EXPECT_EQ (blob[1], 1u); + EXPECT_EQ (blob[2], 0u); + // Kind = UBO block (0) + EXPECT_EQ (blob[3], 0u); + // Slot = 0 + EXPECT_EQ (blob[4], 0u); + // Name length follows, after that the name bytes +} + +TEST (ShaderBindingMapTests, GLFixupBlobEncodesCombinedSamplers) +{ + ShaderReflection refl; + + ShaderReflection::GLCombinedSampler cs; + cs.name = "texSampler"; + cs.textureSlot = 2; + refl.glCombinedSamplers.push_back (cs); + + auto blob = makeGLFixupBlob (refl); + EXPECT_FALSE (blob.empty()); + + // Kind = sampler uniform (1) + EXPECT_EQ (blob[3], 1u); + // Slot = 2 + EXPECT_EQ (blob[4], 2u); +} + +TEST (ShaderBindingMapTests, GLFixupBlobEncodesBothUniformBuffersAndSamplers) +{ + ShaderReflection refl; + + ShaderReflection::ResourceBinding ub; + ub.name = "Uniforms"; + ub.set = 0; + ub.binding = 0; + ub.backendSlot = 0; + refl.uniformBuffers.push_back (ub); + + ShaderReflection::GLCombinedSampler cs; + cs.name = "texSampler"; + cs.textureSlot = 1; + refl.glCombinedSamplers.push_back (cs); + + auto blob = makeGLFixupBlob (refl); + EXPECT_FALSE (blob.empty()); + + // Entry count = 2 + EXPECT_EQ (blob[1], 2u); + EXPECT_EQ (blob[2], 0u); +} + // --------------------------------------------------------------------------- // GpuBuffer::create — validation and null paths @@ -113,21 +233,30 @@ TEST_F (GpuPipelineTests, GpuBufferCreateHeadlessReturnsNull) EXPECT_EQ (GpuBuffer::create (*context, GpuBufferType::vertex, verts, sizeof (verts)), nullptr); } -TEST_F (GpuPipelineTests, GpuBufferCreateWithNullDataReturnsNull) +TEST (GpuBufferDefaults, DefaultPtrIsNull) +{ + GpuBuffer::Ptr b; + EXPECT_EQ (b, nullptr); +} + +// --------------------------------------------------------------------------- +// GpuDevice::updateBuffer — base contract (default returns false) + +TEST_F (GpuPipelineTests, UpdateBufferWithNullBufferReturnsFalse) { - EXPECT_EQ (GpuBuffer::create (*context, GpuBufferType::vertex, nullptr, 16), nullptr); + const float data[] = { 1.0f, 2.0f }; + EXPECT_FALSE (context->updateBuffer (nullptr, data, sizeof (data))); } -TEST_F (GpuPipelineTests, GpuBufferCreateWithZeroSizeReturnsNull) +TEST_F (GpuPipelineTests, UpdateBufferWithNullDataReturnsFalse) { - const float verts[] = { 0.0f }; - EXPECT_EQ (GpuBuffer::create (*context, GpuBufferType::vertex, verts, 0), nullptr); + EXPECT_FALSE (context->updateBuffer (nullptr, nullptr, 16)); } -TEST (GpuBufferDefaults, DefaultPtrIsNull) +TEST_F (GpuPipelineTests, UpdateBufferWithZeroSizeReturnsFalse) { - GpuBuffer::Ptr b; - EXPECT_EQ (b, nullptr); + const float data[] = { 1.0f }; + EXPECT_FALSE (context->updateBuffer (nullptr, data, 0)); } // --------------------------------------------------------------------------- diff --git a/tests/yup_rhi/yup_GpuPipelineMocked.cpp b/tests/yup_rhi/yup_GpuPipelineMocked.cpp index 975055f81..91bf4755d 100644 --- a/tests/yup_rhi/yup_GpuPipelineMocked.cpp +++ b/tests/yup_rhi/yup_GpuPipelineMocked.cpp @@ -26,6 +26,7 @@ using namespace yup; using ::testing::_; +using ::testing::Invoke; using ::testing::NiceMock; using ::testing::Return; using ::testing::ReturnNull; @@ -416,10 +417,140 @@ TEST_F (GpuBufferMockTests, CreateReturnsNullWhenMakeBufferFails) EXPECT_EQ (buf, nullptr); } +TEST_F (GpuBufferMockTests, UpdateBufferOnVertexBufferSucceeds) +{ + auto oreBuf = rive::make_rcp(); + EXPECT_CALL (*mockOreCtx, makeBuffer (_)) + .WillOnce (Return (oreBuf)); + EXPECT_CALL (*oreBuf, update (_, _, _)); + + const float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + auto buf = GpuBuffer::create (ctx, GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + + const float newData[] = { 5.0f, 6.0f, 7.0f, 8.0f }; + EXPECT_TRUE (ctx->updateBuffer (buf, newData, sizeof (newData))); +} + +TEST_F (GpuBufferMockTests, CreateFailsWithStorageType) +{ + const float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + auto buf = GpuBuffer::create (ctx, GpuBufferType::storage, data, sizeof (data)); + EXPECT_EQ (buf, nullptr); +} + // ============================================================================== -// GpuFrame mock-based tests +// GpuDevice — mock-based createBuffer / updateBuffer tests // ============================================================================== +class GpuDeviceMockTests : public ::testing::Test +{ +protected: + void SetUp() override + { + mockOreCtx = std::make_unique>(); + ctx = new OreInjectedGpuDevice (mockOreCtx.get()); + } + + std::unique_ptr> mockOreCtx; + GpuDevice::Ptr ctx; +}; + +TEST_F (GpuDeviceMockTests, CreateBufferVertexSucceeds) +{ + auto oreBuf = rive::make_rcp(); + EXPECT_CALL (*mockOreCtx, makeBuffer (_)) + .WillOnce (Return (oreBuf)); + + const float data[] = { 1.0f, 2.0f }; + auto buf = ctx->createBuffer (GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + EXPECT_EQ (buf->getType(), GpuBufferType::vertex); + EXPECT_TRUE (buf->isValid()); +} + +TEST_F (GpuDeviceMockTests, CreateBufferIndexSucceeds) +{ + auto oreBuf = rive::make_rcp(); + EXPECT_CALL (*mockOreCtx, makeBuffer (_)) + .WillOnce (Return (oreBuf)); + + const uint16_t data[] = { 0, 1, 2 }; + auto buf = ctx->createBuffer (GpuBufferType::index, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + EXPECT_EQ (buf->getType(), GpuBufferType::index); + EXPECT_TRUE (buf->isValid()); +} + +TEST_F (GpuDeviceMockTests, CreateBufferUniformSucceeds) +{ + auto oreBuf = rive::make_rcp(); + EXPECT_CALL (*mockOreCtx, makeBuffer (_)) + .WillOnce (Return (oreBuf)); + + const int data[] = { 42, 43 }; + auto buf = ctx->createBuffer (GpuBufferType::uniform, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + EXPECT_EQ (buf->getType(), GpuBufferType::uniform); + EXPECT_TRUE (buf->isValid()); +} + +TEST_F (GpuDeviceMockTests, CreateBufferReturnsNullWhenOreMakeBufferFails) +{ + EXPECT_CALL (*mockOreCtx, makeBuffer (_)) + .WillOnce (ReturnNull()); + + const float data[] = { 1.0f }; + auto buf = ctx->createBuffer (GpuBufferType::vertex, data, sizeof (data)); + EXPECT_EQ (buf, nullptr); +} + +TEST_F (GpuDeviceMockTests, UpdateBufferWithNullBufferReturnsFalse) +{ + const float data[] = { 1.0f }; + EXPECT_FALSE (ctx->updateBuffer (nullptr, data, sizeof (data))); +} + +TEST_F (GpuDeviceMockTests, UpdateBufferWithNullDataReturnsFalse) +{ + EXPECT_FALSE (ctx->updateBuffer (nullptr, nullptr, 0)); +} + +TEST_F (GpuDeviceMockTests, UpdateBufferWithZeroSizeReturnsFalse) +{ + const float data[] = { 1.0f }; + EXPECT_FALSE (ctx->updateBuffer (nullptr, data, 0)); +} + +TEST_F (GpuDeviceMockTests, UpdateBufferOnValidOreBackedBufferSucceeds) +{ + auto oreBuf = rive::make_rcp(); + EXPECT_CALL (*mockOreCtx, makeBuffer (_)) + .WillOnce (Return (oreBuf)); + EXPECT_CALL (*oreBuf, update (_, _, _)); + + const float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + auto buf = ctx->createBuffer (GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + + const float newData[] = { 5.0f, 6.0f, 7.0f, 8.0f }; + EXPECT_TRUE (ctx->updateBuffer (buf, newData, sizeof (newData))); +} + +TEST_F (GpuDeviceMockTests, UpdateBufferLargerThanOriginalReturnsFalse) +{ + auto oreBuf = rive::make_rcp(); + EXPECT_CALL (*mockOreCtx, makeBuffer (_)) + .WillOnce (Return (oreBuf)); + + const float data[] = { 1.0f }; + auto buf = ctx->createBuffer (GpuBufferType::vertex, data, sizeof (data)); + ASSERT_NE (buf, nullptr); + + const float largerData[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + EXPECT_FALSE (ctx->updateBuffer (buf, largerData, sizeof (largerData))); +} + class GpuFrameMockTests : public ::testing::Test { protected: @@ -441,6 +572,36 @@ TEST_F (GpuFrameMockTests, BeginCallsOreBeginFrame) EXPECT_TRUE (frame.isValid()); } +TEST_F (GpuFrameMockTests, BeginWithNullGpuContextReturnsInvalidFrame) +{ + auto headless = GpuDevice::create (GpuPlatform::Headless, {}); + ASSERT_NE (headless, nullptr); + + auto frame = GpuFrame::begin (headless); + EXPECT_FALSE (frame.isValid()); +} + +TEST_F (GpuFrameMockTests, MoveConstructionFromValidFrameTransfersState) +{ + EXPECT_CALL (*mockOreCtx, beginFrame (_)); + + auto src = GpuFrame::begin (ctx); + ASSERT_TRUE (src.isValid()); + + GpuFrame dst (std::move (src)); + EXPECT_TRUE (dst.isValid()); + EXPECT_FALSE (src.isValid()); +} + +TEST_F (GpuFrameMockTests, WaitForGpuOnInvalidFrameDoesNotCrash) +{ + auto headless = GpuDevice::create (GpuPlatform::Headless, {}); + auto frame = GpuFrame::begin (headless); + EXPECT_FALSE (frame.isValid()); + + EXPECT_NO_THROW (frame.waitForGPU()); +} + TEST_F (GpuFrameMockTests, SubmitCallsOreEndFrame) { // beginFrame + endFrame @@ -475,11 +636,31 @@ TEST_F (GpuFrameMockTests, WaitForGpuCallsOreWaitForGPU) frame.waitForGPU(); } -TEST_F (GpuFrameMockTests, DestructorSubmitsIfNotSubmitted) +TEST_F (GpuFrameMockTests, WaitForGpuIsIdempotent) +{ + EXPECT_CALL (*mockOreCtx, beginFrame (_)); + EXPECT_CALL (*mockOreCtx, endFrame()); + + // A GPU sync is expensive, and the destructor waits too, so repeated waits must + // collapse into the single stall the first one already paid for. + EXPECT_CALL (*mockOreCtx, waitForGPU()); + + auto frame = GpuFrame::begin (ctx); + ASSERT_TRUE (frame.isValid()); + frame.submit(); + frame.waitForGPU(); + frame.waitForGPU(); +} + +TEST_F (GpuFrameMockTests, DestructorSubmitsAndWaitsIfNotSubmitted) { EXPECT_CALL (*mockOreCtx, beginFrame (_)); EXPECT_CALL (*mockOreCtx, endFrame()); + // The encoded passes reference this frame's transient resources by raw pointer, + // and destruction releases them, so the destructor has to drain the GPU first. + EXPECT_CALL (*mockOreCtx, waitForGPU()); + { auto frame = GpuFrame::begin (ctx); ASSERT_TRUE (frame.isValid()); @@ -818,6 +999,300 @@ TEST_F (GpuRenderPassMockTests, DrawEndToEndWithValidPipeline) valid.submit(); } +TEST_F (GpuRenderPassMockTests, DeclaredSamplerIsCreatedWhileCompilingAndReusedByEveryDraw) +{ + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); + + auto bgl = rive::make_rcp(); + bgl->addEntry (1, rive::ore::BindingKind::sampler); + + EXPECT_CALL (*mockOreCtx, makeShaderModule (_)) + .WillOnce (Return (makeShaderModuleWithBindingMap())) + .WillOnce (Return (makeShaderModuleWithBindingMap())); + EXPECT_CALL (*mockOreCtx, makeBindGroupLayout (_)) + .WillOnce (Return (bgl)); + EXPECT_CALL (*mockOreCtx, makePipeline (_, _)) + .WillOnce (Return (rive::make_rcp())); + + // The one declared sampler binding is filled by a sampler created up front, + // so no draw ever asks for another one. + EXPECT_CALL (*mockOreCtx, makeSampler (_)) + .Times (1) + .WillOnce (Return (rive::make_rcp())); + + auto compileResult = GpuPipeline::compile (ctx, makeShaderSource ("// VS"), makeShaderSource ("// FS")); + ASSERT_TRUE (compileResult.wasOk()); + auto compiled = compileResult.getValue(); + + ON_CALL (*mockOreCtx, wrapRiveTexture (_, _, _)) + .WillByDefault (Invoke ([] (rive::gpu::Texture*, uint32_t, uint32_t) + { + return rive::make_rcp(); + })); + ON_CALL (*mockOreCtx, beginRenderPass (_, _)) + .WillByDefault (Invoke ([] (const rive::ore::RenderPassDesc&, std::string*) + { + return std::unique_ptr (new NiceMock()); + })); + ON_CALL (*mockOreCtx, makeBindGroup (_)) + .WillByDefault (Invoke ([] (const rive::ore::BindGroupDesc& desc) + { + // The sampler slot is always populated from the pipeline. + EXPECT_EQ (1u, desc.samplerCount); + return rive::make_rcp(); + })); + + auto frame = makeValidFrame(); + + for (int draw = 0; draw < 3; ++draw) + { + auto pass = target->beginRenderPass (frame); + ASSERT_TRUE (pass.isValid()); + pass.setPipeline (compiled); + EXPECT_TRUE (pass.draw (3)); + pass.finish(); + } + + frame.submit(); +} + +TEST_F (GpuRenderPassMockTests, UniformBuffersAreRecycledAcrossFrames) +{ + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); + + EXPECT_CALL (*mockOreCtx, makeShaderModule (_)) + .WillOnce (Return (makeShaderModuleWithBindingMap())) + .WillOnce (Return (makeShaderModuleWithBindingMap())); + EXPECT_CALL (*mockOreCtx, makeBindGroupLayout (_)) + .WillOnce (Return (rive::make_rcp())); + EXPECT_CALL (*mockOreCtx, makePipeline (_, _)) + .WillOnce (Return (rive::make_rcp())); + + // Two draws per frame need two distinct buffers, but the second frame gets + // both of them back from the pool - so only two are ever created. + EXPECT_CALL (*mockOreCtx, makeBuffer (_)) + .Times (2) + .WillRepeatedly (Invoke ([] (const rive::ore::BufferDesc& desc) + { + return rive::make_rcp (desc.size); + })); + + auto compileResult = GpuPipeline::compile (ctx, makeShaderSource ("// VS"), makeShaderSource ("// FS")); + ASSERT_TRUE (compileResult.wasOk()); + auto compiled = compileResult.getValue(); + + ON_CALL (*mockOreCtx, wrapRiveTexture (_, _, _)) + .WillByDefault (Invoke ([] (rive::gpu::Texture*, uint32_t, uint32_t) + { + return rive::make_rcp(); + })); + ON_CALL (*mockOreCtx, beginRenderPass (_, _)) + .WillByDefault (Invoke ([] (const rive::ore::RenderPassDesc&, std::string*) + { + return std::unique_ptr (new NiceMock()); + })); + ON_CALL (*mockOreCtx, makeBindGroup (_)) + .WillByDefault (Return (rive::make_rcp())); + + const float uniforms[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; + + for (int frameIndex = 0; frameIndex < 2; ++frameIndex) + { + auto frame = makeValidFrame(); + + for (int draw = 0; draw < 2; ++draw) + { + auto pass = target->beginRenderPass (frame); + ASSERT_TRUE (pass.isValid()); + pass.setPipeline (compiled); + pass.setUniformBuffer (0, 0, uniforms, sizeof (uniforms)); + EXPECT_TRUE (pass.draw (3)); + pass.finish(); + } + + frame.submit(); + frame.waitForGPU(); + } +} + +TEST_F (GpuRenderPassMockTests, SetPipelineOnValidPassStoresPipeline) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto vsModule = makeShaderModuleWithBindingMap(); + auto fsModule = makeShaderModuleWithBindingMap(); + auto pipeline = rive::make_rcp(); + auto bgl = rive::make_rcp(); + + EXPECT_CALL (*mockOreCtx, makeShaderModule (_)) + .WillOnce (Return (vsModule)) + .WillOnce (Return (fsModule)); + EXPECT_CALL (*mockOreCtx, makeBindGroupLayout (_)) + .WillOnce (Return (bgl)); + EXPECT_CALL (*mockOreCtx, makePipeline (_, _)) + .WillOnce (Return (pipeline)); + + auto compileResult = GpuPipeline::compile (ctx, makeShaderSource ("// VS"), makeShaderSource ("// FS")); + ASSERT_TRUE (compileResult.wasOk()); + + auto valid = makeValidFrame(); + auto pass = canvas->beginRenderPass (valid); + ASSERT_TRUE (pass.isValid()); + + // Set pipeline twice — second call replaces the first. + EXPECT_NO_THROW ({ + pass.setPipeline (*compileResult.getValue()); + pass.setPipeline (*compileResult.getValue()); + }); + + pass.finish(); + valid.submit(); +} + +TEST_F (GpuRenderPassMockTests, SetTextureOnValidPassStoresAndReplacesBinding) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto valid = makeValidFrame(); + auto pass = canvas->beginRenderPass (valid); + ASSERT_TRUE (pass.isValid()); + + // Setting with null texture should not crash. + EXPECT_NO_THROW (pass.setTexture (0, 0, nullptr)); + + // Setting same group/binding again replaces. + EXPECT_NO_THROW (pass.setTexture (0, 0, nullptr)); + + pass.finish(); + valid.submit(); +} + +TEST_F (GpuRenderPassMockTests, SetUniformBufferOnValidPassStoresAndReplacesBinding) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto valid = makeValidFrame(); + auto pass = canvas->beginRenderPass (valid); + ASSERT_TRUE (pass.isValid()); + + float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + EXPECT_NO_THROW (pass.setUniformBuffer (0, 0, data, sizeof (data))); + + // Replacing same group/binding updates the data. + float newData[] = { 5.0f }; + EXPECT_NO_THROW (pass.setUniformBuffer (0, 0, newData, sizeof (newData))); + + pass.finish(); + valid.submit(); +} + +TEST_F (GpuRenderPassMockTests, SetVertexBufferOnValidPassStoresAndReplacesSlot) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto valid = makeValidFrame(); + auto pass = canvas->beginRenderPass (valid); + ASSERT_TRUE (pass.isValid()); + + EXPECT_NO_THROW (pass.setVertexBuffer (0, nullptr)); + + // Replace slot 0. + EXPECT_NO_THROW (pass.setVertexBuffer (0, nullptr)); + + pass.finish(); + valid.submit(); +} + +TEST_F (GpuRenderPassMockTests, SetIndexBufferOnValidPassStoresFormat) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto valid = makeValidFrame(); + auto pass = canvas->beginRenderPass (valid); + ASSERT_TRUE (pass.isValid()); + + EXPECT_NO_THROW (pass.setIndexBuffer (GpuIndexFormat::uint16, nullptr)); + EXPECT_NO_THROW (pass.setIndexBuffer (GpuIndexFormat::uint32, nullptr)); + + pass.finish(); + valid.submit(); +} + +TEST_F (GpuRenderPassMockTests, DrawWithoutPipelineReturnsFalse) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto valid = makeValidFrame(); + auto pass = canvas->beginRenderPass (valid); + ASSERT_TRUE (pass.isValid()); + + // No pipeline set — draw should fail gracefully. + EXPECT_FALSE (pass.draw (4)); + EXPECT_FALSE (pass.drawIndexed (6)); + + pass.finish(); + valid.submit(); +} + +TEST_F (GpuRenderPassMockTests, FinishOnValidPassReturnsTrueAndIsIdempotent) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto valid = makeValidFrame(); + auto pass = canvas->beginRenderPass (valid); + ASSERT_TRUE (pass.isValid()); + + EXPECT_TRUE (pass.finish()); + EXPECT_FALSE (pass.finish()); // Idempotent. +} + +TEST_F (GpuRenderPassMockTests, MoveConstructionFromValidPassClearsSource) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto valid = makeValidFrame(); + auto src = canvas->beginRenderPass (valid); + ASSERT_TRUE (src.isValid()); + + GpuRenderPass dst (std::move (src)); + EXPECT_TRUE (dst.isValid()); + EXPECT_FALSE (src.isValid()); // After move, src is empty. + + dst.finish(); + valid.submit(); +} + +TEST_F (GpuRenderPassMockTests, MoveAssignmentFromValidPassClearsSource) +{ + auto canvas = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (canvas, nullptr); + + auto valid = makeValidFrame(); + auto src = canvas->beginRenderPass (valid); + ASSERT_TRUE (src.isValid()); + + auto invalidSrc = makeInvalidFrame(); + auto dst = canvas->beginRenderPass (invalidSrc); + EXPECT_FALSE (dst.isValid()); + + dst = std::move (src); + EXPECT_TRUE (dst.isValid()); + EXPECT_FALSE (src.isValid()); + + dst.finish(); + valid.submit(); +} + #if YUP_ENABLE_SHADER_TRANSPILER // ============================================================================== diff --git a/tests/yup_rhi/yup_GpuTarget.cpp b/tests/yup_rhi/yup_GpuTarget.cpp index 3da3ab919..46defc4a9 100644 --- a/tests/yup_rhi/yup_GpuTarget.cpp +++ b/tests/yup_rhi/yup_GpuTarget.cpp @@ -24,6 +24,9 @@ #include using namespace yup; +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Return; class GpuTargetTests : public ::testing::Test { @@ -123,3 +126,82 @@ TEST_F (GpuTargetTests, DefaultPtrIsNull) GpuTarget::Ptr nullTarget; EXPECT_EQ (nullTarget, nullptr); } + +// ============================================================================== +// GpuTarget — mock-based tests (using OreAndTargetGpuDevice) +// ============================================================================== + +class GpuTargetMockTests : public ::testing::Test +{ +protected: + void SetUp() override + { + mockOreCtx = std::make_unique>(); + ctx = new OreAndTargetGpuDevice (mockOreCtx.get(), MockOffscreenTarget::withGpuTexture (256, 128)); + } + + std::unique_ptr> mockOreCtx; + GpuDevice::Ptr ctx; +}; + +TEST_F (GpuTargetMockTests, CreateWithValidDimensionsSucceeds) +{ + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); + EXPECT_GE (target->getWidth(), 0); + EXPECT_GE (target->getHeight(), 0); +} + +TEST_F (GpuTargetMockTests, AsTextureReturnsValidTexture) +{ + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); + + // First call creates the texture via render canvas path. + auto tex = target->asTexture(); + ASSERT_NE (tex, nullptr); + EXPECT_TRUE (tex->isValid()); + + // Second call returns cached result. + auto tex2 = target->asTexture(); + EXPECT_EQ (tex, tex2); +} + +TEST_F (GpuTargetMockTests, ReadPixelsReturnsSuccess) +{ + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); + + std::vector buf (256 * 128 * 4); + // readOffscreenPixels delegates to the device; returns true for headless-based mock. + EXPECT_NO_THROW ({ target->readPixels (buf.data(), buf.size()); }); +} + +TEST_F (GpuTargetMockTests, BeginRenderPassWithValidFrameReturnsValidPass) +{ + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); + + EXPECT_CALL (*mockOreCtx, beginFrame (_)); + + auto frame = GpuFrame::begin (ctx); + ASSERT_TRUE (frame.isValid()); + + auto pass = target->beginRenderPass (frame); + EXPECT_TRUE (pass.isValid()); + + pass.finish(); + frame.submit(); +} + +TEST_F (GpuTargetMockTests, BeginRenderPassWithInvalidFrameReturnsInvalidPass) +{ + auto target = GpuTarget::create (ctx, 256, 128); + ASSERT_NE (target, nullptr); + + auto frame = GpuFrame::begin (GpuDevice::create (GpuPlatform::Headless, {})); + EXPECT_FALSE (frame.isValid()); + + auto pass = target->beginRenderPass (frame); + EXPECT_FALSE (pass.isValid()); +} diff --git a/tests/yup_shading/yup_WgslTranspiler.cpp b/tests/yup_shading/yup_WgslTranspiler.cpp index e3d2bd8b4..e9d4cea6e 100644 --- a/tests/yup_shading/yup_WgslTranspiler.cpp +++ b/tests/yup_shading/yup_WgslTranspiler.cpp @@ -583,6 +583,51 @@ void main() } )glsl"; +constexpr const char* kUnnamedUniformBlock = R"glsl( +#version 450 +layout(set = 0, binding = 0) uniform { + float value; +}; +void main() +{ + float x = value; +} +)glsl"; + +constexpr const char* kUnnamedBufferBlock = R"glsl( +#version 450 +layout(set = 0, binding = 0) buffer { + float value; +}; +void main() +{ + value = 1.0; +} +)glsl"; + +constexpr const char* kFunctionNoParamReassignment = R"glsl( +#version 450 +float add(float a, float b) { return a + b; } +float mul(float a, float b) { return a * b; } +void main() +{ + float x = add(1.0, 2.0) * mul(3.0, 4.0); +} +)glsl"; + +constexpr const char* kIfElseStatement = R"glsl( +#version 450 +void main() +{ + float x; + if (true) { + x = 1.0; + } else { + x = 2.0; + } +} +)glsl"; + constexpr const char* kFragmentNoInputs = R"glsl( #version 450 layout(location = 0) out vec4 outColor; @@ -703,6 +748,53 @@ void main() } )glsl"; +// A uniform block whose members share one declaration, as post-process shaders +// commonly write their parameter block. +constexpr const char* kUniformBlockCommaSeparatedMembers = R"glsl( +#version 450 +layout(set = 0, binding = 0) uniform texture2D u_tex; +layout(set = 0, binding = 1) uniform sampler u_samp; +layout(set = 0, binding = 2) uniform Params { float s, r, rx, ry, dx, dy, pad0, pad1; } p; +layout(location = 0) out vec4 fragColor; + +void main() +{ + vec2 uv = gl_FragCoord.xy / vec2(p.rx, p.ry); + fragColor = texture(sampler2D(u_tex, u_samp), uv + vec2(p.dx, p.dy) * p.s); +} +)glsl"; + +// Comma-separated members in a plain struct, where a declarator also carries its +// own array specifier. The struct is never instantiated: the parser does not +// resolve user-declared struct names as type names inside a function body, so +// `Bundle bundle;` would fail for reasons unrelated to the member list. +constexpr const char* kStructCommaSeparatedMembers = R"glsl( +#version 450 +struct Bundle { + float a, b, weights[4]; + vec2 offset, scale; +}; + +void main() +{ +} +)glsl"; + +//============================================================================== +// AST helpers +//============================================================================== + +/** Finds a named struct or interface block in a parsed translation unit. */ +const wgsl::StructSpecifier* findStruct (const wgsl::TranslationUnit& unit, const std::string& name) +{ + for (const auto& external : unit.declarations) + if (const auto* declaration = std::get_if (&external)) + if (declaration->structSpecifier != nullptr && declaration->structSpecifier->name == name) + return declaration->structSpecifier.get(); + + return nullptr; +} + } // namespace //============================================================================== @@ -765,6 +857,61 @@ TEST_F (WgslParserTests, StructDeclaration) ASSERT_TRUE (r.wasOk()); } +TEST_F (WgslParserTests, NamedBlockWithoutInstanceName) +{ + const char* src = R"glsl( +layout(std140, binding = 0) uniform BlockName { + float value; + vec3 color; +}; +void main() { float x = value + color.r; } +)glsl"; + auto r = parse (src); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + // Parsed as a Declaration with structSpecifier + qualifier, no initDeclaratorList +} + +TEST_F (WgslParserTests, NamedBufferBlockWithoutInstanceName) +{ + const char* src = R"glsl( +layout(std430, binding = 0) buffer StorageBlock { + float data; +}; +void main() { data = 1.0; } +)glsl"; + auto r = parse (src); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); +} + +TEST_F (WgslParserTests, StructWithMultipleFields) +{ + const char* src = R"glsl( +struct Params { + float scale; + vec3 offset; + vec4 color; + int flags; +}; +void main() { Params p; } +)glsl"; + auto r = parse (src); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + // Exercises the "Parse fields" while loop + user-defined type resolution +} + +TEST_F (WgslParserTests, NamedBlockWithInstanceName) +{ + const char* src = R"glsl( +layout(std140, binding = 0) uniform Data { + float value; + vec3 color; +} u; +void main() { float x = u.value; } +)glsl"; + auto r = parse (src); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); +} + TEST_F (WgslParserTests, OutInoutParameters) { auto r = parse (kOutInoutParams); @@ -813,6 +960,36 @@ TEST_F (WgslParserTests, AnonymousUniformBlock) ASSERT_TRUE (r.wasOk()); } +TEST_F (WgslParserTests, UniformBlockWithCommaSeparatedMembers) +{ + auto r = parse (kUniformBlockCommaSeparatedMembers); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + + const auto* params = findStruct (r.getReference(), "Params"); + ASSERT_NE (nullptr, params); + ASSERT_EQ (8u, params->fields.size()); + EXPECT_EQ ("s", params->fields.front().name); + EXPECT_EQ ("pad1", params->fields.back().name); +} + +TEST_F (WgslParserTests, StructWithCommaSeparatedMembers) +{ + auto r = parse (kStructCommaSeparatedMembers); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + + const auto* bundle = findStruct (r.getReference(), "Bundle"); + ASSERT_NE (nullptr, bundle); + ASSERT_EQ (5u, bundle->fields.size()); + EXPECT_EQ ("a", bundle->fields[0].name); + EXPECT_EQ ("b", bundle->fields[1].name); + EXPECT_EQ ("offset", bundle->fields[3].name); + + // An array specifier binds to its own declarator, not to the shared base type. + EXPECT_EQ ("weights", bundle->fields[2].name); + EXPECT_EQ (1u, bundle->fields[2].type.arraySpecifiers.size()); + EXPECT_TRUE (bundle->fields[1].type.arraySpecifiers.empty()); +} + TEST_F (WgslParserTests, ErrorOnMalformedInput) { auto r = parse ("void main( {}"); @@ -1429,6 +1606,55 @@ TEST_F (WgslLoweringTests, OutInoutParametersProcessed) // Should succeed — out/inout params are lowered to pointer equivalents } +TEST_F (WgslLoweringTests, UnnamedUniformBlockHasResources) +{ + auto r = lower (kUnnamedUniformBlock, ShaderStage::fragment); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + + auto& resources = r.getReference().resources; + EXPECT_GE (resources.size(), 1u); + + bool foundValue = false; + for (auto& res : resources) + { + if (res.name == "value") + { + foundValue = true; + EXPECT_EQ (res.group, 0u); + EXPECT_EQ (res.binding, 0u); + } + } + EXPECT_TRUE (foundValue); +} + +TEST_F (WgslLoweringTests, UnnamedBufferBlockHasResources) +{ + auto r = lower (kUnnamedBufferBlock, ShaderStage::fragment); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + + auto& resources = r.getReference().resources; + EXPECT_GE (resources.size(), 1u); + + bool foundValue = false; + for (auto& res : resources) + { + if (res.name == "value") + { + foundValue = true; + EXPECT_EQ (res.group, 0u); + EXPECT_EQ (res.binding, 0u); + } + } + EXPECT_TRUE (foundValue); +} + +TEST_F (WgslLoweringTests, FunctionWithNoReassignedParamsSucceeds) +{ + auto r = lower (kFunctionNoParamReassignment, ShaderStage::fragment); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + // shadowReassignedParams exits early when no parameter is reassigned +} + //============================================================================== // Emitter Golden Tests — WGSL 1.0 output (Task 5.2) //============================================================================== @@ -1613,6 +1839,45 @@ TEST_F (WgslEmitterGoldenTests, UBOBecomesUniformVar) EXPECT_TRUE (wgsl.contains ("var")); } +TEST_F (WgslEmitterGoldenTests, UnnamedUniformBlockEmitsFlatVars) +{ + auto r = transpile (kUnnamedUniformBlock, ShaderStage::fragment); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + auto wgsl = r.getValue(); + + // Unnamed interface block fields are emitted as flat global variables + EXPECT_TRUE (wgsl.contains ("var")) << wgsl; + EXPECT_TRUE (wgsl.contains ("value: f32")) << wgsl; + EXPECT_TRUE (wgsl.contains ("@group(0)")) << wgsl; + EXPECT_TRUE (wgsl.contains ("@binding(0)")) << wgsl; + // Should NOT contain a struct wrapping the fields + EXPECT_FALSE (wgsl.contains ("struct {")) << wgsl; +} + +TEST_F (WgslEmitterGoldenTests, UnnamedBufferBlockEmitsStorageVars) +{ + auto r = transpile (kUnnamedBufferBlock, ShaderStage::fragment); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + auto wgsl = r.getValue(); + + EXPECT_TRUE (wgsl.contains ("var")) << wgsl; + + for (auto* member : { "s", "r", "rx", "ry", "dx", "dy", "pad0", "pad1" }) + EXPECT_TRUE (wgsl.contains (String (member) + ": f32")) << member << " missing from:\n" + << wgsl; +} + TEST_F (WgslEmitterGoldenTests, FloatLiteralFormats) { const char* src = "void main() { float x = 5.0; }"; @@ -1645,6 +1910,39 @@ TEST_F (WgslEmitterGoldenTests, DiscardStatement) EXPECT_TRUE (wgsl.contains ("discard")); } +TEST_F (WgslEmitterGoldenTests, IfElseBranchEmitted) +{ + auto r = transpile (kIfElseStatement, ShaderStage::fragment); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + auto wgsl = r.getValue(); + + EXPECT_TRUE (wgsl.contains ("else")) << wgsl; + EXPECT_TRUE (wgsl.contains ("1.0")) << wgsl; + EXPECT_TRUE (wgsl.contains ("2.0")) << wgsl; +} + +TEST_F (WgslEmitterGoldenTests, IfElseIfChainEmitted) +{ + const char* src = R"glsl( +void main() { + float x; + if (true) { + x = 1.0; + } else if (false) { + x = 2.0; + } else { + x = 3.0; + } +} +)glsl"; + auto r = transpile (src, ShaderStage::fragment); + ASSERT_TRUE (r.wasOk()) << r.getErrorMessage(); + auto wgsl = r.getValue(); + + EXPECT_TRUE (wgsl.contains ("else")) << wgsl; + // else if chain preserves the nesting +} + TEST_F (WgslEmitterGoldenTests, ReturnStatement) { const char* src = "float foo() { return 1.0; } void main() { float x = foo(); return; }"; diff --git a/tests/yup_simd/yup_ColorVectorOperations.cpp b/tests/yup_simd/yup_ColorVectorOperations.cpp index 8e0a24280..3776a9b23 100644 --- a/tests/yup_simd/yup_ColorVectorOperations.cpp +++ b/tests/yup_simd/yup_ColorVectorOperations.cpp @@ -25,6 +25,25 @@ using namespace yup; +namespace +{ +constexpr uint32_t makeRGBA (uint8_t r, uint8_t g, uint8_t b, uint8_t a) noexcept +{ + return static_cast (r) + | (static_cast (g) << 8) + | (static_cast (b) << 16) + | (static_cast (a) << 24); +} + +constexpr uint32_t makeBGRA (uint8_t b, uint8_t g, uint8_t r, uint8_t a) noexcept +{ + return static_cast (b) + | (static_cast (g) << 8) + | (static_cast (r) << 16) + | (static_cast (a) << 24); +} +} // namespace + // ============================================================================== // premultiplyARGB tests // ============================================================================== @@ -83,55 +102,159 @@ TEST (ColorVectorOpsTests, PremultiplyARGBZeroPixels) EXPECT_EQ (pixels[0], 0xdeadbeefu); } +TEST (ColorVectorOpsTests, PremultiplyARGBLargePixelCount) +{ + // Exercise SIMD path with 100 pixels — well past the scalar tail boundary. + constexpr int kCount = 100; + uint32_t pixels[kCount]; + for (int i = 0; i < kCount; ++i) + { + const uint32_t r = (uint32_t) ((i * 3) & 0xff); + const uint32_t g = (uint32_t) ((i * 5) & 0xff); + const uint32_t b = (uint32_t) ((i * 7) & 0xff); + const uint32_t a = (uint32_t) ((i * 11) & 0xff); + pixels[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + + ColorVectorOperations::premultiplyARGB (pixels, kCount); + + for (int i = 0; i < kCount; ++i) + { + const uint32_t a = (uint32_t) ((i * 11) & 0xff); + const uint32_t expectedR = ((uint32_t) ((i * 3) & 0xff) * a + 127u) / 255u; + const uint32_t expectedG = ((uint32_t) ((i * 5) & 0xff) * a + 127u) / 255u; + const uint32_t expectedB = ((uint32_t) ((i * 7) & 0xff) * a + 127u) / 255u; + + EXPECT_EQ ((pixels[i] >> 24) & 0xffu, a); + EXPECT_EQ ((pixels[i] >> 16) & 0xffu, expectedR); + EXPECT_EQ ((pixels[i] >> 8) & 0xffu, expectedG); + EXPECT_EQ (pixels[i] & 0xffu, expectedB); + } +} + +TEST (ColorVectorOpsTests, PremultiplyARGBOddPixelCount) +{ + // 5 pixels — not a multiple of 4, exercises both SIMD and scalar tail. + uint32_t pixels[] = { + (0x80u << 24) | (0xffu << 16) | (0x80u << 8) | 0x80u, + (0x40u << 24) | (0x40u << 16) | (0x40u << 8) | 0x40u, + (0xffu << 24) | (0x10u << 16) | (0x20u << 8) | 0x30u, + (0x00u << 24) | (0xaau << 16) | (0xbbu << 8) | 0xccu, + (0xc0u << 24) | (0x22u << 16) | (0x44u << 8) | 0x66u + }; + + ColorVectorOperations::premultiplyARGB (pixels, 5); + + for (int i = 0; i < 5; ++i) + { + const uint32_t a = (pixels[i] >> 24) & 0xffu; + const uint32_t r = (pixels[i] >> 16) & 0xffu; + const uint32_t g = (pixels[i] >> 8) & 0xffu; + const uint32_t b = pixels[i] & 0xffu; + + // Verify premultiplied channel ≤ min(original channel, alpha) + EXPECT_LE (r, a); + EXPECT_LE (g, a); + EXPECT_LE (b, a); + } +} + // ============================================================================== // premultiplyRGBA tests // ============================================================================== TEST (ColorVectorOpsTests, PremultiplyRGBAMatchesScalarReference) { - uint8 pixels[] = { 255, 128, 64, 128, 1, 2, 3, 255, 1, 2, 3, 0 }; + // RGBA pixels as uint32: [R,G,B,A] with premultiply + uint32_t pixels[] = { + makeRGBA (255, 128, 64, 128), + makeRGBA (1, 2, 3, 255), + makeRGBA (1, 2, 3, 0) + }; ColorVectorOperations::premultiplyRGBA (pixels, 3); - EXPECT_EQ (pixels[0], 128); - EXPECT_EQ (pixels[1], 64); - EXPECT_EQ (pixels[2], 32); - EXPECT_EQ (pixels[3], 128); - EXPECT_EQ (pixels[4], 1); - EXPECT_EQ (pixels[5], 2); - EXPECT_EQ (pixels[6], 3); - EXPECT_EQ (pixels[7], 255); - EXPECT_EQ (pixels[8], 0); - EXPECT_EQ (pixels[9], 0); - EXPECT_EQ (pixels[10], 0); - EXPECT_EQ (pixels[11], 0); + EXPECT_EQ (pixels[0], makeRGBA (128, 64, 32, 128)); + EXPECT_EQ (pixels[1], makeRGBA (1, 2, 3, 255)); + EXPECT_EQ (pixels[2], 0x00000000u); } TEST (ColorVectorOpsTests, PremultiplyRGBAFullAlphaUnchanged) { - uint8 pixels[] = { 100, 150, 200, 255 }; + uint32_t pixels[] = { makeRGBA (100, 150, 200, 255) }; ColorVectorOperations::premultiplyRGBA (pixels, 1); - EXPECT_EQ (pixels[0], 100); - EXPECT_EQ (pixels[1], 150); - EXPECT_EQ (pixels[2], 200); - EXPECT_EQ (pixels[3], 255); + EXPECT_EQ (pixels[0], makeRGBA (100, 150, 200, 255)); } TEST (ColorVectorOpsTests, PremultiplyRGBAZeroAlphaBlacksOut) { - uint8 pixels[] = { 100, 150, 200, 0 }; + uint32_t pixels[] = { makeRGBA (100, 150, 200, 0) }; ColorVectorOperations::premultiplyRGBA (pixels, 1); - EXPECT_EQ (pixels[0], 0); - EXPECT_EQ (pixels[1], 0); - EXPECT_EQ (pixels[2], 0); - EXPECT_EQ (pixels[3], 0); + EXPECT_EQ (pixels[0], 0x00000000u); } TEST (ColorVectorOpsTests, PremultiplyRGBAZeroPixels) { - uint8 pixels[] = { 99, 98, 97, 96 }; + uint32_t pixels[] = { makeRGBA (99, 98, 97, 96) }; ColorVectorOperations::premultiplyRGBA (pixels, 0); - EXPECT_EQ (pixels[0], 99); + EXPECT_EQ (pixels[0], makeRGBA (99, 98, 97, 96)); +} + +TEST (ColorVectorOpsTests, PremultiplyRGBALargePixelCount) +{ + // Exercise SIMD path with 100 pixels — well past the scalar tail boundary. + constexpr int kCount = 100; + uint32_t pixels[kCount]; + for (int i = 0; i < kCount; ++i) + { + const uint8_t r = (uint8_t) ((i * 3) & 0xff); + const uint8_t g = (uint8_t) ((i * 5) & 0xff); + const uint8_t b = (uint8_t) ((i * 7) & 0xff); + const uint8_t a = (uint8_t) ((i * 11) & 0xff); + pixels[i] = makeRGBA (r, g, b, a); + } + + ColorVectorOperations::premultiplyRGBA (pixels, kCount); + + for (int i = 0; i < kCount; ++i) + { + const uint32_t a = (uint32_t) ((i * 11) & 0xff); + const uint32_t expectedR = ((uint32_t) ((i * 3) & 0xff) * a + 127u) / 255u; + const uint32_t expectedG = ((uint32_t) ((i * 5) & 0xff) * a + 127u) / 255u; + const uint32_t expectedB = ((uint32_t) ((i * 7) & 0xff) * a + 127u) / 255u; + + EXPECT_EQ ((pixels[i] >> 24) & 0xffu, a); + EXPECT_EQ (pixels[i] & 0xffu, expectedR); + EXPECT_EQ ((pixels[i] >> 8) & 0xffu, expectedG); + EXPECT_EQ ((pixels[i] >> 16) & 0xffu, expectedB); + } +} + +TEST (ColorVectorOpsTests, PremultiplyRGBAOddPixelCount) +{ + // 5 pixels — not a multiple of 4, exercises both SIMD and scalar tail. + uint32_t pixels[] = { + makeRGBA (255, 128, 64, 128), + makeRGBA (64, 64, 64, 64), + makeRGBA (16, 32, 48, 255), + makeRGBA (170, 187, 204, 0), + makeRGBA (34, 68, 102, 192) + }; + + ColorVectorOperations::premultiplyRGBA (pixels, 5); + + for (int i = 0; i < 5; ++i) + { + const uint32_t a = (pixels[i] >> 24) & 0xffu; + const uint32_t r = pixels[i] & 0xffu; + const uint32_t g = (pixels[i] >> 8) & 0xffu; + const uint32_t b = (pixels[i] >> 16) & 0xffu; + + // Verify premultiplied channel ≤ min(original channel, alpha) + EXPECT_LE (r, a); + EXPECT_LE (g, a); + EXPECT_LE (b, a); + } } // ============================================================================== @@ -192,48 +315,80 @@ TEST (ColorVectorOpsTests, ConvertARGBtoRGBAAllOnes) EXPECT_EQ (rgba[0], 0xffffffffu); } +TEST (ColorVectorOpsTests, ConvertARGBtoRGBALargePixelCount) +{ + // Exercise SIMD path with 100 pixels — well past the scalar tail boundary. + constexpr int kCount = 100; + uint32_t argb[kCount]; + for (int i = 0; i < kCount; ++i) + { + const uint32_t a = (uint32_t) ((i * 7) & 0xff); + const uint32_t r = (uint32_t) ((i * 3) & 0xff); + const uint32_t g = (uint32_t) ((i * 5) & 0xff); + const uint32_t b = (uint32_t) (i & 0xff); + argb[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + + uint32_t rgba[kCount] = {}; + ColorVectorOperations::convertARGBtoRGBA (argb, rgba, kCount); + + for (int i = 0; i < kCount; ++i) + { + // ARGB 0xAARRGGBB -> RGBA 0xRRGGBBAA + EXPECT_EQ ((rgba[i] >> 24) & 0xffu, (uint32_t) ((i * 3) & 0xff)); // old R -> new R + EXPECT_EQ ((rgba[i] >> 16) & 0xffu, (uint32_t) ((i * 5) & 0xff)); // old G -> new G + EXPECT_EQ ((rgba[i] >> 8) & 0xffu, (uint32_t) (i & 0xff)); // old B -> new B + EXPECT_EQ (rgba[i] & 0xffu, (uint32_t) ((i * 7) & 0xff)); // old A -> new A + } +} + +TEST (ColorVectorOpsTests, ConvertARGBtoRGBAOddPixelCount) +{ + // 5 pixels — not a multiple of 4, exercises both SIMD and scalar tail. + const uint32_t argb[] = { + 0x12345678u, 0xaabbccddu, 0xffeeddccu, 0x01020304u, 0x99887766u + }; + uint32_t rgba[5] = {}; + + ColorVectorOperations::convertARGBtoRGBA (argb, rgba, 5); + + EXPECT_EQ (rgba[0], 0x34567812u); + EXPECT_EQ (rgba[1], 0xbbccddaau); + EXPECT_EQ (rgba[2], 0xeeddccffu); + EXPECT_EQ (rgba[3], 0x02030401u); + EXPECT_EQ (rgba[4], 0x88776699u); +} + // ============================================================================== // convertGrayscaleToRGBA tests // ============================================================================== TEST (ColorVectorOpsTests, ConvertGrayscaleToRGBA) { - const uint8 gray[] = { 0, 127, 255 }; - uint8 rgba[12] = {}; + const uint8_t gray[] = { 0, 127, 255 }; + uint32_t rgba[3] = {}; ColorVectorOperations::convertGrayscaleToRGBA (gray, rgba, 3); - EXPECT_EQ (rgba[0], 0); - EXPECT_EQ (rgba[1], 0); - EXPECT_EQ (rgba[2], 0); - EXPECT_EQ (rgba[3], 255); - EXPECT_EQ (rgba[4], 127); - EXPECT_EQ (rgba[5], 127); - EXPECT_EQ (rgba[6], 127); - EXPECT_EQ (rgba[7], 255); - EXPECT_EQ (rgba[8], 255); - EXPECT_EQ (rgba[9], 255); - EXPECT_EQ (rgba[10], 255); - EXPECT_EQ (rgba[11], 255); + EXPECT_EQ (rgba[0], makeRGBA (0, 0, 0, 255)); + EXPECT_EQ (rgba[1], makeRGBA (127, 127, 127, 255)); + EXPECT_EQ (rgba[2], makeRGBA (255, 255, 255, 255)); } TEST (ColorVectorOpsTests, ConvertGrayscaleToRGBAAlwaysOpaqueAlpha) { - const uint8 gray[] = { 128 }; - uint8 rgba[4] = {}; + const uint8_t gray[] = { 128 }; + uint32_t rgba[1] = {}; ColorVectorOperations::convertGrayscaleToRGBA (gray, rgba, 1); - EXPECT_EQ (rgba[0], 128); - EXPECT_EQ (rgba[1], 128); - EXPECT_EQ (rgba[2], 128); - EXPECT_EQ (rgba[3], 255); + EXPECT_EQ (rgba[0], makeRGBA (128, 128, 128, 255)); } TEST (ColorVectorOpsTests, ConvertGrayscaleToRGBAZeroPixels) { - const uint8 gray[] = { 99 }; - uint8 rgba[4] = { 1, 2, 3, 4 }; + const uint8_t gray[] = { 99 }; + uint32_t rgba[] = { 0x04030201u }; ColorVectorOperations::convertGrayscaleToRGBA (gray, rgba, 0); - EXPECT_EQ (rgba[0], 1); + EXPECT_EQ (rgba[0], 0x04030201u); } // ============================================================================== @@ -242,38 +397,29 @@ TEST (ColorVectorOpsTests, ConvertGrayscaleToRGBAZeroPixels) TEST (ColorVectorOpsTests, ConvertRGBToRGBA) { - const uint8 rgb[] = { 1, 2, 3, 4, 5, 6 }; - uint8 rgba[8] = {}; + const uint8_t rgb[] = { 1, 2, 3, 4, 5, 6 }; + uint32_t rgba[2] = {}; ColorVectorOperations::convertRGBToRGBA (rgb, rgba, 2); - EXPECT_EQ (rgba[0], 1); - EXPECT_EQ (rgba[1], 2); - EXPECT_EQ (rgba[2], 3); - EXPECT_EQ (rgba[3], 255); - EXPECT_EQ (rgba[4], 4); - EXPECT_EQ (rgba[5], 5); - EXPECT_EQ (rgba[6], 6); - EXPECT_EQ (rgba[7], 255); + EXPECT_EQ (rgba[0], makeRGBA (1, 2, 3, 255)); + EXPECT_EQ (rgba[1], makeRGBA (4, 5, 6, 255)); } TEST (ColorVectorOpsTests, ConvertRGBToRGBAAlwaysOpaqueAlpha) { - const uint8 rgb[] = { 10, 20, 30 }; - uint8 rgba[4] = {}; + const uint8_t rgb[] = { 10, 20, 30 }; + uint32_t rgba[1] = {}; ColorVectorOperations::convertRGBToRGBA (rgb, rgba, 1); - EXPECT_EQ (rgba[0], 10); - EXPECT_EQ (rgba[1], 20); - EXPECT_EQ (rgba[2], 30); - EXPECT_EQ (rgba[3], 255); + EXPECT_EQ (rgba[0], makeRGBA (10, 20, 30, 255)); } TEST (ColorVectorOpsTests, ConvertRGBToRGBAZeroPixels) { - const uint8 rgb[] = { 99, 98, 97 }; - uint8 rgba[4] = { 1, 2, 3, 4 }; + const uint8_t rgb[] = { 99, 98, 97 }; + uint32_t rgba[] = { 0x04030201u }; ColorVectorOperations::convertRGBToRGBA (rgb, rgba, 0); - EXPECT_EQ (rgba[0], 1); + EXPECT_EQ (rgba[0], 0x04030201u); } // ============================================================================== @@ -355,3 +501,140 @@ TEST (ColorVectorOpsTests, LerpRowsMultiplePixels) for (int i = 0; i < 16; ++i) EXPECT_NEAR (dst[i], rowA[i] + (rowB[i] - rowA[i]) * 0.5f, 1.0e-4f); } + +// ============================================================================== +// convertBGRAtoRGBA tests +// ============================================================================== + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBASwapsRedAndBlue) +{ + // BGRA pixels: bytes [B,G,R,A], swap → RGBA: bytes [R,G,B,A] + uint32_t pixels[] = { + makeBGRA (10, 20, 30, 40), + makeBGRA (50, 60, 70, 80) + }; + + ColorVectorOperations::convertBGRAtoRGBA (pixels, 2); + + EXPECT_EQ (pixels[0], makeBGRA (30, 20, 10, 40)); // R↔B + EXPECT_EQ (pixels[1], makeBGRA (70, 60, 50, 80)); +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBASinglePixel) +{ + uint32_t pixels[] = { makeBGRA (1, 2, 3, 255) }; + + ColorVectorOperations::convertBGRAtoRGBA (pixels, 1); + + EXPECT_EQ (pixels[0], makeBGRA (3, 2, 1, 255)); +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBAAllChannelsEqual) +{ + // When R == B, the swap is a no-op. + uint32_t pixels[] = { makeBGRA (100, 200, 100, 255) }; + + ColorVectorOperations::convertBGRAtoRGBA (pixels, 1); + + EXPECT_EQ (pixels[0], makeBGRA (100, 200, 100, 255)); +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBAAllZero) +{ + uint32_t pixels[] = { 0x00000000u, 0x00000000u }; + + ColorVectorOperations::convertBGRAtoRGBA (pixels, 2); + + EXPECT_EQ (pixels[0], 0x00000000u); + EXPECT_EQ (pixels[1], 0x00000000u); +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBAAllOnes) +{ + uint32_t pixels[] = { 0xffffffffu, 0xffffffffu }; + + ColorVectorOperations::convertBGRAtoRGBA (pixels, 2); + + EXPECT_EQ (pixels[0], 0xffffffffu); + EXPECT_EQ (pixels[1], 0xffffffffu); +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBAZeroPixels) +{ + uint32_t pixels[] = { makeBGRA (99, 98, 97, 96) }; + + ColorVectorOperations::convertBGRAtoRGBA (pixels, 0); + + EXPECT_EQ (pixels[0], makeBGRA (99, 98, 97, 96)); +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBANegativeCount) +{ + uint32_t pixels[] = { makeBGRA (1, 2, 3, 4) }; + + // Should no-op gracefully. + ColorVectorOperations::convertBGRAtoRGBA (pixels, -1); + + EXPECT_EQ (pixels[0], makeBGRA (1, 2, 3, 4)); +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBALargePixelCount) +{ + // Exercise SIMD path with 100 pixels — well past any scalar tail boundary. + constexpr int kCount = 100; + uint32_t pixels[kCount]; + for (int i = 0; i < kCount; ++i) + pixels[i] = makeBGRA ((uint8_t) (i & 0xff), + (uint8_t) ((i * 2) & 0xff), + (uint8_t) ((i * 3) & 0xff), + (uint8_t) ((i * 5) & 0xff)); + + ColorVectorOperations::convertBGRAtoRGBA (pixels, kCount); + + for (int i = 0; i < kCount; ++i) + { + const auto p = pixels[i]; + EXPECT_EQ ((p >> 0) & 0xffu, (uint32_t) ((i * 3) & 0xff)); // was B + EXPECT_EQ ((p >> 8) & 0xffu, (uint32_t) ((i * 2) & 0xff)); // G unchanged + EXPECT_EQ ((p >> 16) & 0xffu, (uint32_t) (i & 0xff)); // was R + EXPECT_EQ ((p >> 24) & 0xffu, (uint32_t) ((i * 5) & 0xff)); // A unchanged + } +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGBAOddPixelCount) +{ + // 5 pixels — not a multiple of 4 (SIMD) pixels. + // The scalar tail path handles the last pixel. + uint32_t pixels[] = { + makeBGRA (1, 2, 10, 255), + makeBGRA (3, 4, 20, 128), + makeBGRA (5, 6, 30, 64), + makeBGRA (7, 8, 40, 32), + makeBGRA (9, 11, 50, 16) + }; + + ColorVectorOperations::convertBGRAtoRGBA (pixels, 5); + + EXPECT_EQ (pixels[0], makeBGRA (10, 2, 1, 255)); + EXPECT_EQ (pixels[1], makeBGRA (20, 4, 3, 128)); + EXPECT_EQ (pixels[2], makeBGRA (30, 6, 5, 64)); + EXPECT_EQ (pixels[3], makeBGRA (40, 8, 7, 32)); + EXPECT_EQ (pixels[4], makeBGRA (50, 11, 9, 16)); +} + +TEST (ColorVectorOpsTests, ConvertBGRAtoRGRADoubleSwapIsIdentity) +{ + // Two swaps should restore the original values. + uint32_t pixels[] = { + makeBGRA (10, 20, 30, 40), + makeBGRA (50, 60, 70, 80), + makeBGRA (90, 100, 110, 120) + }; + + ColorVectorOperations::convertBGRAtoRGBA (pixels, 3); + ColorVectorOperations::convertBGRAtoRGBA (pixels, 3); + + EXPECT_EQ (pixels[0], makeBGRA (10, 20, 30, 40)); + EXPECT_EQ (pixels[1], makeBGRA (50, 60, 70, 80)); + EXPECT_EQ (pixels[2], makeBGRA (90, 100, 110, 120)); +} diff --git a/tests/yup_simd/yup_SIMDRegister.cpp b/tests/yup_simd/yup_SIMDRegister.cpp index 43ea61de3..1cd0f2252 100644 --- a/tests/yup_simd/yup_SIMDRegister.cpp +++ b/tests/yup_simd/yup_SIMDRegister.cpp @@ -26,12 +26,12 @@ using namespace yup; // ============================================================================== -// Float4 tests +// Float32x4 tests // ============================================================================== TEST (SIMDRegisterTests, Float4DefaultConstructorIsZero) { - Float4 r; + Float32x4 r; float stored[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; r.storeUnaligned (stored); @@ -41,7 +41,7 @@ TEST (SIMDRegisterTests, Float4DefaultConstructorIsZero) TEST (SIMDRegisterTests, Float4ZeroHelperIsAllZero) { - const auto r = Float4::zero(); + const auto r = Float32x4::zero(); float stored[4] = {}; r.storeUnaligned (stored); @@ -51,7 +51,7 @@ TEST (SIMDRegisterTests, Float4ZeroHelperIsAllZero) TEST (SIMDRegisterTests, Float4BroadcastFillsAllLanes) { - const auto r = Float4::broadcast (3.14f); + const auto r = Float32x4::broadcast (3.14f); float stored[4] = {}; r.storeUnaligned (stored); @@ -62,7 +62,7 @@ TEST (SIMDRegisterTests, Float4BroadcastFillsAllLanes) TEST (SIMDRegisterTests, Float4ElementAccessOperator) { const float values[4] = { 10.0f, 20.0f, 30.0f, 40.0f }; - const auto r = Float4::loadUnaligned (values); + const auto r = Float32x4::loadUnaligned (values); EXPECT_FLOAT_EQ (r[0], 10.0f); EXPECT_FLOAT_EQ (r[1], 20.0f); @@ -75,9 +75,9 @@ TEST (SIMDRegisterTests, Float4ArithmeticAndHorizontalOps) const float aValues[4] = { 1.0f, -2.0f, 3.0f, -4.0f }; const float bValues[4] = { 5.0f, 6.0f, -7.0f, -8.0f }; - const auto a = Float4::loadUnaligned (aValues); - const auto b = Float4::loadUnaligned (bValues); - const auto result = a + b * Float4::broadcast (2.0f); + const auto a = Float32x4::loadUnaligned (aValues); + const auto b = Float32x4::loadUnaligned (bValues); + const auto result = a + b * Float32x4::broadcast (2.0f); float stored[4] = {}; result.storeUnaligned (stored); @@ -94,8 +94,8 @@ TEST (SIMDRegisterTests, Float4Subtraction) const float aValues[4] = { 10.0f, 20.0f, 30.0f, 40.0f }; const float bValues[4] = { 1.0f, 3.0f, 5.0f, 7.0f }; - const auto a = Float4::loadUnaligned (aValues); - const auto b = Float4::loadUnaligned (bValues); + const auto a = Float32x4::loadUnaligned (aValues); + const auto b = Float32x4::loadUnaligned (bValues); const auto result = a - b; float stored[4] = {}; @@ -110,8 +110,8 @@ TEST (SIMDRegisterTests, Float4Division) const float aValues[4] = { 4.0f, 9.0f, 16.0f, 25.0f }; const float bValues[4] = { 2.0f, 3.0f, 4.0f, 5.0f }; - const auto a = Float4::loadUnaligned (aValues); - const auto b = Float4::loadUnaligned (bValues); + const auto a = Float32x4::loadUnaligned (aValues); + const auto b = Float32x4::loadUnaligned (bValues); const auto result = a / b; float stored[4] = {}; @@ -126,8 +126,8 @@ TEST (SIMDRegisterTests, Float4CompoundAddAssign) const float aValues[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; const float bValues[4] = { 10.0f, 20.0f, 30.0f, 40.0f }; - auto a = Float4::loadUnaligned (aValues); - const auto b = Float4::loadUnaligned (bValues); + auto a = Float32x4::loadUnaligned (aValues); + const auto b = Float32x4::loadUnaligned (bValues); a += b; float stored[4] = {}; @@ -142,8 +142,8 @@ TEST (SIMDRegisterTests, Float4CompoundMulAssign) const float aValues[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; const float bValues[4] = { 2.0f, 3.0f, 4.0f, 5.0f }; - auto a = Float4::loadUnaligned (aValues); - const auto b = Float4::loadUnaligned (bValues); + auto a = Float32x4::loadUnaligned (aValues); + const auto b = Float32x4::loadUnaligned (bValues); a *= b; float stored[4] = {}; @@ -158,8 +158,8 @@ TEST (SIMDRegisterTests, Float4ElementwiseMin) const float aValues[4] = { 1.0f, 5.0f, 2.0f, 4.0f }; const float bValues[4] = { 3.0f, 2.0f, 4.0f, 1.0f }; - const auto a = Float4::loadUnaligned (aValues); - const auto b = Float4::loadUnaligned (bValues); + const auto a = Float32x4::loadUnaligned (aValues); + const auto b = Float32x4::loadUnaligned (bValues); const auto result = a.min (b); float stored[4] = {}; @@ -174,8 +174,8 @@ TEST (SIMDRegisterTests, Float4ElementwiseMax) const float aValues[4] = { 1.0f, 5.0f, 2.0f, 4.0f }; const float bValues[4] = { 3.0f, 2.0f, 4.0f, 1.0f }; - const auto a = Float4::loadUnaligned (aValues); - const auto b = Float4::loadUnaligned (bValues); + const auto a = Float32x4::loadUnaligned (aValues); + const auto b = Float32x4::loadUnaligned (bValues); const auto result = a.max (b); float stored[4] = {}; @@ -188,7 +188,7 @@ TEST (SIMDRegisterTests, Float4ElementwiseMax) TEST (SIMDRegisterTests, Float4AbsOnMixedValues) { const float values[4] = { -1.0f, 2.0f, -3.0f, 4.0f }; - const auto r = Float4::loadUnaligned (values); + const auto r = Float32x4::loadUnaligned (values); const auto result = r.abs(); float stored[4] = {}; @@ -203,21 +203,21 @@ TEST (SIMDRegisterTests, Float4AbsOnMixedValues) TEST (SIMDRegisterTests, Float4SumAllLanes) { const float values[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; - const auto r = Float4::loadUnaligned (values); + const auto r = Float32x4::loadUnaligned (values); EXPECT_FLOAT_EQ (r.sum(), 10.0f); } TEST (SIMDRegisterTests, Float4SumWithNegatives) { const float values[4] = { 1.0f, -1.0f, 2.0f, -2.0f }; - const auto r = Float4::loadUnaligned (values); + const auto r = Float32x4::loadUnaligned (values); EXPECT_FLOAT_EQ (r.sum(), 0.0f); } TEST (SIMDRegisterTests, Float4HmaxFindsLargest) { const float values[4] = { -3.0f, 7.0f, 1.0f, -10.0f }; - const auto r = Float4::loadUnaligned (values); + const auto r = Float32x4::loadUnaligned (values); EXPECT_FLOAT_EQ (r.hmax(), 7.0f); } @@ -228,7 +228,7 @@ TEST (SIMDRegisterTests, MulAddAndLoadStoreRoundTrip) alignas (16) const float add[4] = { 10.0f, 20.0f, 30.0f, 40.0f }; alignas (16) float stored[4] = {}; - const auto result = Float4::loadAligned (base).mulAdd (Float4::loadAligned (mul), Float4::loadAligned (add)); + const auto result = Float32x4::loadAligned (base).mulAdd (Float32x4::loadAligned (mul), Float32x4::loadAligned (add)); result.storeAligned (stored); for (int i = 0; i < 4; ++i) @@ -238,7 +238,7 @@ TEST (SIMDRegisterTests, MulAddAndLoadStoreRoundTrip) TEST (SIMDRegisterTests, Float4LoadFromPointerConstructor) { const float values[4] = { 5.0f, 6.0f, 7.0f, 8.0f }; - const Float4 r (values); + const Float32x4 r (values); float stored[4] = {}; r.storeUnaligned (stored); @@ -249,26 +249,26 @@ TEST (SIMDRegisterTests, Float4LoadFromPointerConstructor) TEST (SIMDRegisterTests, Float4ScalarConstructorBroadcasts) { - const Float4 r (42.0f); + const Float32x4 r (42.0f); for (int i = 0; i < 4; ++i) EXPECT_FLOAT_EQ (r[i], 42.0f); } // ============================================================================== -// Float8 tests +// Float32x8 tests // ============================================================================== TEST (SIMDRegisterTests, Float8DefaultConstructorIsZero) { - Float8 r; + Float32x8 r; for (int i = 0; i < 8; ++i) EXPECT_FLOAT_EQ (r[i], 0.0f); } TEST (SIMDRegisterTests, Float8BroadcastAndArithmetic) { - const Float8 a = Float8::broadcast (2.0f); - const Float8 b = Float8::broadcast (3.0f); + const Float32x8 a = Float32x8::broadcast (2.0f); + const Float32x8 b = Float32x8::broadcast (3.0f); const auto result = a * b; for (int i = 0; i < 8; ++i) @@ -278,7 +278,7 @@ TEST (SIMDRegisterTests, Float8BroadcastAndArithmetic) TEST (SIMDRegisterTests, Float8LoadStoreRoundTrip) { float values[8] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f }; - const auto r = Float8::loadUnaligned (values); + const auto r = Float32x8::loadUnaligned (values); float stored[8] = {}; r.storeUnaligned (stored); @@ -290,14 +290,14 @@ TEST (SIMDRegisterTests, Float8LoadStoreRoundTrip) TEST (SIMDRegisterTests, Float8Sum) { float values[8] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f }; - const auto r = Float8::loadUnaligned (values); + const auto r = Float32x8::loadUnaligned (values); EXPECT_FLOAT_EQ (r.sum(), 36.0f); } TEST (SIMDRegisterTests, Float8Hmax) { float values[8] = { 1.0f, -5.0f, 3.0f, 9.0f, 2.0f, -3.0f, 4.0f, 0.0f }; - const auto r = Float8::loadUnaligned (values); + const auto r = Float32x8::loadUnaligned (values); EXPECT_FLOAT_EQ (r.hmax(), 9.0f); } @@ -307,18 +307,18 @@ TEST (SIMDRegisterTests, Float8AbsNegatesAll) for (int i = 0; i < 8; ++i) values[i] = (i % 2 == 0) ? -(float) (i + 1) : (float) (i + 1); - const auto r = Float8::loadUnaligned (values).abs(); + const auto r = Float32x8::loadUnaligned (values).abs(); for (int i = 0; i < 8; ++i) EXPECT_FLOAT_EQ (r[i], (float) (i + 1)); } // ============================================================================== -// Double2 tests +// Float64x2 tests // ============================================================================== TEST (SIMDRegisterTests, Double2DefaultConstructorIsZero) { - Double2 r; + Float64x2 r; for (int i = 0; i < 2; ++i) EXPECT_DOUBLE_EQ (r[i], 0.0); } @@ -328,8 +328,8 @@ TEST (SIMDRegisterTests, Double2ArithmeticOperations) const double aValues[2] = { 1.5, -2.5 }; const double bValues[2] = { 3.0, 4.0 }; - const auto a = Double2::loadUnaligned (aValues); - const auto b = Double2::loadUnaligned (bValues); + const auto a = Float64x2::loadUnaligned (aValues); + const auto b = Float64x2::loadUnaligned (bValues); const auto sum = a + b; const auto diff = a - b; @@ -348,7 +348,7 @@ TEST (SIMDRegisterTests, Double2ArithmeticOperations) TEST (SIMDRegisterTests, Double2BroadcastAndSum) { - const auto r = Double2::broadcast (3.14); + const auto r = Float64x2::broadcast (3.14); EXPECT_NEAR (r.sum(), 6.28, 1.0e-12); } @@ -357,8 +357,8 @@ TEST (SIMDRegisterTests, Double2MinMax) const double aValues[2] = { 1.0, 5.0 }; const double bValues[2] = { 3.0, 2.0 }; - const auto a = Double2::loadUnaligned (aValues); - const auto b = Double2::loadUnaligned (bValues); + const auto a = Float64x2::loadUnaligned (aValues); + const auto b = Float64x2::loadUnaligned (bValues); const auto minResult = a.min (b); const auto maxResult = a.max (b); @@ -370,13 +370,13 @@ TEST (SIMDRegisterTests, Double2MinMax) } // ============================================================================== -// Double4 tests +// Float64x4 tests // ============================================================================== TEST (SIMDRegisterTests, Double4LoadStoreRoundTrip) { const double values[4] = { 1.1, 2.2, 3.3, 4.4 }; - const auto r = Double4::loadUnaligned (values); + const auto r = Float64x4::loadUnaligned (values); double stored[4] = {}; r.storeUnaligned (stored); @@ -391,9 +391,9 @@ TEST (SIMDRegisterTests, Double4MulAdd) const double mulValues[4] = { 2.0, 3.0, 4.0, 5.0 }; const double addValues[4] = { 10.0, 20.0, 30.0, 40.0 }; - const auto base = Double4::loadUnaligned (baseValues); - const auto mul = Double4::loadUnaligned (mulValues); - const auto add = Double4::loadUnaligned (addValues); + const auto base = Float64x4::loadUnaligned (baseValues); + const auto mul = Float64x4::loadUnaligned (mulValues); + const auto add = Float64x4::loadUnaligned (addValues); const auto result = base.mulAdd (mul, add); for (int i = 0; i < 4; ++i) @@ -403,14 +403,14 @@ TEST (SIMDRegisterTests, Double4MulAdd) TEST (SIMDRegisterTests, Double4Sum) { const double values[4] = { 1.0, 2.0, 3.0, 4.0 }; - const auto r = Double4::loadUnaligned (values); + const auto r = Float64x4::loadUnaligned (values); EXPECT_DOUBLE_EQ (r.sum(), 10.0); } TEST (SIMDRegisterTests, Double4Hmax) { const double values[4] = { -1.0, 3.5, 2.0, -4.0 }; - const auto r = Double4::loadUnaligned (values); + const auto r = Float64x4::loadUnaligned (values); EXPECT_DOUBLE_EQ (r.hmax(), 3.5); } diff --git a/thirdparty/rive_renderer/rive_renderer.h b/thirdparty/rive_renderer/rive_renderer.h index fed638ed7..811a4a7c2 100644 --- a/thirdparty/rive_renderer/rive_renderer.h +++ b/thirdparty/rive_renderer/rive_renderer.h @@ -92,10 +92,10 @@ #endif /** Config: YUP_RIVE_OPENGL_MINOR - Enables a speficic OpenGL minor version. Must be at least 2. + Enables a speficic OpenGL minor version. Must be at least 3 (OpenGL 4.3+, required for compute shaders). */ #ifndef YUP_RIVE_OPENGL_MINOR -#define YUP_RIVE_OPENGL_MINOR 2 +#define YUP_RIVE_OPENGL_MINOR 3 #endif //============================================================================== diff --git a/thirdparty/rive_renderer/source/generated/shaders/atomic_draw.glsl.hpp b/thirdparty/rive_renderer/source/generated/shaders/atomic_draw.glsl.hpp index ad1486034..63b9ec63b 100644 --- a/thirdparty/rive_renderer/source/generated/shaders/atomic_draw.glsl.hpp +++ b/thirdparty/rive_renderer/source/generated/shaders/atomic_draw.glsl.hpp @@ -168,7 +168,7 @@ l0=min(l0,k.Ee); #endif return l0;} #ifdef O -e void yb(uint V0,o4 O0,T4(d)n){ +e void yb(uint V0,o4 O0,T4(d) n){ #ifdef OC if(all(lessThan(abs(O0.xy-unpackUnorm4x8(V0).xy),A2(.25/255.))))n=min(n,O0.z);else n=.0; #else @@ -176,9 +176,9 @@ if(V0==O0>>16)n=min(n,unpackHalf2x16(O0).x);else n=.0; #endif } #endif -e void Y7(uint l0,d m0,e1(i)P +e void Y7(uint l0,d m0,e1(i) P #if defined(O)&&!defined(IC) -,T4(o4)q1 +,T4(o4) q1 #endif G6 P3){W0 r1=L5(TC,l0);d n=m0;if((r1.x&(Fe|C9))!=0u){n=abs(n); #ifdef PC diff --git a/thirdparty/rive_renderer/source/generated/shaders/atomic_draw.minified.glsl b/thirdparty/rive_renderer/source/generated/shaders/atomic_draw.minified.glsl index 8e0f2b95d..ffb36c3a7 100644 --- a/thirdparty/rive_renderer/source/generated/shaders/atomic_draw.minified.glsl +++ b/thirdparty/rive_renderer/source/generated/shaders/atomic_draw.minified.glsl @@ -161,7 +161,7 @@ l0=min(l0,k.Ee); #endif return l0;} #ifdef ENABLE_CLIPPING -e void yb(uint V0,o4 O0,T4(d)n){ +e void yb(uint V0,o4 O0,T4(d) n){ #ifdef PLS_BLEND_SRC_OVER if(all(lessThan(abs(O0.xy-unpackUnorm4x8(V0).xy),A2(.25/255.))))n=min(n,O0.z);else n=.0; #else @@ -169,9 +169,9 @@ if(V0==O0>>16)n=min(n,unpackHalf2x16(O0).x);else n=.0; #endif } #endif -e void Y7(uint l0,d m0,e1(i)P +e void Y7(uint l0,d m0,e1(i) P #if defined(ENABLE_CLIPPING)&&!defined(RESOLVE_PLS) -,T4(o4)q1 +,T4(o4) q1 #endif G6 P3){W0 r1=L5(TC,l0);d n=m0;if((r1.x&(Fe|C9))!=0u){n=abs(n); #ifdef ENABLE_EVEN_ODD diff --git a/thirdparty/rive_renderer/source/generated/shaders/bezier_utils.glsl.hpp b/thirdparty/rive_renderer/source/generated/shaders/bezier_utils.glsl.hpp index 74400cc47..d2d7a90f1 100644 --- a/thirdparty/rive_renderer/source/generated/shaders/bezier_utils.glsl.hpp +++ b/thirdparty/rive_renderer/source/generated/shaders/bezier_utils.glsl.hpp @@ -11,7 +11,7 @@ const char bezier_utils[] = R"===(#ifndef Hb #ifndef J6 #define J6 c #endif -e float K9(c o,c b){float Le=dot(o,b);float Ib=dot(o,o)*dot(b,b);return(Ib==.0)?1.:clamp(Le*inversesqrt(Ib),-1.,1.);}e void Me(c w0,c x0,c E0,c I0,e1(c)A,e1(c)F,e1(c)d2){d2=x0-w0;c K6=E0-x0;c h8=I0-w0;F=K6-d2;A=-3.*K6+h8;}e Z L9(c w0,c x0,c E0,c I0){Z t;t[0]=(any(notEqual(w0,x0))?x0:any(notEqual(x0,E0))?E0:I0)-w0;t[1]=I0-(any(notEqual(I0,E0))?E0:any(notEqual(E0,x0))?x0:w0);return t;}e float Ne(c w0,c x0,c E0,c I0,float w1,float Oe){c A,F,d2;Me(w0,x0,E0,I0,A,F,d2);c L6=3.*(((A*w1)+2.*F)*w1+d2);float Jb=length(L6);if(Jb==.0){return.0;}L6*=1./Jb;float i8=2.*dot(A,L6);float M6=3.*(i8*w1+4.*dot(F,L6))*w1+6.*dot(d2,L6);float M9=min(w1,1.-w1);float Pe=(i8*M9*M9+M6)*M9;float Kb=min(Oe,Pe*.9999);float V2;if(i8==.0){V2=Kb/M6;}else{float H=1./i8;float b=M6*H,I1=-Kb*H;float N6=(-1./3.)*b,O6=.5*I1;float Lb=O6*O6-N6*N6*N6;if(Lb<.0){float j8=sqrt(N6);float h1=acos(O6/(j8*j8*j8));V2=-2.*j8*cos(h1*(1./3.)+(-A3*2./3.));}else{float A=pow(abs(O6)+sqrt(Lb),1./3.);if(O6<.0)A=-A;V2=A!=.0?A+N6/A:.0;}}V2=abs(V2);g t0011=w1+Hb(-V2,-V2,V2,V2);g Mb=(A.xyxy*t0011+2.*F.xyxy)*t0011+d2.xyxy;Z F2=L9(w0,x0,E0,I0);c Qe=t0011.x<1e-3?F2[0]:Mb.xy;c Re=t0011.z>1.-1e-3?F2[1]:Mb.zw;return acos(K9(Qe,Re));}e float k8(float o,float b){o=b<.0?-o:o;b=abs(b);return o>.0?(o1.-1e-3?F2[1]:Mb.zw;return acos(K9(Qe,Re));}e float k8(float o,float b){o=b<.0?-o:o;b=abs(b);return o>.0?(oT5.y?P6.x:P6.y;return max(T5.x,T5.y); #else diff --git a/thirdparty/rive_renderer/source/generated/shaders/bezier_utils.minified.glsl b/thirdparty/rive_renderer/source/generated/shaders/bezier_utils.minified.glsl index dac5f0032..cc951d9f9 100644 --- a/thirdparty/rive_renderer/source/generated/shaders/bezier_utils.minified.glsl +++ b/thirdparty/rive_renderer/source/generated/shaders/bezier_utils.minified.glsl @@ -4,7 +4,7 @@ #ifndef J6 #define J6 c #endif -e float K9(c o,c b){float Le=dot(o,b);float Ib=dot(o,o)*dot(b,b);return(Ib==.0)?1.:clamp(Le*inversesqrt(Ib),-1.,1.);}e void Me(c w0,c x0,c E0,c I0,e1(c)A,e1(c)F,e1(c)d2){d2=x0-w0;c K6=E0-x0;c h8=I0-w0;F=K6-d2;A=-3.*K6+h8;}e Z L9(c w0,c x0,c E0,c I0){Z t;t[0]=(any(notEqual(w0,x0))?x0:any(notEqual(x0,E0))?E0:I0)-w0;t[1]=I0-(any(notEqual(I0,E0))?E0:any(notEqual(E0,x0))?x0:w0);return t;}e float Ne(c w0,c x0,c E0,c I0,float w1,float Oe){c A,F,d2;Me(w0,x0,E0,I0,A,F,d2);c L6=3.*(((A*w1)+2.*F)*w1+d2);float Jb=length(L6);if(Jb==.0){return.0;}L6*=1./Jb;float i8=2.*dot(A,L6);float M6=3.*(i8*w1+4.*dot(F,L6))*w1+6.*dot(d2,L6);float M9=min(w1,1.-w1);float Pe=(i8*M9*M9+M6)*M9;float Kb=min(Oe,Pe*.9999);float V2;if(i8==.0){V2=Kb/M6;}else{float H=1./i8;float b=M6*H,I1=-Kb*H;float N6=(-1./3.)*b,O6=.5*I1;float Lb=O6*O6-N6*N6*N6;if(Lb<.0){float j8=sqrt(N6);float h1=acos(O6/(j8*j8*j8));V2=-2.*j8*cos(h1*(1./3.)+(-A3*2./3.));}else{float A=pow(abs(O6)+sqrt(Lb),1./3.);if(O6<.0)A=-A;V2=A!=.0?A+N6/A:.0;}}V2=abs(V2);g t0011=w1+Hb(-V2,-V2,V2,V2);g Mb=(A.xyxy*t0011+2.*F.xyxy)*t0011+d2.xyxy;Z F2=L9(w0,x0,E0,I0);c Qe=t0011.x<1e-3?F2[0]:Mb.xy;c Re=t0011.z>1.-1e-3?F2[1]:Mb.zw;return acos(K9(Qe,Re));}e float k8(float o,float b){o=b<.0?-o:o;b=abs(b);return o>.0?(o1.-1e-3?F2[1]:Mb.zw;return acos(K9(Qe,Re));}e float k8(float o,float b){o=b<.0?-o:o;b=abs(b);return o>.0?(oT5.y?P6.x:P6.y;return max(T5.x,T5.y); #else diff --git a/thirdparty/rive_renderer/source/generated/shaders/draw_clockwise_atomic_path.frag.hpp b/thirdparty/rive_renderer/source/generated/shaders/draw_clockwise_atomic_path.frag.hpp index 13f55a488..daabdbe25 100644 --- a/thirdparty/rive_renderer/source/generated/shaders/draw_clockwise_atomic_path.frag.hpp +++ b/thirdparty/rive_renderer/source/generated/shaders/draw_clockwise_atomic_path.frag.hpp @@ -14,7 +14,7 @@ r0(R2,d0); #ifndef K Ja(d6,z6); #endif -K1 N3 Ea(ga,Yd,S0);O3 e void xh(T4(float)o3,d o0,uint T1,e1(uint)p1,e1(d)J3){ +K1 N3 Ea(ga,Yd,S0);O3 e void xh(T4(float) o3,d o0,uint T1,e1(uint) p1,e1(d) J3){ #ifdef K if(min(o3,o0)>=1.){return;} #endif @@ -30,7 +30,7 @@ d V1=V5(p1&ja)*ha;d G1=max(V1,o0);q=K8(V1,G1,o3); #ifndef K J3=G1; #endif -}o3*=q;}e void yh(T4(float)o3,d P4,uint T1,e1(uint)p1,e1(d)J3){d q=.0;uint fb=q7(abs(P4));p1=pd(S0,T1); +}o3*=q;}e void yh(T4(float) o3,d P4,uint T1,e1(uint) p1,e1(d) J3){d q=.0;uint fb=q7(abs(P4));p1=pd(S0,T1); #ifdef K if(min(o3,P4)>=1.&&(p1=(k.W1|j5))){return;} #endif diff --git a/thirdparty/rive_renderer/source/generated/shaders/draw_clockwise_atomic_path.minified.frag b/thirdparty/rive_renderer/source/generated/shaders/draw_clockwise_atomic_path.minified.frag index 4fd50e2e0..27d51c1ac 100644 --- a/thirdparty/rive_renderer/source/generated/shaders/draw_clockwise_atomic_path.minified.frag +++ b/thirdparty/rive_renderer/source/generated/shaders/draw_clockwise_atomic_path.minified.frag @@ -7,7 +7,7 @@ r0(R2,d0); #ifndef FIXED_FUNCTION_COLOR_OUTPUT Ja(d6,z6); #endif -K1 N3 Ea(ga,Yd,S0);O3 e void xh(T4(float)o3,d o0,uint T1,e1(uint)p1,e1(d)J3){ +K1 N3 Ea(ga,Yd,S0);O3 e void xh(T4(float) o3,d o0,uint T1,e1(uint) p1,e1(d) J3){ #ifdef FIXED_FUNCTION_COLOR_OUTPUT if(min(o3,o0)>=1.){return;} #endif @@ -23,7 +23,7 @@ d V1=V5(p1&ja)*ha;d G1=max(V1,o0);q=K8(V1,G1,o3); #ifndef FIXED_FUNCTION_COLOR_OUTPUT J3=G1; #endif -}o3*=q;}e void yh(T4(float)o3,d P4,uint T1,e1(uint)p1,e1(d)J3){d q=.0;uint fb=q7(abs(P4));p1=pd(S0,T1); +}o3*=q;}e void yh(T4(float) o3,d P4,uint T1,e1(uint) p1,e1(d) J3){d q=.0;uint fb=q7(abs(P4));p1=pd(S0,T1); #ifdef FIXED_FUNCTION_COLOR_OUTPUT if(min(o3,P4)>=1.&&(p1=(k.W1|j5))){return;} #endif diff --git a/thirdparty/rive_renderer/source/generated/shaders/draw_path_common.glsl.hpp b/thirdparty/rive_renderer/source/generated/shaders/draw_path_common.glsl.hpp index e22ffa804..16d988ba5 100644 --- a/thirdparty/rive_renderer/source/generated/shaders/draw_path_common.glsl.hpp +++ b/thirdparty/rive_renderer/source/generated/shaders/draw_path_common.glsl.hpp @@ -51,11 +51,11 @@ g Ic(float ma,c D8,float F1){c g6=(1.-D8*abs(F1))*.5;float Y3,l5;if(abs(ma-T6)<1 e d d8(g J F3){d Y3=J.z;d l5=max(J.w,.0);d h6=Y3>=.0?g5(l5):.0;if(abs(Y3)>pc);}e float Lc(Z Y0,c Xf){c e2=Z0(Y0,Xf);return(abs(e2.x)+abs(e2.y))*(1./dot(e2,e2));}e bool p9(g h7,g oa,int T,e1(uint)c3,e1(c)Yf +e U m5(int Kc){return U(Kc&((1<>pc);}e float Lc(Z Y0,c Xf){c e2=Z0(Y0,Xf);return(abs(e2.x)+abs(e2.y))*(1./dot(e2,e2));}e bool p9(g h7,g oa,int T,e1(uint) c3,e1(c) Yf #ifndef BB -,e1(g)N1 +,e1(g) N1 #else -,e1(X)i7 +,e1(X) i7 #endif i6){int E8=int(h7.x);float F1=h7.y;float pa=h7.z;int Mc=floatBitsToInt(h7.w)>>2;int j7=floatBitsToInt(h7.w)&3;int qa=min(E8,Mc-1);int G4=T*Mc+qa;C4 n5=v1(DC,m5(G4));uint e0=f5(n5.w);uint F8=max(e0&wc,1u);Q ra=P0(XC,F8-1u);c Nc=uintBitsToFloat(ra.xy);c3=ra.z&0xffffu;uint Oc=ra.w;Z Y0=j2(uintBitsToFloat(P0(MB,c3*4u)));Q H4=P0(MB,c3*4u+1u);c c2=uintBitsToFloat(H4.xy);float H2=uintBitsToFloat(H4.z);float I2=uintBitsToFloat(H4.w);uint Pc=e0&D3;if(Pc!=0u){E8=int(oa.x);F1=oa.y;pa=oa.z;}if(E8!=qa){int Qc=G4+E8-qa;C4 Rc=v1(DC,m5(Qc));if((f5(Rc.w)&(D3|0xffffu))!=(e0&(D3|0xffffu))){bool Zf=H2==.0||Nc.x!=.0;if(Zf){G4=int(Oc);n5=v1(DC,m5(G4));}}else{G4=Qc;n5=Rc;}e0=(f5(n5.w)&~D3)|Pc;}float h1; #ifdef HB @@ -94,11 +94,11 @@ N1.xy=mix(N1.xy,c(1.,-1.),bf(k.og!=0u)); return true;} #endif #if defined(CB)&&defined(DB) -e c tb(V j6,e1(uint)c3 +e c tb(V j6,e1(uint) c3 #ifdef BB -,e1(X)i7 +,e1(X) i7 #else -,e1(d)pg +,e1(d) pg #endif i6){c3=floatBitsToUint(j6.z)&0xffffu; #ifdef BB @@ -109,11 +109,11 @@ pg=U9(floatBitsToInt(j6.z)>>16); c k6=j6.xy;Z Y0=j2(uintBitsToFloat(P0(MB,c3*4u)));Q H4=P0(MB,c3*4u+1u);c c2=uintBitsToFloat(H4.xy);k6=Z0(Y0,k6)+c2;return k6;} #endif #if defined(CB)&&defined(EB) -e c sb(V j6,e1(uint)c3, +e c sb(V j6,e1(uint) c3, #ifdef BB -e1(X)i7, +e1(X) i7, #endif -e1(c)qg i6){c3=floatBitsToUint(j6.z)&0xffffu;Q J4=P0(MB,c3*4u+2u); +e1(c) qg i6){c3=floatBitsToUint(j6.z)&0xffffu;Q J4=P0(MB,c3*4u+2u); #ifdef BB i7=i2(J4.x); #endif diff --git a/thirdparty/rive_renderer/source/generated/shaders/draw_path_common.minified.glsl b/thirdparty/rive_renderer/source/generated/shaders/draw_path_common.minified.glsl index c797cfff0..84df112fd 100644 --- a/thirdparty/rive_renderer/source/generated/shaders/draw_path_common.minified.glsl +++ b/thirdparty/rive_renderer/source/generated/shaders/draw_path_common.minified.glsl @@ -44,11 +44,11 @@ g Ic(float ma,c D8,float F1){c g6=(1.-D8*abs(F1))*.5;float Y3,l5;if(abs(ma-T6)<1 e d d8(g J F3){d Y3=J.z;d l5=max(J.w,.0);d h6=Y3>=.0?g5(l5):.0;if(abs(Y3)>pc);}e float Lc(Z Y0,c Xf){c e2=Z0(Y0,Xf);return(abs(e2.x)+abs(e2.y))*(1./dot(e2,e2));}e bool p9(g h7,g oa,int T,e1(uint)c3,e1(c)Yf +e U m5(int Kc){return U(Kc&((1<>pc);}e float Lc(Z Y0,c Xf){c e2=Z0(Y0,Xf);return(abs(e2.x)+abs(e2.y))*(1./dot(e2,e2));}e bool p9(g h7,g oa,int T,e1(uint) c3,e1(c) Yf #ifndef RENDER_MODE_MSAA -,e1(g)N1 +,e1(g) N1 #else -,e1(X)i7 +,e1(X) i7 #endif i6){int E8=int(h7.x);float F1=h7.y;float pa=h7.z;int Mc=floatBitsToInt(h7.w)>>2;int j7=floatBitsToInt(h7.w)&3;int qa=min(E8,Mc-1);int G4=T*Mc+qa;C4 n5=v1(DC,m5(G4));uint e0=f5(n5.w);uint F8=max(e0&wc,1u);Q ra=P0(XC,F8-1u);c Nc=uintBitsToFloat(ra.xy);c3=ra.z&0xffffu;uint Oc=ra.w;Z Y0=j2(uintBitsToFloat(P0(MB,c3*4u)));Q H4=P0(MB,c3*4u+1u);c c2=uintBitsToFloat(H4.xy);float H2=uintBitsToFloat(H4.z);float I2=uintBitsToFloat(H4.w);uint Pc=e0&D3;if(Pc!=0u){E8=int(oa.x);F1=oa.y;pa=oa.z;}if(E8!=qa){int Qc=G4+E8-qa;C4 Rc=v1(DC,m5(Qc));if((f5(Rc.w)&(D3|0xffffu))!=(e0&(D3|0xffffu))){bool Zf=H2==.0||Nc.x!=.0;if(Zf){G4=int(Oc);n5=v1(DC,m5(G4));}}else{G4=Qc;n5=Rc;}e0=(f5(n5.w)&~D3)|Pc;}float h1; #ifdef ENABLE_FEATHER @@ -87,11 +87,11 @@ N1.xy=mix(N1.xy,c(1.,-1.),bf(k.og!=0u)); return true;} #endif #if defined(VERTEX)&&defined(DRAW_INTERIOR_TRIANGLES) -e c tb(V j6,e1(uint)c3 +e c tb(V j6,e1(uint) c3 #ifdef RENDER_MODE_MSAA -,e1(X)i7 +,e1(X) i7 #else -,e1(d)pg +,e1(d) pg #endif i6){c3=floatBitsToUint(j6.z)&0xffffu; #ifdef RENDER_MODE_MSAA @@ -102,11 +102,11 @@ pg=U9(floatBitsToInt(j6.z)>>16); c k6=j6.xy;Z Y0=j2(uintBitsToFloat(P0(MB,c3*4u)));Q H4=P0(MB,c3*4u+1u);c c2=uintBitsToFloat(H4.xy);k6=Z0(Y0,k6)+c2;return k6;} #endif #if defined(VERTEX)&&defined(ATLAS_BLIT) -e c sb(V j6,e1(uint)c3, +e c sb(V j6,e1(uint) c3, #ifdef RENDER_MODE_MSAA -e1(X)i7, +e1(X) i7, #endif -e1(c)qg i6){c3=floatBitsToUint(j6.z)&0xffffu;Q J4=P0(MB,c3*4u+2u); +e1(c) qg i6){c3=floatBitsToUint(j6.z)&0xffffu;Q J4=P0(MB,c3*4u+2u); #ifdef RENDER_MODE_MSAA i7=i2(J4.x); #endif diff --git a/thirdparty/rive_renderer/source/shaders/minify.py b/thirdparty/rive_renderer/source/shaders/minify.py index c01fade10..3395e0c96 100644 --- a/thirdparty/rive_renderer/source/shaders/minify.py +++ b/thirdparty/rive_renderer/source/shaders/minify.py @@ -481,6 +481,12 @@ def emit_tokens_to_rewritten_glsl(self, out, *, preserve_exported_switches, call out.write('\n') elif needs_whitespace and lasttoken_needs_whitespace: out.write(' ') + elif tok.type == "ID" and lasttoken.type == "OP" and lasttoken.value == ")": + # Mesa's GLSL compiler can reject minified GLSL when a ')' from a + # macro argument list is directly adjacent to the next identifier + # without any whitespace, e.g., OUT(float2)foo. + # Insert a space to work around this Mesa compiler bug. + out.write(' ') # is_newline will be false once we output the token (unless this value otherwise gets # updated). diff --git a/thirdparty/spirv_cross/upstream/spirv_msl.cpp b/thirdparty/spirv_cross/upstream/spirv_msl.cpp index 1b0eb4279..d9e366020 100644 --- a/thirdparty/spirv_cross/upstream/spirv_msl.cpp +++ b/thirdparty/spirv_cross/upstream/spirv_msl.cpp @@ -16115,7 +16115,10 @@ uint32_t CompilerMSL::get_metal_resource_index(SPIRVariable &var, SPIRType::Base var_binding = get_decoration(var.self, DecorationBinding); // Avoid emitting sentinel bindings. if (var_binding < 0x80000000u) + { + set_extended_decoration(var.self, resource_decoration, var_binding); return var_binding; + } } }