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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Graphics

- Fixed GPU compute silently stalling after a few frames on OpenGL with some drivers (AMD desktop GL): compute now runs on a dedicated, unshared GL context (`GpuDevice::Options::computeContextActivator`, routed through `GpuDevice::runOnComputeContext()`) which exclusively owns every compute resource — pipeline compilation, dispatches, storage buffer create/update/readback and deletion — falling back to the rendering context when unavailable. The GL compute pass also saves and restores the program and `GL_UNIFORM_BUFFER` bindings it touches, so it can no longer desync Rive's cached GL state
- Fixed GL storage buffers being deleted right after creation (moving a `GpuBuffer::Impl` copied the plain GL buffer name, so the moved-from object's destructor freed the just-created buffer) and a crash when releasing GPU buffers or compute pipelines after their window closed (GL releases are routed through the owning device and skipped once the window's contexts are gone)
- Fixed Emscripten randomly rendering nothing or freezing the tab: the render-thread rework unbound the GL context between frames so the render thread could take it, but on Emscripten rendering is timer-driven on the single browser thread and message-thread work between frames (image decodes, font atlas uploads) still issues GL calls — with no context current those throw in JS and kill the main loop. With timer-driven rendering the window context now stays permanently current
- Fixed Emscripten freezing the browser tab when a demo requested a headless GL compute device: with no current WebGL context every GL call throws in JS, killing the requestAnimationFrame main loop. The GL `GpuDevice` now fails construction gracefully when no WebGL context is current and `isComputeAvailable()` reports false on a device whose GL initialization failed. `GpuAudioProcessingDemo` also sized its CPU ring buffers only when GPU compute was available, so the no-GPU audio path wrote and read out of bounds
- Fixed `SDLComponentNative::renderFrame()` on Emscripten encoding a frame with a zero-sized render target, which trips Rive's `beginFrame()` assertion — fatal there, since an assertion abort throws inside the `requestAnimationFrame` callback and permanently kills the browser's main loop. It now skips rendering entirely until a non-zero content size has been observed
- 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`
Expand All @@ -35,6 +40,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `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
- `SDLComponentNative` now renders each window on its own dedicated render thread instead of the message thread: the component-tree walk runs under a `MessageManagerLock` while GL command submission and buffer swap happen unlocked, so multiple windows no longer serialize their frame rendering (and vsync waits) on the message thread
- Fixed `SDLComponentNative::repaint()` calling `-[NSWindow screen]` (via `getSize()` → `getWindowUnitsPerPoint()` → `SDL_GetDisplayForWindow()`) from the render thread, which macOS's Main Thread Checker flags since AppKit requires that call on the main thread. It now uses the already up-to-date `screenBounds` cached by the main-thread window event handlers instead of querying the display live
- Fixed `SDLComponentNative::runWithGraphicsContext()` never invoking its callback on non-OpenGL desktop backends (Metal, Direct3D), silently dropping the work. `Component::renderSubtreeOffscreen()` routes through this hook, so any component with a `ComponentEffect` set (e.g. `ComponentEffectsDemo`) rendered nothing on Metal/D3D; `SDLComponentNative::runWithComputeContext()` falls back to the same hook when no dedicated compute context exists, so GPU compute work initiated off the render thread was silently dropped there too
- Fixed `WaitableTimer`'s non-Windows fallback overshooting frame deadlines by several milliseconds on macOS: it blocked for the entire remaining wait on a single `condition_variable::wait_until`, whose wake time is subject to OS scheduling / timer-coalescing latency. It now blocks for the bulk of the wait and closes the last few milliseconds with a tiered busy-wait against `Time::getMillisecondCounterHiRes()`, restoring the precision the pre-`WaitableTimer` implementation had

#### Rive Runtime Bump

Expand Down
9 changes: 9 additions & 0 deletions docs/graphics/rhi/compute-shaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ does not yet expose compute dispatch, so `GpuComputePipeline` and
| WebGPU | `wgpu::ComputePipeline` | `DispatchWorkgroups()` |
| OpenGL | `GL_COMPUTE_SHADER` + program link | `glDispatchCompute()` |

On OpenGL, compute work runs on a dedicated, unshared GL context
(`GpuDevice::Options::computeContextActivator`, routed through
`GpuDevice::runOnComputeContext()`). Some drivers silently stop executing
compute dispatches when they are interleaved with rendering on the same
context — or on any context of the same share group — so pipeline compilation,
dispatches, storage buffer create/update/readback, and the matching deletions
all live exclusively on that context. When no dedicated context is available,
compute falls back to the rendering context.

## Compiling a compute pipeline

### From GLSL (online, requires `YUP_ENABLE_SHADER_TRANSPILER`)
Expand Down
12 changes: 12 additions & 0 deletions docs/multithreading/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ audio-thread safety are still to come.
- **Synchronization** - `CriticalSection`, atomics, and lock-free patterns for
the audio thread.
- **Async** - timers, async updaters, and deferred callbacks.

## Render thread

Each `ComponentNative` window renders on its own dedicated `Thread`. Each
frame runs the `refreshDisplay` walk (component animation/logic) followed by the
repaint of any dirty regions, both under a `MessageManagerLock` so only that
window's tree suspends the message thread; GL command submission
(`GraphicsContext::end`) and the buffer swap happen after the lock is released.
The frame loop wakes on a repaint request or near the frame deadline, so
`refreshDisplay` keeps running at the desired frame rate even when nothing is
dirty. This keeps the message thread responsive with multiple windows open and
ensures vsync waits never serialize input and timer dispatch.
4 changes: 4 additions & 0 deletions examples/graphics/source/examples/AudioFileDemo.h
Original file line number Diff line number Diff line change
Expand Up @@ -576,9 +576,13 @@ class AudioFileDemo : public yup::Component
~AudioFileDemo() override
{
stopPlayback();

waveformThreadPool.removeAllJobs (true, -1);

transportSource.setSource (nullptr);
meteringSource.setLooping (false);
sourcePlayer.setSource (nullptr);

deviceManager.removeAudioCallback (&sourcePlayer);
deviceManager.closeAudioDevice();
}
Expand Down
8 changes: 6 additions & 2 deletions examples/graphics/source/examples/GpuAudioProcessingDemo.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ class GpuAudioProcessingDemo : public yup::Component
}
}

for (int i = 0; i < kRingSize; ++i)
{
cpuUploadBuf[i].resize (static_cast<size_t> (gpuBlockSize));
cpuOutputBuf[i].resize (static_cast<size_t> (gpuBlockSize));
}

if (computeDevice == nullptr || ! computeDevice->isComputeAvailable())
return;

Expand All @@ -203,8 +209,6 @@ class GpuAudioProcessingDemo : public yup::Component
{
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<size_t> (gpuBlockSize));
cpuOutputBuf[i].resize (static_cast<size_t> (gpuBlockSize));
}

writePos = 0;
Expand Down
7 changes: 1 addition & 6 deletions examples/graphics/source/examples/OffscreenRenderDemo.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,7 @@ class OffscreenRenderDemo : public yup::Component
if (capturedContext == nullptr)
{
capturedContext = &g.getGraphicsContext();
yup::MessageManager::callAsync ([this]
{
renderOffscreen();
});
renderOffscreen();
}

auto bounds = getLocalBounds().to<float>().reduced (10.0f);
Expand Down Expand Up @@ -177,8 +174,6 @@ class OffscreenRenderDemo : public yup::Component

saveButton->setEnabled (true);
statusLabel->setText ("Rendered to 256x256 GPU texture. GPU draw active.", yup::dontSendNotification);

repaint();
}

void savePixelsToFile()
Expand Down
32 changes: 13 additions & 19 deletions examples/graphics/source/examples/SpinningCubeDemo.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class SpinningCubeDemo : public yup::Component
}

lottiePlayer.advanceTime (lastFrameTimeSeconds);
repaint();
repaint (getCubeArea());
}

//==============================================================================
Expand Down Expand Up @@ -219,9 +219,13 @@ class SpinningCubeDemo : public yup::Component
// so it can be sampled as a texture by the cube's fragment shader.
yup::GpuTexture::Ptr lottieTexture = renderLottieTexture();

// 3. Render the 3D cube into sceneCanvas via GpuPipeline + GpuRenderPass.
// 3. Render the 3D cube into sceneCanvas via GpuPipeline + GpuRenderPass, and
// apply the blur passes below, all against a single shared GpuFrame - the
// cube pass and both blur passes run on the same command buffer.
auto frame = yup::GpuFrame::begin (capturedContext->getGpuDevice());

if (cubePipeline != nullptr)
renderCube (*sceneCanvas, w, h, lottieTexture);
renderCube (frame, *sceneCanvas, w, h, lottieTexture);

// 4. Apply separable Gaussian blur: two O(radius) passes (H then V).
yup::GpuTexture::Ptr outputTex = sceneCanvas->asTexture();
Expand Down Expand Up @@ -251,9 +255,6 @@ class SpinningCubeDemo : public yup::Component

if (blurCanvasA != nullptr && blurCanvasB != nullptr)
{
// Both blur passes share a single GpuFrame.
auto frame = yup::GpuFrame::begin (capturedContext->getGpuDevice());

auto runPass = [&] (yup::GpuTarget& passCanvas, const yup::GpuTexture::Ptr& input, float dirX, float dirY) -> yup::GpuTexture::Ptr
{
BlurParams params { blurSigma, radius, (float) w, (float) h, dirX, dirY, 0.0f, 0.0f };
Expand All @@ -270,14 +271,14 @@ class SpinningCubeDemo : public yup::Component

outputTex = runPass (*blurCanvasA, outputTex, 1.0f, 0.0f); // horizontal
outputTex = runPass (*blurCanvasB, outputTex, 0.0f, 1.0f); // vertical

// Submit without stalling: all contexts share one command queue,
// so the main frame that samples outputTex is serialised after
// this work on the GPU. No CPU wait is required.
frame.submit();
}
}

// Submit without stalling: all contexts share one command queue, so the
// main frame that samples outputTex is serialised after this work on
// the GPU. No CPU wait is required.
frame.submit();

// 5. Composite to main view.
if (outputTex != nullptr)
g.drawTexture (outputTex, cubeBounds);
Expand Down Expand Up @@ -971,7 +972,7 @@ void main() {

// ---- Per-frame cube render pass -----------------------------------------

void renderCube (yup::GpuTarget& canvas, int w, int h, const yup::GpuTexture::Ptr& lottieTexture)
void renderCube (yup::GpuFrame& frame, yup::GpuTarget& canvas, int w, int h, const yup::GpuTexture::Ptr& lottieTexture)
{
if (cubePipeline == nullptr || cubeVBO == nullptr || cubeIBO == nullptr)
return;
Expand All @@ -984,8 +985,6 @@ void main() {

CubeUniforms uniforms { angleY, angleX, (float) w / (float) h, 0.0f };

auto frame = yup::GpuFrame::begin (capturedContext->getGpuDevice());

auto pass = canvas.beginRenderPass (frame, { true, yup::Color (0xff1a1a2e) });
pass.setPipeline (cubePipeline);
pass.setUniformBuffer (0, 0, &uniforms, sizeof (uniforms));
Expand All @@ -995,11 +994,6 @@ void main() {
pass.setIndexBuffer (yup::GpuIndexFormat::uint16, cubeIBO);
pass.drawIndexed (yup::numElementsInArray (kCubeIdx));
pass.finish();

// Submit without stalling: the shared command queue serialises this
// work ahead of the main frame that samples the scene texture, so no
// CPU wait is needed here.
frame.submit();
}

//==============================================================================
Expand Down
79 changes: 67 additions & 12 deletions examples/graphics/source/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
#include <BinaryData.h>
#endif

// Enable this to enable leak detection tools on windows
// #define YUP_ENABLE_WINDOWS_BREAK_ALLOC 277639

#if YUP_WINDOWS && YUP_ENABLE_WINDOWS_BREAK_ALLOC
#include <crtdbg.h>
#endif

//==============================================================================

inline yup::File getAssetPath (yup::StringRef subPath = {})
Expand Down Expand Up @@ -319,36 +326,80 @@ class CustomWindow

void selectComponent (int index)
{
for (auto* component : components)
{
if (component != nullptr)
component->setVisible (false);
}
if (! yup::isPositiveAndBelow (index, components.size()))
return;

if (components[index] == nullptr)
{
components.set (index, demoFactories[index]().release());
addChildComponent (components[index]);
}

for (int i = 0; i < components.size(); ++i)
{
if (i == index)
continue;

if (components[i] != nullptr)
{
components[i]->setVisible (false);
components.set (i, nullptr);
}
}

resized(); // Ensure the newly created component is sized correctly

components[index]->setVisible (true);
}

private:
void updateWindowTitle()
{
yup::String title;
auto nativeComponent = getNativeComponent();

auto currentFps = getNativeComponent()->getCurrentFrameRate();
auto currentFps = nativeComponent ? nativeComponent->getCurrentFrameRate() : 0.0f;
title << "[" << yup::String (currentFps, 1) << " FPS]";
title << " | YUP On Rive Renderer";
title << " | " << yup::YUPApplication::getInstance()->getApplicationName() << " ";

if (nativeComponent)
{
if (auto context = nativeComponent->getGraphicsContext())
{
switch (context->getPlatform())
{
case yup::GpuPlatform::Direct3D:
title << " | D3D11";
break;

case yup::GpuPlatform::Metal:
title << " | Metal";
break;

case yup::GpuPlatform::OpenGL:
title << " | OpenGL 4.x";
break;

case yup::GpuPlatform::OpenGLES:
title << " | OpenGLES 3.x";
break;

case yup::GpuPlatform::WebGPU:
title << " | WebGPU";
break;

case yup::GpuPlatform::Headless:
title << " | Headless";
break;
}
}

if (getNativeComponent()->isAtomicModeEnabled())
title << " (atomic)";
if (nativeComponent->isAtomicModeEnabled())
title << " (atomic)";

auto [width, height] = getNativeComponent()->getContentSize();
title << " | " << width << " x " << height;
auto [width, height] = nativeComponent->getContentSize();
title << " | " << width << " x " << height;
}

setTitle (title);
}
Expand All @@ -369,7 +420,7 @@ struct Application : yup::YUPApplication

yup::String getApplicationName() override
{
return "yup! graphics";
return "YUP! demos";
}

yup::String getApplicationVersion() override
Expand All @@ -379,6 +430,10 @@ struct Application : yup::YUPApplication

void initialise (const yup::String& commandLineParameters) override
{
#if YUP_WINDOWS && YUP_ENABLE_WINDOWS_BREAK_ALLOC
_CrtSetBreakAlloc (YUP_ENABLE_WINDOWS_BREAK_ALLOC);
#endif

YUP_PROFILE_START();

yup::Logger::outputDebugString ("Starting app " + commandLineParameters);
Expand Down
Loading
Loading