diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cfcd22f3..2feddcbd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,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` @@ -37,6 +42,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 diff --git a/docs/graphics/rhi/compute-shaders.md b/docs/graphics/rhi/compute-shaders.md index 450e1884e..2fe97b176 100644 --- a/docs/graphics/rhi/compute-shaders.md +++ b/docs/graphics/rhi/compute-shaders.md @@ -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`) diff --git a/examples/graphics/source/examples/AudioFileDemo.h b/examples/graphics/source/examples/AudioFileDemo.h index 2d439a0c0..02d7db8f7 100644 --- a/examples/graphics/source/examples/AudioFileDemo.h +++ b/examples/graphics/source/examples/AudioFileDemo.h @@ -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(); } diff --git a/examples/graphics/source/examples/GpuAudioProcessingDemo.h b/examples/graphics/source/examples/GpuAudioProcessingDemo.h index 7e66a932f..f7384234b 100644 --- a/examples/graphics/source/examples/GpuAudioProcessingDemo.h +++ b/examples/graphics/source/examples/GpuAudioProcessingDemo.h @@ -193,6 +193,12 @@ class GpuAudioProcessingDemo : public yup::Component } } + for (int i = 0; i < kRingSize; ++i) + { + cpuUploadBuf[i].resize (static_cast (gpuBlockSize)); + cpuOutputBuf[i].resize (static_cast (gpuBlockSize)); + } + if (computeDevice == nullptr || ! computeDevice->isComputeAvailable()) return; @@ -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 (gpuBlockSize)); - cpuOutputBuf[i].resize (static_cast (gpuBlockSize)); } writePos = 0; diff --git a/examples/graphics/source/examples/OffscreenRenderDemo.h b/examples/graphics/source/examples/OffscreenRenderDemo.h index 3d7fb74c3..cda1bd0e9 100644 --- a/examples/graphics/source/examples/OffscreenRenderDemo.h +++ b/examples/graphics/source/examples/OffscreenRenderDemo.h @@ -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().reduced (10.0f); @@ -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() diff --git a/examples/graphics/source/examples/SpinningCubeDemo.h b/examples/graphics/source/examples/SpinningCubeDemo.h index 05a2d8e58..4ff28b396 100644 --- a/examples/graphics/source/examples/SpinningCubeDemo.h +++ b/examples/graphics/source/examples/SpinningCubeDemo.h @@ -179,7 +179,7 @@ class SpinningCubeDemo : public yup::Component } lottiePlayer.advanceTime (lastFrameTimeSeconds); - repaint(); + repaint (getCubeArea()); } //============================================================================== @@ -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(); @@ -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 }; @@ -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); @@ -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; @@ -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)); @@ -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(); } //============================================================================== diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index af68ea4b7..1ba8987cf 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -40,6 +40,13 @@ #include #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 +#endif + //============================================================================== inline yup::File getAssetPath (yup::StringRef subPath = {}) @@ -319,11 +326,8 @@ 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) { @@ -331,7 +335,20 @@ class CustomWindow 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); } @@ -339,16 +356,50 @@ class CustomWindow 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); } @@ -369,7 +420,7 @@ struct Application : yup::YUPApplication yup::String getApplicationName() override { - return "yup! graphics"; + return "YUP! demos"; } yup::String getApplicationVersion() override @@ -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); diff --git a/modules/yup_core/threads/yup_WaitableTimer.cpp b/modules/yup_core/threads/yup_WaitableTimer.cpp new file mode 100644 index 000000000..9bb577953 --- /dev/null +++ b/modules/yup_core/threads/yup_WaitableTimer.cpp @@ -0,0 +1,89 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== + +WaitableTimer::WaitableTimer() +{ +#if YUP_WINDOWS + handle = CreateWaitableTimerExW (nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); + // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION needs Windows 10 1803+, fall back to a plain waitable timer on older systems. + if (handle == nullptr) + handle = CreateWaitableTimerExW (nullptr, nullptr, 0, TIMER_ALL_ACCESS); +#endif +} + +WaitableTimer::~WaitableTimer() +{ +#if YUP_WINDOWS + if (handle != nullptr) + CloseHandle (handle); +#endif +} + +void WaitableTimer::waitUntil (double milliseconds) +{ +#if YUP_WINDOWS + const auto relativeMs = (milliseconds - 1.0) - Time::getMillisecondCounterHiRes(); + if (relativeMs <= 0.0) + return; + + LARGE_INTEGER dueTime; + dueTime.QuadPart = -static_cast (relativeMs * 10000.0); // relative, in 100ns units + + if (handle != nullptr && SetWaitableTimer (handle, &dueTime, 0, nullptr, nullptr, FALSE) != 0) + { + WaitForSingleObject (handle, INFINITE); + + while (Time::getMillisecondCounterHiRes() < milliseconds) + std::this_thread::yield(); + + return; + } +#endif + + waitUntilFallback (milliseconds); +} + +void WaitableTimer::waitUntilFallback (double milliseconds) +{ + if (const auto nowMs = Time::getMillisecondCounterHiRes(); milliseconds - nowMs > 4.0) + { + const auto target = std::chrono::steady_clock::now() + std::chrono::duration ((milliseconds - 4.0) - nowMs); + + std::unique_lock lock (mutex); + cv.wait_until (lock, target); + } + + while (Time::getMillisecondCounterHiRes() < milliseconds - 4.0) + std::this_thread::sleep_for (std::chrono::microseconds (25)); + + while (Time::getMillisecondCounterHiRes() < milliseconds - 2.0) + std::this_thread::sleep_for (std::chrono::microseconds (10)); + + while (Time::getMillisecondCounterHiRes() < milliseconds) + std::this_thread::sleep_for (std::chrono::microseconds (1)); +} + +} // namespace yup diff --git a/modules/yup_core/threads/yup_WaitableTimer.h b/modules/yup_core/threads/yup_WaitableTimer.h new file mode 100644 index 000000000..0595adee9 --- /dev/null +++ b/modules/yup_core/threads/yup_WaitableTimer.h @@ -0,0 +1,82 @@ +/* + ============================================================================== + + 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 + +namespace yup +{ + +//============================================================================== +/** + A timer that blocks a thread until an exact deadline without polling. + + waitUntil() suspends the calling thread until the given absolute time, + measured in milliseconds on the same clock as + Time::getMillisecondCounterHiRes(). Unlike a plain sleep, the wait is + accurate to well under a millisecond on every platform: + + - On Windows it uses a high-resolution waitable timer + (CreateWaitableTimerExW, with CREATE_WAITABLE_TIMER_HIGH_RESOLUTION when + the OS supports it), which fires on a precise deadline and consumes no + CPU while blocked. Plain sleeps are only as accurate as the 1 ms timer + tick requested via timeBeginPeriod(), so a sleep targeting a deadline + can overshoot it by up to a full tick. + - On other platforms it blocks on a condition variable for the bulk of + the wait (to avoid burning CPU), then finishes with a short, tiered + busy-wait against Time::getMillisecondCounterHiRes() for the last few + milliseconds. The condition-variable wake alone is not precise enough + on its own: OS scheduling and timer-coalescing latency (particularly + on macOS) can make it overshoot the deadline by several milliseconds, + which the trailing busy-wait corrects for. + + Use it for frame pacing or any loop that must meet a deadline rather than + merely sleep for a while. + + @see Time::getMillisecondCounterHiRes +*/ +class YUP_API WaitableTimer +{ +public: + WaitableTimer(); + ~WaitableTimer(); + + /** + Waits until the given absolute time in milliseconds, on the same clock + as Time::getMillisecondCounterHiRes(). + + Returns immediately if the deadline has already passed. + */ + void waitUntil (double milliseconds); + +private: + void waitUntilFallback (double milliseconds); + +#if YUP_WINDOWS + void* handle = nullptr; +#endif + + std::mutex mutex; + std::condition_variable cv; + + YUP_DECLARE_NON_COPYABLE (WaitableTimer) +}; + +} // namespace yup diff --git a/modules/yup_core/yup_core.cpp b/modules/yup_core/yup_core.cpp index 0f1488da7..454442fa3 100644 --- a/modules/yup_core/yup_core.cpp +++ b/modules/yup_core/yup_core.cpp @@ -1,376 +1,377 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - 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. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or 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. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -#ifdef YUP_CORE_H_INCLUDED -/* When you add this cpp file to your project, you mustn't include it in a file where you've - already included any other headers - just put it inside a file on its own, possibly with your config - flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix - header files that the compiler may be using. -*/ -#error "Incorrect use of YUP cpp file" -#endif - -#define YUP_CORE_INCLUDE_OBJC_HELPERS 1 -#define YUP_CORE_INCLUDE_COM_SMART_PTR 1 -#define YUP_CORE_INCLUDE_NATIVE_HEADERS 1 -#define YUP_CORE_INCLUDE_JNI_HELPERS 1 - -#include "yup_core.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#if ! (YUP_ANDROID || YUP_BSD) -#include -#include -#endif - -#if YUP_WINDOWS -YUP_BEGIN_IGNORE_WARNINGS_MSVC (4091) -#include -YUP_END_IGNORE_WARNINGS_MSVC - -#if ! YUP_DONT_AUTOLINK_TO_WIN32_LIBRARIES -#pragma comment(lib, "DbgHelp.lib") -#endif - -#else -#if YUP_LINUX || YUP_BSD || YUP_ANDROID -#include -#include -#include -#include -#include -#endif - -#if YUP_WASM -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#endif - -#if YUP_EMSCRIPTEN -#include -#include -#endif - -#if YUP_LINUX || YUP_BSD -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if __has_include() -#include -#endif - -#if YUP_USE_CURL -#if ! __has_include() -#error "YUP_USE_CURL is explicitly enabled but is not available" -#endif -#include -#endif -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#if ! (YUP_WASM || (YUP_ANDROID && __ANDROID_API__ < 33)) -#include -#endif - -#if ! (YUP_WASM || YUP_WINDOWS) -#include -#endif - -extern char** environ; -#endif - -#if YUP_MAC -#include -#include -#include -#endif - -#if YUP_MAC || YUP_IOS -#include -#include -#endif - -#if YUP_ANDROID -#include -#include -#include -#include -#endif - -#undef check - -//============================================================================== -#include "profiling/yup_Profiler.cpp" - -//============================================================================== -#include "containers/yup_AbstractFifo.cpp" -#include "containers/yup_NamedValueSet.cpp" -#include "containers/yup_PropertySet.cpp" -#include "files/yup_DirectoryIterator.cpp" -#include "files/yup_RangedDirectoryIterator.cpp" -#include "files/yup_File.cpp" -#include "files/yup_FileInputStream.cpp" -#include "files/yup_FileOutputStream.cpp" -#include "files/yup_FileSearchPath.cpp" -#include "files/yup_TemporaryFile.cpp" -#include "logging/yup_FileLogger.cpp" -#include "logging/yup_Logger.cpp" -#include "maths/yup_BigInteger.cpp" -#include "maths/yup_Expression.cpp" -#include "maths/yup_Random.cpp" -#include "memory/yup_MemoryBlock.cpp" -#include "memory/yup_AllocationHooks.cpp" -#include "cryptography/yup_SHA1.cpp" -#include "misc/yup_RuntimePermissions.cpp" -#include "misc/yup_Result.cpp" -#include "misc/yup_Uuid.cpp" -#include "misc/yup_ConsoleApplication.cpp" -#include "misc/yup_ScopeGuard.cpp" -#include "network/yup_MACAddress.cpp" -#include "network/yup_NamedPipe.cpp" -#include "network/yup_Socket.cpp" -#include "network/yup_IPAddress.cpp" -#include "streams/yup_BufferedInputStream.cpp" -#include "streams/yup_FileInputSource.cpp" -#include "streams/yup_InputStream.cpp" -#include "streams/yup_MemoryInputStream.cpp" -#include "streams/yup_MemoryOutputStream.cpp" -#include "streams/yup_SubregionStream.cpp" -#include "system/yup_SystemStats.cpp" -#include "text/yup_CharacterFunctions.cpp" -#include "text/yup_Identifier.cpp" -#include "text/yup_LocalisedStrings.cpp" -#include "text/yup_String.cpp" -#include "streams/yup_OutputStream.cpp" -#include "text/yup_StringArray.cpp" -#include "text/yup_StringPairArray.cpp" -#include "text/yup_StringPool.cpp" -#include "text/yup_TextDiff.cpp" -#include "text/yup_Base64.cpp" -#include "threads/yup_ReadWriteLock.cpp" -#include "threads/yup_SpinLock.cpp" -#include "threads/yup_RecursiveSpinLock.cpp" -#include "threads/yup_Thread.cpp" -#include "threads/yup_ThreadPool.cpp" -#include "threads/yup_TimeSliceThread.cpp" -#include "time/yup_PerformanceCounter.cpp" -#include "time/yup_RelativeTime.cpp" -#include "time/yup_Time.cpp" -#include "containers/yup_Variant.cpp" -#include "javascript/yup_JSON.cpp" -#include "javascript/yup_JSONUtils.cpp" -#include "javascript/yup_Javascript.cpp" -#include "yaml/yup_YAML.cpp" -#include "containers/yup_DynamicObject.cpp" -#include "xml/yup_XmlDocument.cpp" -#include "xml/yup_XmlElement.cpp" -#include "files/yup_FileFilter.cpp" -#include "files/yup_WildcardFileFilter.cpp" -#include "native/yup_ThreadPriorities_native.h" -#include "native/yup_PlatformTimerListener.h" - -//============================================================================== -#if ! YUP_WINDOWS -#include "native/yup_SharedCode_posix.h" -#include "native/yup_NamedPipe_posix.cpp" -#if ! YUP_ANDROID || __ANDROID_API__ >= 24 -#include "native/yup_IPAddress_posix.h" -#endif -#endif - -//============================================================================== -#if YUP_MAC -#include "native/yup_Watchdog_mac.h" -#endif - -//============================================================================== -#if YUP_MAC || YUP_IOS -#include "native/yup_Files_apple.mm" -#include "native/yup_Network_apple.mm" -#include "native/yup_Strings_apple.mm" -#include "native/yup_SharedCode_intel.h" -#include "native/yup_SystemStats_apple.mm" -#include "native/yup_Threads_apple.mm" -#include "native/yup_PlatformTimer_generic.cpp" -#include "native/yup_Process_apple.mm" - -//============================================================================== -#elif YUP_WINDOWS -#include "native/yup_Files_windows.cpp" -#include "native/yup_Network_windows.cpp" -#include "native/yup_Registry_windows.cpp" -#include "native/yup_SystemStats_windows.cpp" -#include "native/yup_Threads_windows.cpp" -#include "native/yup_PlatformTimer_windows.cpp" -#include "native/yup_Watchdog_windows.h" - -//============================================================================== -#elif YUP_LINUX -#include "native/yup_CommonFile_linux.cpp" -#include "native/yup_Files_linux.cpp" -#include "native/yup_Network_linux.cpp" -#if YUP_USE_CURL -#include "native/yup_Network_curl.cpp" -#endif -#include "native/yup_SystemStats_linux.cpp" -#include "native/yup_Threads_linux.cpp" -#include "native/yup_PlatformTimer_generic.cpp" -#include "native/yup_Watchdog_linux.h" - -//============================================================================== -#elif YUP_BSD -#include "native/yup_CommonFile_linux.cpp" -#include "native/yup_Files_linux.cpp" -#include "native/yup_Network_linux.cpp" -#if YUP_USE_CURL -#include "native/yup_Network_curl.cpp" -#endif -#include "native/yup_SharedCode_intel.h" -#include "native/yup_SystemStats_linux.cpp" -#include "native/yup_Threads_linux.cpp" -#include "native/yup_PlatformTimer_generic.cpp" - -//============================================================================== -#elif YUP_ANDROID -#include "native/yup_CommonFile_linux.cpp" -#include "native/yup_JNIHelpers_android.cpp" -#include "native/yup_Files_android.cpp" -#include "native/yup_Misc_android.cpp" -#include "native/yup_Network_android.cpp" -#include "native/yup_SystemStats_android.cpp" -#include "native/yup_Threads_android.cpp" -#include "native/yup_RuntimePermissions_android.cpp" -#include "native/yup_PlatformTimer_generic.cpp" - -//============================================================================== -#elif YUP_WASM -#include "native/yup_WebAssemblyHelpers_wasm.h" -#include "native/yup_SystemStats_wasm.cpp" -#include "native/yup_Files_wasm.cpp" -#include "native/yup_Network_wasm.cpp" -#include "native/yup_Threads_wasm.cpp" -#include "native/yup_CommonFile_linux.cpp" -#include "native/yup_PlatformTimer_generic.cpp" -#endif - -#include "files/yup_common_MimeTypes.h" -#include "files/yup_common_MimeTypes.cpp" -#include "native/yup_AndroidDocument_android.cpp" -#include "threads/yup_HighResolutionTimer.cpp" -#include "threads/yup_WaitableEvent.cpp" -#include "threads/yup_CancelToken.cpp" -#include "threads/yup_CancelTokenSource.cpp" -#include "network/yup_URL.cpp" -#include "network/yup_WebInputStream.cpp" -#include "streams/yup_URLInputSource.cpp" - -#if ! YUP_WASM -#include "threads/yup_ChildProcess.cpp" -#endif - -//============================================================================== -#include - -#include "zip/yup_GZIPDecompressorInputStream.cpp" -#include "zip/yup_GZIPCompressorOutputStream.cpp" -#include "zip/yup_ZipFile.cpp" - -//============================================================================== -#include "files/yup_Watchdog.cpp" - -//============================================================================== -#if YUP_MODULE_AVAILABLE_sqlite3_library -#include "database/yup_SqliteDatabase.cpp" -#endif - -//============================================================================== -namespace yup -{ -/* - As the very long class names here try to explain, the purpose of this code is to cause - a linker error if not all of your compile units are consistent in the options that they - enable before including YUP headers. The reason this is important is that if you have - two cpp files, and one includes the yup headers with debug enabled, and the other doesn't, - then each will be generating code with different memory layouts for the classes, and - you'll get subtle and hard-to-track-down memory corruption bugs! -*/ -#if YUP_DEBUG -this_will_fail_to_link_if_some_of_your_compile_units_are_built_in_debug_mode ::this_will_fail_to_link_if_some_of_your_compile_units_are_built_in_debug_mode() noexcept -{ -} -#else -this_will_fail_to_link_if_some_of_your_compile_units_are_built_in_release_mode ::this_will_fail_to_link_if_some_of_your_compile_units_are_built_in_release_mode() noexcept -{ -} -#endif -} // namespace yup +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - 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. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or 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. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifdef YUP_CORE_H_INCLUDED +/* When you add this cpp file to your project, you mustn't include it in a file where you've + already included any other headers - just put it inside a file on its own, possibly with your config + flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix + header files that the compiler may be using. +*/ +#error "Incorrect use of YUP cpp file" +#endif + +#define YUP_CORE_INCLUDE_OBJC_HELPERS 1 +#define YUP_CORE_INCLUDE_COM_SMART_PTR 1 +#define YUP_CORE_INCLUDE_NATIVE_HEADERS 1 +#define YUP_CORE_INCLUDE_JNI_HELPERS 1 + +#include "yup_core.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if ! (YUP_ANDROID || YUP_BSD) +#include +#include +#endif + +#if YUP_WINDOWS +YUP_BEGIN_IGNORE_WARNINGS_MSVC (4091) +#include +YUP_END_IGNORE_WARNINGS_MSVC + +#if ! YUP_DONT_AUTOLINK_TO_WIN32_LIBRARIES +#pragma comment(lib, "DbgHelp.lib") +#endif + +#else +#if YUP_LINUX || YUP_BSD || YUP_ANDROID +#include +#include +#include +#include +#include +#endif + +#if YUP_WASM +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#if YUP_EMSCRIPTEN +#include +#include +#endif + +#if YUP_LINUX || YUP_BSD +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __has_include() +#include +#endif + +#if YUP_USE_CURL +#if ! __has_include() +#error "YUP_USE_CURL is explicitly enabled but is not available" +#endif +#include +#endif +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#if ! (YUP_WASM || (YUP_ANDROID && __ANDROID_API__ < 33)) +#include +#endif + +#if ! (YUP_WASM || YUP_WINDOWS) +#include +#endif + +extern char** environ; +#endif + +#if YUP_MAC +#include +#include +#include +#endif + +#if YUP_MAC || YUP_IOS +#include +#include +#endif + +#if YUP_ANDROID +#include +#include +#include +#include +#endif + +#undef check + +//============================================================================== +#include "profiling/yup_Profiler.cpp" + +//============================================================================== +#include "containers/yup_AbstractFifo.cpp" +#include "containers/yup_NamedValueSet.cpp" +#include "containers/yup_PropertySet.cpp" +#include "files/yup_DirectoryIterator.cpp" +#include "files/yup_RangedDirectoryIterator.cpp" +#include "files/yup_File.cpp" +#include "files/yup_FileInputStream.cpp" +#include "files/yup_FileOutputStream.cpp" +#include "files/yup_FileSearchPath.cpp" +#include "files/yup_TemporaryFile.cpp" +#include "logging/yup_FileLogger.cpp" +#include "logging/yup_Logger.cpp" +#include "maths/yup_BigInteger.cpp" +#include "maths/yup_Expression.cpp" +#include "maths/yup_Random.cpp" +#include "memory/yup_MemoryBlock.cpp" +#include "memory/yup_AllocationHooks.cpp" +#include "cryptography/yup_SHA1.cpp" +#include "misc/yup_RuntimePermissions.cpp" +#include "misc/yup_Result.cpp" +#include "misc/yup_Uuid.cpp" +#include "misc/yup_ConsoleApplication.cpp" +#include "misc/yup_ScopeGuard.cpp" +#include "network/yup_MACAddress.cpp" +#include "network/yup_NamedPipe.cpp" +#include "network/yup_Socket.cpp" +#include "network/yup_IPAddress.cpp" +#include "streams/yup_BufferedInputStream.cpp" +#include "streams/yup_FileInputSource.cpp" +#include "streams/yup_InputStream.cpp" +#include "streams/yup_MemoryInputStream.cpp" +#include "streams/yup_MemoryOutputStream.cpp" +#include "streams/yup_SubregionStream.cpp" +#include "system/yup_SystemStats.cpp" +#include "text/yup_CharacterFunctions.cpp" +#include "text/yup_Identifier.cpp" +#include "text/yup_LocalisedStrings.cpp" +#include "text/yup_String.cpp" +#include "streams/yup_OutputStream.cpp" +#include "text/yup_StringArray.cpp" +#include "text/yup_StringPairArray.cpp" +#include "text/yup_StringPool.cpp" +#include "text/yup_TextDiff.cpp" +#include "text/yup_Base64.cpp" +#include "threads/yup_ReadWriteLock.cpp" +#include "threads/yup_SpinLock.cpp" +#include "threads/yup_RecursiveSpinLock.cpp" +#include "threads/yup_Thread.cpp" +#include "threads/yup_ThreadPool.cpp" +#include "threads/yup_TimeSliceThread.cpp" +#include "time/yup_PerformanceCounter.cpp" +#include "time/yup_RelativeTime.cpp" +#include "time/yup_Time.cpp" +#include "containers/yup_Variant.cpp" +#include "javascript/yup_JSON.cpp" +#include "javascript/yup_JSONUtils.cpp" +#include "javascript/yup_Javascript.cpp" +#include "yaml/yup_YAML.cpp" +#include "containers/yup_DynamicObject.cpp" +#include "xml/yup_XmlDocument.cpp" +#include "xml/yup_XmlElement.cpp" +#include "files/yup_FileFilter.cpp" +#include "files/yup_WildcardFileFilter.cpp" +#include "native/yup_ThreadPriorities_native.h" +#include "native/yup_PlatformTimerListener.h" + +//============================================================================== +#if ! YUP_WINDOWS +#include "native/yup_SharedCode_posix.h" +#include "native/yup_NamedPipe_posix.cpp" +#if ! YUP_ANDROID || __ANDROID_API__ >= 24 +#include "native/yup_IPAddress_posix.h" +#endif +#endif + +//============================================================================== +#if YUP_MAC +#include "native/yup_Watchdog_mac.h" +#endif + +//============================================================================== +#if YUP_MAC || YUP_IOS +#include "native/yup_Files_apple.mm" +#include "native/yup_Network_apple.mm" +#include "native/yup_Strings_apple.mm" +#include "native/yup_SharedCode_intel.h" +#include "native/yup_SystemStats_apple.mm" +#include "native/yup_Threads_apple.mm" +#include "native/yup_PlatformTimer_generic.cpp" +#include "native/yup_Process_apple.mm" + +//============================================================================== +#elif YUP_WINDOWS +#include "native/yup_Files_windows.cpp" +#include "native/yup_Network_windows.cpp" +#include "native/yup_Registry_windows.cpp" +#include "native/yup_SystemStats_windows.cpp" +#include "native/yup_Threads_windows.cpp" +#include "native/yup_PlatformTimer_windows.cpp" +#include "native/yup_Watchdog_windows.h" + +//============================================================================== +#elif YUP_LINUX +#include "native/yup_CommonFile_linux.cpp" +#include "native/yup_Files_linux.cpp" +#include "native/yup_Network_linux.cpp" +#if YUP_USE_CURL +#include "native/yup_Network_curl.cpp" +#endif +#include "native/yup_SystemStats_linux.cpp" +#include "native/yup_Threads_linux.cpp" +#include "native/yup_PlatformTimer_generic.cpp" +#include "native/yup_Watchdog_linux.h" + +//============================================================================== +#elif YUP_BSD +#include "native/yup_CommonFile_linux.cpp" +#include "native/yup_Files_linux.cpp" +#include "native/yup_Network_linux.cpp" +#if YUP_USE_CURL +#include "native/yup_Network_curl.cpp" +#endif +#include "native/yup_SharedCode_intel.h" +#include "native/yup_SystemStats_linux.cpp" +#include "native/yup_Threads_linux.cpp" +#include "native/yup_PlatformTimer_generic.cpp" + +//============================================================================== +#elif YUP_ANDROID +#include "native/yup_CommonFile_linux.cpp" +#include "native/yup_JNIHelpers_android.cpp" +#include "native/yup_Files_android.cpp" +#include "native/yup_Misc_android.cpp" +#include "native/yup_Network_android.cpp" +#include "native/yup_SystemStats_android.cpp" +#include "native/yup_Threads_android.cpp" +#include "native/yup_RuntimePermissions_android.cpp" +#include "native/yup_PlatformTimer_generic.cpp" + +//============================================================================== +#elif YUP_WASM +#include "native/yup_WebAssemblyHelpers_wasm.h" +#include "native/yup_SystemStats_wasm.cpp" +#include "native/yup_Files_wasm.cpp" +#include "native/yup_Network_wasm.cpp" +#include "native/yup_Threads_wasm.cpp" +#include "native/yup_CommonFile_linux.cpp" +#include "native/yup_PlatformTimer_generic.cpp" +#endif + +#include "files/yup_common_MimeTypes.h" +#include "files/yup_common_MimeTypes.cpp" +#include "native/yup_AndroidDocument_android.cpp" +#include "threads/yup_HighResolutionTimer.cpp" +#include "threads/yup_WaitableEvent.cpp" +#include "threads/yup_WaitableTimer.cpp" +#include "threads/yup_CancelToken.cpp" +#include "threads/yup_CancelTokenSource.cpp" +#include "network/yup_URL.cpp" +#include "network/yup_WebInputStream.cpp" +#include "streams/yup_URLInputSource.cpp" + +#if ! YUP_WASM +#include "threads/yup_ChildProcess.cpp" +#endif + +//============================================================================== +#include + +#include "zip/yup_GZIPDecompressorInputStream.cpp" +#include "zip/yup_GZIPCompressorOutputStream.cpp" +#include "zip/yup_ZipFile.cpp" + +//============================================================================== +#include "files/yup_Watchdog.cpp" + +//============================================================================== +#if YUP_MODULE_AVAILABLE_sqlite3_library +#include "database/yup_SqliteDatabase.cpp" +#endif + +//============================================================================== +namespace yup +{ +/* + As the very long class names here try to explain, the purpose of this code is to cause + a linker error if not all of your compile units are consistent in the options that they + enable before including YUP headers. The reason this is important is that if you have + two cpp files, and one includes the yup headers with debug enabled, and the other doesn't, + then each will be generating code with different memory layouts for the classes, and + you'll get subtle and hard-to-track-down memory corruption bugs! +*/ +#if YUP_DEBUG +this_will_fail_to_link_if_some_of_your_compile_units_are_built_in_debug_mode ::this_will_fail_to_link_if_some_of_your_compile_units_are_built_in_debug_mode() noexcept +{ +} +#else +this_will_fail_to_link_if_some_of_your_compile_units_are_built_in_release_mode ::this_will_fail_to_link_if_some_of_your_compile_units_are_built_in_release_mode() noexcept +{ +} +#endif +} // namespace yup diff --git a/modules/yup_core/yup_core.h b/modules/yup_core/yup_core.h index f547c47d2..b8f8de9be 100644 --- a/modules/yup_core/yup_core.h +++ b/modules/yup_core/yup_core.h @@ -367,6 +367,7 @@ YUP_END_IGNORE_WARNINGS_MSVC #include "threads/yup_Process.h" #include "threads/yup_SpinLock.h" #include "threads/yup_WaitableEvent.h" +#include "threads/yup_WaitableTimer.h" #include "threads/yup_CancelToken.h" #include "threads/yup_CancelTokenSource.h" #include "threads/yup_Thread.h" diff --git a/modules/yup_dsp/onsets/yup_FilterBank.cpp b/modules/yup_dsp/onsets/yup_FilterBank.cpp index bb85394a6..9facf350f 100644 --- a/modules/yup_dsp/onsets/yup_FilterBank.cpp +++ b/modules/yup_dsp/onsets/yup_FilterBank.cpp @@ -63,7 +63,7 @@ void FilterBank::build (int bandsPerOctave, float fMin, float fMax, int numFFTBi const int bands = static_cast (frequencies.size()) - 2; jassert (bands >= 3); - matrix.assign (static_cast (numFFTBins) * static_cast (bands), 0.0f); + matrix.assign (static_cast (numFFTBins) * static_cast (bands) + static_cast (bands), 0.0f); numBands = bands; for (int band = 0; band < bands; ++band) diff --git a/modules/yup_graphics/context/yup_GraphicsContext.h b/modules/yup_graphics/context/yup_GraphicsContext.h index 8280f6527..2668a929e 100644 --- a/modules/yup_graphics/context/yup_GraphicsContext.h +++ b/modules/yup_graphics/context/yup_GraphicsContext.h @@ -113,8 +113,27 @@ class YUP_API GraphicsContext virtual std::unique_ptr makeRenderer (int width, int height) = 0; //============================================================================== + /** Attaches the rendering surface to the native window or view. + + This is called on the message thread and is the only place where a + backend is allowed to touch native UI (e.g. attaching a Metal + `CAMetalLayer` to an `NSView`, or creating a D3D swapchain for an + `HWND`). Implementations that do not need native UI may leave it as a + no-op. + + @param nativeHandle A platform-specific handle to the native window or view. + @param width The initial surface width in pixels. + @param height The initial surface height in pixels. + @param dpiScale The scale factor for high-DPI displays. + */ + virtual void attachToWindow (void* nativeHandle, int width, int height, float dpiScale) {} + /** Handles changes in the size of the rendering surface. + Unlike attachToWindow(), this runs on the render thread (so it must not + touch native UI) and is responsible for resizing the GPU render target + and any dependent textures to the new dimensions. + @param nativeHandle A platform-specific handle to the native window or screen. @param width The new width of the surface. @param height The new height of the surface. diff --git a/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp b/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp index aa3f08316..3ddb690ac 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_d3d.cpp @@ -66,6 +66,7 @@ class GraphicsContextD3D : public GraphicsContext if (! isHeadless) { swapchain.Reset(); + cachedBackbuffer.Reset(); DXGI_SWAP_CHAIN_DESC1 scd {}; scd.Width = width; scd.Height = height; @@ -121,16 +122,20 @@ class GraphicsContextD3D : public GraphicsContext renderTarget->setTargetTexture (headlessDrawTexture); else { - ComPtr backbuffer; - HRESULT hr = swapchain->GetBuffer (0, __uuidof (ID3D11Texture2D), reinterpret_cast (backbuffer.ReleaseAndGetAddressOf())); - if (FAILED (hr)) + if (cachedBackbuffer == nullptr) { - auto reason = device->GetDeviceRemovedReason(); - fprintf (stderr, "D3D: GetBuffer failed: hr=0x%08X, deviceRemovedReason=0x%08X\n", static_cast (hr), static_cast (reason)); - renderTarget->setTargetTexture (nullptr); - return; + HRESULT hr = swapchain->GetBuffer (0, __uuidof (ID3D11Texture2D), reinterpret_cast (cachedBackbuffer.ReleaseAndGetAddressOf())); + if (FAILED (hr)) + { + auto reason = device->GetDeviceRemovedReason(); + fprintf (stderr, "D3D: GetBuffer failed: hr=0x%08X, deviceRemovedReason=0x%08X\n", static_cast (hr), static_cast (reason)); + cachedBackbuffer.Reset(); + renderTarget->setTargetTexture (nullptr); + return; + } + + renderTarget->setTargetTexture (cachedBackbuffer); } - renderTarget->setTargetTexture (backbuffer); } } @@ -152,7 +157,6 @@ class GraphicsContextD3D : public GraphicsContext } } - renderTarget->setTargetTexture (nullptr); } private: @@ -163,6 +167,7 @@ class GraphicsContextD3D : public GraphicsContext ComPtr device; ComPtr deviceContext; ComPtr swapchain; + ComPtr cachedBackbuffer; ComPtr readbackTexture; ComPtr headlessDrawTexture; rive::rcp renderTarget; diff --git a/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp b/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp index 310880431..faa461d92 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp @@ -150,36 +150,41 @@ class GraphicsContextMetal : public GraphicsContext //============================================================================== - void onSizeChanged (void* window, int width, int height, float dpiScale, uint32_t sampleCount) override + void attachToWindow (void* window, int width, int height, float dpiScale) override { + if (swapchain != nil) + return; + #if YUP_MAC NSWindow* nsWindow = (__bridge NSWindow*) window; NSView* nsView = [nsWindow contentView]; + nsView.wantsLayer = YES; #endif - if (swapchain == nil) - { + swapchain = [CAMetalLayer layer]; + swapchain.device = gpu; + swapchain.opaque = YES; + swapchain.framebufferOnly = ! options.readableFramebuffer; + swapchain.pixelFormat = MTLPixelFormatBGRA8Unorm; #if YUP_MAC - nsView.wantsLayer = YES; -#endif - - swapchain = [CAMetalLayer layer]; - swapchain.device = gpu; - swapchain.opaque = YES; - swapchain.framebufferOnly = ! options.readableFramebuffer; - swapchain.pixelFormat = MTLPixelFormatBGRA8Unorm; -#if YUP_MAC - swapchain.displaySyncEnabled = NO; + swapchain.displaySyncEnabled = NO; #endif #if YUP_IOS - UIView* view = (__bridge UIView*) window; - swapchain.frame = view.bounds; - [view.layer addSublayer:swapchain]; + UIView* view = (__bridge UIView*) window; + swapchain.frame = view.bounds; + [view.layer addSublayer:swapchain]; #else - nsView.layer = swapchain; + nsView.layer = swapchain; #endif - } + + swapchain.contentsScale = dpiScale; + swapchain.drawableSize = CGSizeMake (width, height); + } + + void onSizeChanged (void*, int width, int height, float dpiScale, uint32_t) override + { + jassert (swapchain != nil); swapchain.contentsScale = dpiScale; swapchain.drawableSize = CGSizeMake (width, height); diff --git a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp index 7dd384e00..cfa992a1c 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp @@ -109,6 +109,9 @@ class GraphicsContextOpenGL : public GraphicsContext cleanupOffscreenResources(); glGenTextures (1, &offscreenTexture); + if (offscreenTexture == 0) + fprintf (stderr, "createOffscreenResources: glGenTextures returned 0 (no current/usable GL context?) glGetError=0x%x\n", glGetError()); + 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); @@ -123,12 +126,15 @@ class GraphicsContextOpenGL : public GraphicsContext glBindTexture (GL_TEXTURE_2D, 0); glGenFramebuffers (1, &offscreenFramebuffer); + if (offscreenFramebuffer == 0) + fprintf (stderr, "createOffscreenResources: glGenFramebuffers returned 0 glGetError=0x%x\n", glGetError()); + 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) - fprintf (stderr, "Offscreen framebuffer is not complete: 0x%x\n", status); + fprintf (stderr, "Offscreen framebuffer is not complete: status=0x%x, fbo=%u, tex=%u\n", status, offscreenFramebuffer, offscreenTexture); glBindFramebuffer (GL_FRAMEBUFFER, 0); @@ -158,9 +164,17 @@ class GraphicsContextOpenGL : public GraphicsContext fprintf (stderr, "blitToMainFramebuffer: Invalid program or texture\n"); return; } + + const GLboolean scissorWasEnabled = glIsEnabled (GL_SCISSOR_TEST); + if (scissorWasEnabled) + glDisable (GL_SCISSOR_TEST); + glBindFramebuffer (GL_READ_FRAMEBUFFER, offscreenFramebuffer); glBindFramebuffer (GL_DRAW_FRAMEBUFFER, 0); glBlitFramebuffer (0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + if (scissorWasEnabled) + glEnable (GL_SCISSOR_TEST); } Options options; diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 3522b01fa..d51824108 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -1947,6 +1947,7 @@ class YUP_API Rectangle return { xy.roundToInt(), size.roundToInt() }; } + /** Returns the rectangle wrapped at the nearest integer. */ template [[nodiscard]] constexpr auto toNearestInt() const noexcept -> std::enable_if_t, Rectangle> @@ -1954,6 +1955,19 @@ class YUP_API Rectangle return { xy.toNearestInt(), size.toNearestInt() }; } + /** Returns the smallest rectangle container aligned at integer boundaries. */ + template + [[nodiscard]] constexpr auto smallestIntContainer() const noexcept + -> std::enable_if_t, Rectangle> + { + const auto x = std::floor (xy.getX()); + const auto y = std::floor (xy.getY()); + const auto right = std::ceil (xy.getX() + size.getWidth()); + const auto bottom = std::ceil (xy.getY() + size.getHeight()); + + return { x, y, right - x, bottom - y }; + } + //============================================================================== /** Multiplies the size and position of the rectangle by a scale factor. diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index 195d6feb0..c3d5c1915 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -1243,43 +1243,77 @@ GpuCanvas::Ptr Component::renderSnapshotOffscreen (GraphicsContext& ctx, bool in if (getWidth() <= 0.0f || getHeight() <= 0.0f) return nullptr; - auto canvas = renderSubtreeOffscreen (ctx, getOpacity(), false); - if (canvas == nullptr) - return nullptr; + const auto renderSnapshot = [&] () -> GpuCanvas::Ptr + { + auto canvas = renderSubtreeOffscreen (ctx, getOpacity(), false); + if (canvas == nullptr) + return nullptr; - if (! includeEffects || componentEffect == nullptr) - return canvas; + if (! includeEffects || componentEffect == nullptr) + return canvas; - auto texture = canvas->asTexture(); + auto texture = canvas->asTexture(); - auto effectCanvas = GpuCanvas::create (ctx, canvas->getWidth(), canvas->getHeight()); - if (effectCanvas == nullptr) - return canvas; + auto effectCanvas = GpuCanvas::create (ctx, canvas->getWidth(), canvas->getHeight()); + if (effectCanvas == nullptr) + return canvas; - auto& g = effectCanvas->beginDraw(); - auto localBounds = getLocalBounds(); - g.setDrawingArea (localBounds); - componentEffect->apply (g, texture, localBounds); + auto& g = effectCanvas->beginDraw(); + auto localBounds = getLocalBounds(); + g.setDrawingArea (localBounds); + componentEffect->apply (g, texture, localBounds); + + return effectCanvas; + }; + + if (auto* nativeComponent = getNativeComponent()) + { + GpuCanvas::Ptr canvas; + nativeComponent->runWithGraphicsContext ([&] { canvas = renderSnapshot(); }); + return canvas; + } - return effectCanvas; + return renderSnapshot(); } Image Component::snapshotToImage (GraphicsContext& ctx, bool includeEffects) { - auto canvas = renderSnapshotOffscreen (ctx, includeEffects); - if (canvas == nullptr) - return {}; + Image result; + const auto takeSnapshot = [&] + { + auto canvas = renderSnapshotOffscreen (ctx, includeEffects); + if (canvas == nullptr) + return; + + result = canvas->asImage(); + }; + + if (auto* nativeComponent = getNativeComponent()) + nativeComponent->runWithGraphicsContext (takeSnapshot); + else + takeSnapshot(); - return canvas->asImage(); + return result; } GpuTexture::Ptr Component::snapshotToTexture (GraphicsContext& ctx, bool includeEffects) { - auto canvas = renderSnapshotOffscreen (ctx, includeEffects); - if (canvas == nullptr) - return nullptr; + GpuTexture::Ptr result; + const auto takeSnapshot = [&] + { + auto canvas = renderSnapshotOffscreen (ctx, includeEffects); + if (canvas == nullptr) + return; + + result = canvas->asTexture(); + }; - return canvas->asTexture(); + if (auto* nativeComponent = getNativeComponent()) + nativeComponent->runWithGraphicsContext (takeSnapshot); + else + takeSnapshot(); + + return result; } //============================================================================== @@ -1344,34 +1378,46 @@ GpuCanvas::Ptr Component::renderSubtreeOffscreen (GraphicsContext& ctx, float op if (getWidth() <= 0.0f || getHeight() <= 0.0f) return nullptr; - const auto w = static_cast (getWidth()); - const auto h = static_cast (getHeight()); - - GpuCanvas::Ptr canvas; - if (reuseCanvas != nullptr && reuseCanvas->getWidth() == w && reuseCanvas->getHeight() == h) - { - canvas = std::move (reuseCanvas); - } - else + const auto renderOffscreen = [&] () -> GpuCanvas::Ptr { - reuseCanvas = nullptr; - canvas = GpuCanvas::create (ctx, w, h); - } + const auto w = static_cast (getWidth()); + const auto h = static_cast (getHeight()); - if (canvas == nullptr) - return nullptr; + 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; + + auto& offscreenG = canvas->beginDraw(); - auto& offscreenG = canvas->beginDraw(); + options.paintAsOffscreenRoot = true; - options.paintAsOffscreenRoot = true; + auto localBounds = getLocalBounds(); + paintSubtree (offscreenG, localBounds, localBounds, opacity, renderContinuous); - auto localBounds = getLocalBounds(); - paintSubtree (offscreenG, localBounds, localBounds, opacity, renderContinuous); + options.paintAsOffscreenRoot = false; - options.paintAsOffscreenRoot = false; + canvas->commit(); + return canvas; + }; + + if (auto* nativeComponent = getNativeComponent()) + { + GpuCanvas::Ptr canvas; + nativeComponent->runWithGraphicsContext ([&] { canvas = renderOffscreen(); }); + return canvas; + } - canvas->commit(); - return canvas; + return renderOffscreen(); } //============================================================================== diff --git a/modules/yup_gui/component/yup_ComponentNative.h b/modules/yup_gui/component/yup_ComponentNative.h index 789af262f..3b7c76b02 100644 --- a/modules/yup_gui/component/yup_ComponentNative.h +++ b/modules/yup_gui/component/yup_ComponentNative.h @@ -213,6 +213,22 @@ class YUP_API ComponentNative : public ReferenceCountedObject /** Destructor. */ virtual ~ComponentNative(); + //============================================================================== + /** Runs @a fn with the native GPU context made current on this thread, when + the backend requires it. + + OpenGL contexts are thread-affine, and the windowing layer binds the + context to a dedicated render thread. Offscreen GPU work initiated from + other threads (e.g. component snapshots taken from the message thread) + must run through this hook so the context is bound, and its access is + serialized with the render thread, for the duration of @a fn. + + The default implementation simply invokes @a fn. + + @param fn The GPU work to run with the context current. + */ + virtual void runWithGraphicsContext (const std::function& fn) { fn(); } + //============================================================================== /** Sets the window title. diff --git a/modules/yup_gui/native/yup_Windowing_sdl.cpp b/modules/yup_gui/native/yup_Windowing_sdl.cpp index 70eb48f92..a7eaa5d25 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl.cpp @@ -84,8 +84,6 @@ SDLComponentNative::SDLComponentNative (Component& component, #if YUP_WINDOWS if (parent != nullptr) { - // Register a plain window class to avoid triggering SDL's WndProc during creation - // (SDL will subclass it afterwards when wrapping the existing HWND). static const wchar_t childWindowClass[] = L"YUPChildWindow"; static bool childWindowClassRegistered = false; @@ -170,6 +168,17 @@ SDLComponentNative::SDLComponentNative (Component& component, } SDL_GL_MakeCurrent (window, windowContext); + +#if ! YUP_EMSCRIPTEN + SDL_GL_SetAttribute (SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0); + computeContext = SDL_GL_CreateContext (window); + + if (computeContext == nullptr) + Logger::outputDebugString ("SDL: unable to create GL compute context, compute will fall back to the window context: " + String (SDL_GetError())); + + SDL_GL_MakeCurrent (window, windowContext); +#endif + YUP_MODULE_DBG (GUI_WINDOWING, "SDL: created GL context"); } @@ -180,6 +189,15 @@ SDLComponentNative::SDLComponentNative (Component& component, { return reinterpret_cast (SDL_GL_GetProcAddress (name)); }; + graphicsOptions.contextActivator = [this] (const std::function& fn) + { + runWithGraphicsContext (fn); + }; + graphicsOptions.computeContextActivator = [this] (const std::function& fn) + { + runWithComputeContext (fn); + }; + context = GraphicsContext::createContext (currentGraphicsApi, graphicsOptions); if (context == nullptr) { @@ -196,10 +214,23 @@ SDLComponentNative::SDLComponentNative (Component& component, jmax (1, screenBounds.getWidth()), jmax (1, screenBounds.getHeight()) }); + // Attach the graphics surface to the native view on the message thread. + { + const auto contentSize = getContentSize(); + context->attachToWindow (getNativeHandle(), contentSize.getWidth(), contentSize.getHeight(), getScaleDpi()); + } + // Check mouse capture if (shouldCaptureMouse && isVisible()) updateMouseCapture (true); + // Release the GL context on the message thread + if constexpr (! renderDrivenByTimer) + { + if (currentGraphicsApi == GpuPlatform::OpenGL || currentGraphicsApi == GpuPlatform::OpenGLES) + SDL_GL_MakeCurrent (window, nullptr); + } + // Start the rendering startRendering(); @@ -215,9 +246,6 @@ SDLComponentNative::~SDLComponentNative() // Stop the rendering first, before touching any SDL resources stopRendering(); - // Cancel any pending async update that may have been scheduled by the render thread - cancelPendingUpdate(); - // Remove event watch SDL_RemoveEventWatch (eventDispatcher, this); YUP_MODULE_DBG (GUI_WINDOWING, "SDL: unregistered window event watch"); @@ -233,7 +261,23 @@ SDLComponentNative::~SDLComponentNative() // Destroy the window if (window != nullptr) { + SDL_GL_MakeCurrent (window, nullptr); + + if (computeContext != nullptr) + { + SDL_GL_DestroyContext (computeContext); + computeContext = nullptr; + } + + if (windowContext != nullptr) + { + SDL_GL_DestroyContext (windowContext); + windowContext = nullptr; + } + + SDL_DestroyProperties (SDL_GetWindowProperties (window)); SDL_DestroyWindow (window); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL: destroyed window"); window = nullptr; } @@ -312,9 +356,6 @@ void SDLComponentNative::toFront() Size SDLComponentNative::getContentSize() const { - // The drawable size must match the real window surface: deriving it from the - // logical size multiplied by a scale factor would drift from the actual pixel - // size on platforms where SDL window coordinates are physical pixels. if (window != nullptr) { int width = 0, height = 0; @@ -324,7 +365,7 @@ Size SDLComponentNative::getContentSize() const return { width, height }; } - return { jmax (1, screenBounds.getWidth()), jmax (1, screenBounds.getHeight()) }; + return { jmax (1, currentContentWidth), jmax (1, currentContentHeight) }; } //============================================================================== @@ -646,24 +687,24 @@ void SDLComponentNative::enableContinuousRepainting (bool shouldBeEnabled) bool SDLComponentNative::isAtomicModeEnabled() const { - return renderAtomicMode; + return renderAtomicMode.load (std::memory_order_relaxed); } void SDLComponentNative::enableAtomicMode (bool shouldBeEnabled) { - renderAtomicMode = shouldBeEnabled; + renderAtomicMode.store (shouldBeEnabled, std::memory_order_relaxed); repaint(); } bool SDLComponentNative::isWireframeEnabled() const { - return renderWireframe; + return renderWireframe.load (std::memory_order_relaxed); } void SDLComponentNative::enableWireframe (bool shouldBeEnabled) { - renderWireframe = shouldBeEnabled; + renderWireframe.store (shouldBeEnabled, std::memory_order_relaxed); repaint(); } @@ -672,14 +713,27 @@ void SDLComponentNative::enableWireframe (bool shouldBeEnabled) void SDLComponentNative::repaint() { - currentRepaintAreas.clearQuick(); + const auto fullArea = Rectangle().withSize (screenBounds.getSize().to()); + + { + const ScopedLock sl (repaintLock); + currentRepaintAreas.clearQuick(); + currentRepaintAreas.add (fullArea); + } - currentRepaintAreas.add (Rectangle().withSize (getSize().to())); + if constexpr (! renderDrivenByTimer) + renderEvent.signal(); } void SDLComponentNative::repaint (const Rectangle& rect) { - currentRepaintAreas.add (rect); + { + const ScopedLock sl (repaintLock); + currentRepaintAreas.add (rect); + } + + if constexpr (! renderDrivenByTimer) + renderEvent.signal(); } const RectangleList& SDLComponentNative::getRepaintAreas() const @@ -801,50 +855,100 @@ void SDLComponentNative::run() const double maxFrameTimeSeconds = 1.0 / static_cast (desiredFrameRate); const double maxFrameTimeMs = maxFrameTimeSeconds * 1000.0; + WaitableTimer frameTimer; + const auto waitUntil = [&frameTimer] (double waitUntilSeconds) + { + frameTimer.waitUntil (waitUntilSeconds * 1000.0); + }; + while (! threadShouldExit()) { - double frameStartTimeSeconds = yup::Time::getMillisecondCounterHiRes() / 1000.0; + const double frameStartTimeSeconds = yup::Time::getMillisecondCounterHiRes() / 1000.0; - // Trigger and wait for rendering - renderEvent.reset(); - cancelPendingUpdate(); - triggerAsyncUpdate(); renderEvent.wait (maxFrameTimeMs - 4.0); + renderEvent.reset(); if (threadShouldExit()) break; - // Measure spent time and cap the framerate - double currentTimeSeconds = yup::Time::getMillisecondCounterHiRes() / 1000.0; - double timeSpentSeconds = currentTimeSeconds - frameStartTimeSeconds; + YUP_AUTORELEASEPOOL + { + renderFrame(); + } + if (threadShouldExit()) + break; + + // Cap the frame rate with the waitable timer. + const double timeSpentSeconds = (yup::Time::getMillisecondCounterHiRes() / 1000.0) - frameStartTimeSeconds; const double secondsToWait = maxFrameTimeSeconds - timeSpentSeconds; + if (secondsToWait > 0.0) - { - const auto waitUntilMs = (currentTimeSeconds + secondsToWait) * 1000.0; + waitUntil ((yup::Time::getMillisecondCounterHiRes() / 1000.0) + secondsToWait); + } +} - while (yup::Time::getMillisecondCounterHiRes() < waitUntilMs - 4.0) - std::this_thread::sleep_for (std::chrono::microseconds (25)); +//============================================================================== - while (yup::Time::getMillisecondCounterHiRes() < waitUntilMs - 2.0) - std::this_thread::sleep_for (std::chrono::microseconds (10)); +void SDLComponentNative::runWithComputeContext (const std::function& fn) +{ + if (computeContext == nullptr) + { + runWithGraphicsContext (fn); + return; + } - while (yup::Time::getMillisecondCounterHiRes() < waitUntilMs) - std::this_thread::sleep_for (std::chrono::microseconds (1)); - } + const ScopedLock sl (glContextLock); + + if (computeContextLost) + return; + + auto* previousContext = SDL_GL_GetCurrentContext(); + + if (! SDL_GL_MakeCurrent (window, computeContext) || SDL_GL_GetCurrentContext() != computeContext) + { + Logger::outputDebugString ("SDL: unable to make GL compute context current, GPU compute is disabled: " + String (SDL_GetError())); + + computeContextLost = true; + SDL_GL_MakeCurrent (window, previousContext); + return; } + + fn(); + SDL_GL_MakeCurrent (window, previousContext); } -void SDLComponentNative::handleAsyncUpdate() +//============================================================================== + +void SDLComponentNative::runWithGraphicsContext (const std::function& fn) { - if (! isThreadRunning() || ! isInitialised.test_and_set()) - return; + if constexpr (! renderDrivenByTimer) + { + const bool isGL = currentGraphicsApi == GpuPlatform::OpenGL || currentGraphicsApi == GpuPlatform::OpenGLES; + + if (isGL) + { + const ScopedLock sl (glContextLock); + + const bool wasCurrent = isGL && (SDL_GL_GetCurrentContext() == windowContext); + + if (! wasCurrent) + SDL_GL_MakeCurrent (window, windowContext); - getRenderContext(); + fn(); - renderEvent.signal(); + if (! wasCurrent) + SDL_GL_MakeCurrent (window, nullptr); + + return; + } + } + + fn(); } +//============================================================================== + void SDLComponentNative::timerCallback() { #if ! (YUP_MOBILE || YUP_EMSCRIPTEN) @@ -867,25 +971,29 @@ void SDLComponentNative::timerCallback() pollCapturedMouseState(); #endif - getRenderContext(); + if constexpr (renderDrivenByTimer) + renderFrame(); } //============================================================================== -void SDLComponentNative::getRenderContext() +void SDLComponentNative::renderFrame() { - YUP_PROFILE_NAMED_INTERNAL_TRACE (RenderContext); + YUP_PROFILE_NAMED_INTERNAL_TRACE (RenderFrame); if (context == nullptr) return; const auto contentSize = getContentSize(); - auto contentWidth = contentSize.getWidth(); - auto contentHeight = contentSize.getHeight(); + const auto contentWidth = contentSize.getWidth(); + const auto contentHeight = contentSize.getHeight(); if (contentWidth == 0 || contentHeight == 0 || ! isVisible()) return; + const bool isGL = currentGraphicsApi == GpuPlatform::OpenGL || currentGraphicsApi == GpuPlatform::OpenGLES; + + // Resize GL resources on the render thread, which owns the GL context. if (currentContentWidth != contentWidth || currentContentHeight != contentHeight) { YUP_PROFILE_NAMED_INTERNAL_TRACE (ResizeRenderer); @@ -895,15 +1003,33 @@ void SDLComponentNative::getRenderContext() currentContentWidth = contentWidth; currentContentHeight = contentHeight; + if (isGL) + { + if constexpr (! renderDrivenByTimer) + { + glContextLock.enter(); + SDL_GL_MakeCurrent (window, windowContext); + } + } + context->onSizeChanged (getNativeHandle(), contentWidth, contentHeight, getScaleDpi(), 0); renderer = context->makeRenderer (contentWidth, contentHeight); YUP_MODULE_DBG (GUI_WINDOWING, "SDL: renderer " << String (renderer != nullptr ? "created" : "creation failed")); + if (isGL) + { + if constexpr (! renderDrivenByTimer) + { + SDL_GL_MakeCurrent (window, nullptr); + glContextLock.exit(); + } + } + repaint(); } - auto renderContinuous = shouldRenderContinuous.load (std::memory_order_relaxed); - auto currentTimeSeconds = yup::Time::getMillisecondCounterHiRes() / 1000.0; + const auto renderContinuous = shouldRenderContinuous.load (std::memory_order_relaxed); + const auto currentTimeSeconds = yup::Time::getMillisecondCounterHiRes() / 1000.0; const auto measureFramesPerSeconds = ErasedScopeGuard ([&] { @@ -920,96 +1046,130 @@ void SDLComponentNative::getRenderContext() } }); - { - YUP_PROFILE_NAMED_INTERNAL_TRACE (RefreshDisplay); + const auto loadAction = (renderContinuous) + ? rive::gpu::LoadAction::clear + : rive::gpu::LoadAction::preserveRenderTarget; - component.internalRefreshDisplay (currentTimeSeconds - lastRenderTimeSeconds); - lastRenderTimeSeconds = currentTimeSeconds; - } + rive::gpu::RenderContext::FrameDescriptor frameDescriptor; + frameDescriptor.renderTargetWidth = static_cast (currentContentWidth); + frameDescriptor.renderTargetHeight = static_cast (currentContentHeight); + frameDescriptor.loadAction = loadAction; + frameDescriptor.clearColor = clearColor.getARGB(); + frameDescriptor.disableRasterOrdering = renderAtomicMode.load (std::memory_order_relaxed); + frameDescriptor.wireframe = renderWireframe.load (std::memory_order_relaxed); + frameDescriptor.fillsDisabled = false; + frameDescriptor.strokesDisabled = false; + frameDescriptor.clockwiseFillOverride = true; - if (renderContinuous) - repaint(); - else if (currentRepaintAreas.isEmpty()) - return; + RectangleList repaintAreas; + bool glContextLocked = false; - auto renderFrame = [&] + auto renderInternal = [&]() -> bool { - YUP_PROFILE_NAMED_INTERNAL_TRACE (RenderFrame); + { + YUP_PROFILE_NAMED_INTERNAL_TRACE (RefreshDisplay); + + component.internalRefreshDisplay (currentTimeSeconds - lastRenderTimeSeconds); + lastRenderTimeSeconds = currentTimeSeconds; + } + + if (renderContinuous) + repaint(); + + { + const ScopedLock sl (repaintLock); + repaintAreas = std::move (currentRepaintAreas); + currentRepaintAreas.clearQuick(); + } - // Setup frame description - const auto loadAction = (renderContinuous) - ? rive::gpu::LoadAction::clear - : rive::gpu::LoadAction::preserveRenderTarget; + if (! renderContinuous && repaintAreas.isEmpty()) + return false; - rive::gpu::RenderContext::FrameDescriptor frameDescriptor; - frameDescriptor.renderTargetWidth = static_cast (currentContentWidth); - frameDescriptor.renderTargetHeight = static_cast (currentContentHeight); - frameDescriptor.loadAction = loadAction; - frameDescriptor.clearColor = clearColor.getARGB(); - frameDescriptor.disableRasterOrdering = renderAtomicMode; - frameDescriptor.wireframe = renderWireframe; - frameDescriptor.fillsDisabled = false; - frameDescriptor.strokesDisabled = false; - frameDescriptor.clockwiseFillOverride = true; + if (isGL) + { + if constexpr (! renderDrivenByTimer) + { + glContextLock.enter(); + glContextLocked = true; + SDL_GL_MakeCurrent (window, windowContext); + } + } { YUP_PROFILE_NAMED_INTERNAL_TRACE (ContextBegin); - // Begin context drawing context->begin (frameDescriptor); } + // Repaint the component hierarchy (runs user paint() under the lock). + const auto repaintComponents = [&] { - const auto repaintComponents = [&] + if (renderer != nullptr) { - // Repaint components hierarchy - if (renderer != nullptr) - { - const auto dpiScale = getScaleDpi(); + YUP_PROFILE_NAMED_INTERNAL_TRACE (InternalPaint); - for (auto& repaintArea : currentRepaintAreas) - { - YUP_PROFILE_NAMED_INTERNAL_TRACE (InternalPaint); + const auto dpiScale = getScaleDpi(); + const auto repaintArea = repaintAreas.getBoundingBox().smallestIntContainer(); - Graphics g (*context, *renderer, dpiScale); - component.internalPaint (g, repaintArea, renderContinuous); - } - } - }; + Graphics g (*context, *renderer, dpiScale); + component.internalPaint (g, repaintArea, renderContinuous); + } + }; - if (PaintProfiler::hasRegisteredComponents() && PaintProfiler::getInstance().isEnabled()) + if (PaintProfiler::hasRegisteredComponents() && PaintProfiler::getInstance().isEnabled()) + { + PaintProfiler::getInstance().beginFrame(); + const auto endFrameGuard = ErasedScopeGuard ([&] { - PaintProfiler::getInstance().beginFrame(); - const auto endFrameGuard = ErasedScopeGuard ([&] - { - PaintProfiler::getInstance().endFrame(); - }); + PaintProfiler::getInstance().endFrame(); + }); - repaintComponents(); - } - else - { - repaintComponents(); - } + repaintComponents(); } - - // Finish context drawing + else { - YUP_PROFILE_NAMED_INTERNAL_TRACE (ContextEnd); - - context->end (getNativeHandle()); - context->tick(); + repaintComponents(); } + + return true; }; - renderFrame(); + if constexpr (! renderDrivenByTimer) + { + const MessageManagerLock mmLock (Thread::getCurrentThread()); + if (! mmLock.lockWasGained()) + return; + + if (! renderInternal()) + return; + } + else + { + if (! renderInternal()) + return; + } + + { + YUP_PROFILE_NAMED_INTERNAL_TRACE (ContextEnd); - // Swap buffers - if (window != nullptr && (currentGraphicsApi == GpuPlatform::OpenGL || currentGraphicsApi == GpuPlatform::OpenGLES)) + context->end (getNativeHandle()); + context->tick(); + } + + if (isGL && window != nullptr) + { SDL_GL_SwapWindow (window); - // Clear repainted areas - currentRepaintAreas.clearQuick(); + if constexpr (! renderDrivenByTimer) + { + if (glContextLocked) + { + SDL_GL_MakeCurrent (window, nullptr); + + glContextLock.exit(); + } + } + } } //============================================================================== @@ -1029,6 +1189,9 @@ void SDLComponentNative::startRendering() } else { + if (! isTimerRunning()) + startTimerHz (desiredFrameRate); + if (! isThreadRunning()) startThread (Priority::high); } @@ -1042,15 +1205,13 @@ void SDLComponentNative::stopRendering() { YUP_MODULE_DBG (GUI_WINDOWING, "SDL: stopRendering requested: rendering=" << String (isRendering() ? "true" : "false")); - if constexpr (renderDrivenByTimer) + if (isTimerRunning()) { - if (isTimerRunning()) - { - stopTimer(); - YUP_MODULE_DBG (GUI_WINDOWING, "SDL: stopped render timer"); - } + stopTimer(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL: stopped render/input timer"); } - else + + if constexpr (! renderDrivenByTimer) { if (isThreadRunning()) { @@ -1515,8 +1676,6 @@ void SDLComponentNative::handleFocusChanged (bool gotFocus) component.internalFocusChanged (true); - // Re-notify the focused widget so it restarts its caret and text input. - // This pairs with the focusLost() call in the gotFocus=false branch below. if (lastComponentFocused != nullptr && lastComponentFocused.get() != std::addressof (component)) { auto focusBailOut = Component::BailOutChecker (lastComponentFocused.get()); @@ -1529,8 +1688,6 @@ void SDLComponentNative::handleFocusChanged (bool gotFocus) } else { - // Properly notify the focused widget so it stops its caret and text input - // via relinquishTextInput(), keeping textInputActive in sync with SDL's state. if (lastComponentFocused != nullptr && lastComponentFocused.get() != std::addressof (component)) { auto focusBailOut = Component::BailOutChecker (lastComponentFocused.get()); diff --git a/modules/yup_gui/native/yup_Windowing_sdl.h b/modules/yup_gui/native/yup_Windowing_sdl.h index 6a6568d3f..b3b01842f 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl.h +++ b/modules/yup_gui/native/yup_Windowing_sdl.h @@ -27,12 +27,11 @@ class SDLComponentNative final : public ComponentNative , public Timer , public Thread - , public AsyncUpdater { -#if (YUP_EMSCRIPTEN && (RIVE_WEBGL || RIVE_WEBGPU)) && ! defined(__EMSCRIPTEN_PTHREADS__) - static constexpr bool renderDrivenByTimer = false; -#else +#if YUP_EMSCRIPTEN static constexpr bool renderDrivenByTimer = true; +#else + static constexpr bool renderDrivenByTimer = false; #endif public: @@ -109,6 +108,9 @@ class SDLComponentNative final GraphicsContext* getGraphicsContext() override; + //============================================================================== + void runWithGraphicsContext (const std::function& fn) override; + //============================================================================== void* getNativeHandle() const override; @@ -119,7 +121,6 @@ class SDLComponentNative final //============================================================================== void run() override; - void handleAsyncUpdate() override; void timerCallback() override; //============================================================================== @@ -175,7 +176,8 @@ class SDLComponentNative final Component* findComponentForMouseEvent (const Point& position); void updateComponentUnderMouse (const MouseEvent& event); - void getRenderContext(); + void runWithComputeContext (const std::function& fn); + void renderFrame(); void startRendering(); void stopRendering(); @@ -184,6 +186,8 @@ class SDLComponentNative final SDL_Window* window = nullptr; SDL_GLContext windowContext = nullptr; + SDL_GLContext computeContext = nullptr; + bool computeContextLost = false; void* parentWindow = nullptr; String windowTitle; @@ -216,6 +220,11 @@ class SDLComponentNative final RelativeTime doubleClickTime; RectangleList currentRepaintAreas; + CriticalSection repaintLock; + + /** Serializes access to the shared GL context between the render thread and + any other thread running offscreen GPU work (see runWithGraphicsContext). */ + CriticalSection glContextLock; float desiredFrameRate = 60.0f; std::atomic currentFrameRate = 0.0f; @@ -231,8 +240,8 @@ class SDLComponentNative final WaitableEvent renderEvent { true }; std::atomic shouldRenderContinuous = false; double lastRenderTimeSeconds = 0.0; - bool renderAtomicMode = false; - bool renderWireframe = false; + std::atomic renderAtomicMode = false; + std::atomic renderWireframe = false; bool updateOnlyWhenFocused = false; bool shouldCaptureMouse = false; bool mouseCaptureActive = false; diff --git a/modules/yup_rhi/context/yup_GpuDevice.cpp b/modules/yup_rhi/context/yup_GpuDevice.cpp index cbf39ef5b..7caa4ee5e 100644 --- a/modules/yup_rhi/context/yup_GpuDevice.cpp +++ b/modules/yup_rhi/context/yup_GpuDevice.cpp @@ -104,7 +104,6 @@ ReferenceCountedObjectPtr GpuDevice::createBuffer (GpuBufferType type if (data == nullptr || byteSize == 0) return nullptr; - // Storage buffers must be handled by backend overrides. if (type == GpuBufferType::storage) return nullptr; @@ -118,9 +117,11 @@ ReferenceCountedObjectPtr GpuDevice::createBuffer (GpuBufferType 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; @@ -128,7 +129,7 @@ ReferenceCountedObjectPtr GpuDevice::createBuffer (GpuBufferType type desc.size = (uint32_t) byteSize; desc.data = data; - desc.immutable = true; + desc.immutable = false; desc.label = "GpuBuffer"; auto buffer = oreCtx->makeBuffer (desc); @@ -152,7 +153,6 @@ bool GpuDevice::updateBuffer (GpuBuffer::Ptr buffer, const void* data, size_t by if (impl == nullptr) return false; - // For ore-backed buffers (vertex, index, uniform), update in place. if (impl->oreBuffer != nullptr) { if (byteSize > buffer->getSizeInBytes()) @@ -214,7 +214,7 @@ rive::rcp GpuDevice::UniformBufferPool::acquire (rive::ore::C 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.immutable = false; desc.label = "GpuRenderPass uniform"; return oreCtx.makeBuffer (desc); @@ -228,7 +228,6 @@ void GpuDevice::UniformBufferPool::release (rive::rcp buffer) // 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)); } diff --git a/modules/yup_rhi/context/yup_GpuDevice.h b/modules/yup_rhi/context/yup_GpuDevice.h index 94c5de567..d612b120f 100644 --- a/modules/yup_rhi/context/yup_GpuDevice.h +++ b/modules/yup_rhi/context/yup_GpuDevice.h @@ -53,7 +53,7 @@ class YUP_API GpuDevice : public ReferenceCountedObject struct Options { /** Default constructor, initializes the options with default values. */ - constexpr Options() noexcept = default; + Options() noexcept = default; bool retinaDisplay = true; ///< Whether the context supports Retina or high-DPI displays. bool readableFramebuffer = false; ///< Allows the framebuffer to be readable. @@ -61,6 +61,36 @@ class YUP_API GpuDevice : public ReferenceCountedObject 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). + + /** Optional callback that runs GPU work with the native rendering context made + current on the calling thread. + + GL contexts are thread-affine and the windowing layer may bind the context + to a dedicated render thread. Offscreen GPU work initiated from other threads + (e.g. snapshots taken from the message thread) must go through this callback + so the context is bound — and its access serialized with the render thread — + for the duration of the work. Backends without a thread-affine context + (D3D, Metal, WebGPU) ignore it. When null, GPU work runs as-is. + + @param fn The GPU work to run with the context current. + */ + std::function&)> contextActivator; + + /** Optional callback that runs GPU compute work with a dedicated compute + context made current on the calling thread. + + Some GL drivers stop executing compute dispatches when they are + interleaved with rendering on the same context — or on any context of + the same share group. Providing a dedicated, unshared context isolates + the compute command stream from the render pass; every compute resource + (program, storage buffers) then lives exclusively on that context. + When null, compute work falls back to contextActivator (or runs as-is + when that is also null). Backends without a thread-affine context + (D3D, Metal, WebGPU) ignore it. + + @param fn The GPU compute work to run with the compute context current. + */ + std::function&)> computeContextActivator; }; //============================================================================== @@ -97,6 +127,13 @@ class YUP_API GpuDevice : public ReferenceCountedObject virtual GpuPlatform getPlatform() const noexcept = 0; //============================================================================== + /** Returns true if a GPU (ore) context is available for RHI operations. + + 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 getGpuContext() != nullptr; } + /** 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 @@ -118,13 +155,7 @@ class YUP_API GpuDevice : public ReferenceCountedObject */ virtual rive::ore::Context* getGpuContext() const noexcept { return nullptr; } - /** Returns true if a GPU (ore) context is available for RHI operations. - - 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 getGpuContext() != nullptr; } - + //============================================================================== /** Returns true if compute shaders are available on this backend. Compute shaders are available on Metal, D3D11, D3D12, Vulkan, and @@ -132,6 +163,22 @@ class YUP_API GpuDevice : public ReferenceCountedObject */ virtual bool isComputeAvailable() const noexcept { return false; } + /** Runs GPU compute work with the backend's compute context current on the + calling thread. + + On OpenGL this routes @p fn through Options::computeContextActivator so + the work is encoded on a dedicated compute context, isolated from the + rendering command stream (falling back to Options::contextActivator when + no compute activator was provided). On every other backend @p fn runs + directly. + + Used internally by GpuComputePass and GpuComputePipeline; user code + normally never needs to call this. + + @param fn The GPU compute work to run with the compute context current. + */ + virtual void runOnComputeContext (const std::function& fn) const { fn(); } + //============================================================================== /** Creates platform-specific GPU offscreen resources for the given dimensions. diff --git a/modules/yup_rhi/native/yup_GpuComputePass_opengl.cpp b/modules/yup_rhi/native/yup_GpuComputePass_opengl.cpp index 7bc6521b9..24d774f9e 100644 --- a/modules/yup_rhi/native/yup_GpuComputePass_opengl.cpp +++ b/modules/yup_rhi/native/yup_GpuComputePass_opengl.cpp @@ -29,6 +29,11 @@ namespace yup class GpuComputePassImplGL final : public GpuComputePass::Impl { public: + explicit GpuComputePassImplGL (GpuDevice& deviceToUse) + : device (deviceToUse) + { + } + bool isValid() const override { return true; } //========================================================================== @@ -42,44 +47,56 @@ class GpuComputePassImplGL final : public GpuComputePass::Impl if (pipe == nullptr || pipe->getProgram() == 0) return false; - glUseProgram (pipe->getProgram()); - - for (auto& sb : storageBindings) + device.runOnComputeContext ([&] { - if (sb.buffer == nullptr) - continue; + GLint previousProgram = 0; + GLint previousUniformBuffer = 0; + glGetIntegerv (GL_CURRENT_PROGRAM, &previousProgram); + glGetIntegerv (GL_UNIFORM_BUFFER_BINDING, &previousUniformBuffer); - auto* bufImpl = sb.buffer->getImpl(); - if (bufImpl == nullptr || bufImpl->glBuffer == 0) - continue; + glUseProgram (pipe->getProgram()); - GLuint index = static_cast (sb.group * 16 + sb.binding); - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, bufImpl->glBuffer); - } + for (auto& sb : storageBindings) + { + if (sb.buffer == nullptr) + continue; - for (auto& ub : uboBindings) - { - if (ub.data.empty()) - continue; + auto* bufImpl = sb.buffer->getImpl(); + if (bufImpl == nullptr || bufImpl->glStorageBuffer.id == 0) + continue; - GLuint ubo = 0; - glGenBuffers (1, &ubo); - if (ubo == 0) - continue; + GLuint index = static_cast (sb.group * 16 + sb.binding); + glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, bufImpl->glStorageBuffer.id); + } - glBindBuffer (GL_UNIFORM_BUFFER, ubo); - glBufferData (GL_UNIFORM_BUFFER, - static_cast (ub.data.size()), - ub.data.data(), - GL_DYNAMIC_DRAW); + for (auto& ub : uboBindings) + { + if (ub.data.empty()) + continue; - GLuint index = static_cast (ub.group * 16 + ub.binding); - glBindBufferBase (GL_UNIFORM_BUFFER, index, ubo); + GLuint ubo = 0; + glGenBuffers (1, &ubo); + if (ubo == 0) + continue; - tempBuffers.push_back (ubo); - } + 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); + + glUseProgram (static_cast (previousProgram)); + glBindBuffer (GL_UNIFORM_BUFFER, static_cast (previousUniformBuffer)); + }); - glDispatchCompute (groupsX, groupsY, groupsZ); return true; } @@ -87,26 +104,30 @@ class GpuComputePassImplGL final : public GpuComputePass::Impl void finish() override { - if (! tempBuffers.empty()) + device.runOnComputeContext ([&] { - glDeleteBuffers (static_cast (tempBuffers.size()), tempBuffers.data()); - tempBuffers.clear(); - } - - glMemoryBarrier (GL_SHADER_STORAGE_BARRIER_BIT - | GL_UNIFORM_BARRIER_BIT - | GL_BUFFER_UPDATE_BARRIER_BIT); + 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: + GpuDevice& device; std::vector tempBuffers; }; //============================================================================== -std::unique_ptr yup_createComputePassImplGL (GpuDevice&) +std::unique_ptr yup_createComputePassImplGL (GpuDevice& device) { - return std::make_unique(); + return std::make_unique (device); } } // namespace yup diff --git a/modules/yup_rhi/native/yup_GpuComputePipeline_d3d.cpp b/modules/yup_rhi/native/yup_GpuComputePipeline_d3d.cpp index ce3ce8143..07f93aa0b 100644 --- a/modules/yup_rhi/native/yup_GpuComputePipeline_d3d.cpp +++ b/modules/yup_rhi/native/yup_GpuComputePipeline_d3d.cpp @@ -58,10 +58,34 @@ ResultValue yup_constructComputePipelineD3D11 (GpuDevic auto& d3dCtx = static_cast (ctx); + std::string hlsl (static_cast (source.code), source.codeSize); + hlsl.erase (std::remove (hlsl.begin(), hlsl.end(), '\r'), hlsl.end()); + + const char* entryPoint = source.entryPoint != nullptr ? source.entryPoint : "main"; + + ComPtr compiledBlob; + ComPtr errorBlob; + HRESULT hr = D3DCompile (hlsl.data(), hlsl.size(), nullptr, nullptr, nullptr, + entryPoint, "cs_5_0", + D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, + 0, compiledBlob.ReleaseAndGetAddressOf(), errorBlob.ReleaseAndGetAddressOf()); + if (FAILED (hr) || compiledBlob == nullptr) + { + String errorMessage = "D3D11 compute shader compilation failed"; + + if (errorBlob != nullptr && errorBlob->GetBufferSize() > 0) + errorMessage << ": " << static_cast (errorBlob->GetBufferPointer()); + + return makeResultValueFail (errorMessage); + } + ComPtr computeShader; - HRESULT hr = d3dCtx.getD3DDevice()->CreateComputeShader (source.code, source.codeSize, nullptr, computeShader.ReleaseAndGetAddressOf()); + hr = d3dCtx.getD3DDevice()->CreateComputeShader (compiledBlob->GetBufferPointer(), + compiledBlob->GetBufferSize(), + nullptr, + computeShader.ReleaseAndGetAddressOf()); if (FAILED (hr) || computeShader == nullptr) - return makeResultValueFail ("D3D11 compute shader compilation failed"); + return makeResultValueFail ("D3D11 compute shader creation failed"); return makeResultValueOk (GpuComputePipeline::Ptr (new GpuComputePipelineD3D11 (std::move (computeShader), workgroupSize))); } diff --git a/modules/yup_rhi/native/yup_GpuComputePipeline_opengl.cpp b/modules/yup_rhi/native/yup_GpuComputePipeline_opengl.cpp index 9f826ba81..20c1bd313 100644 --- a/modules/yup_rhi/native/yup_GpuComputePipeline_opengl.cpp +++ b/modules/yup_rhi/native/yup_GpuComputePipeline_opengl.cpp @@ -29,16 +29,29 @@ namespace yup class GpuComputePipelineGL final : public GpuComputePipeline { public: - GpuComputePipelineGL (GLuint program, GpuWorkgroupSize wgs) - : glProgram (program) + GpuComputePipelineGL (GpuDevice::Ptr deviceToUse, GLuint program, GpuWorkgroupSize wgs) + : device (std::move (deviceToUse)) + , glProgram (program) , workgroupSize (wgs) { } ~GpuComputePipelineGL() override { - if (glProgram != 0) + if (glProgram == 0) + return; + + if (device != nullptr) + { + device->runOnComputeContext ([program = glProgram] + { + glDeleteProgram (program); + }); + } + else + { glDeleteProgram (glProgram); + } } GpuWorkgroupSize getWorkgroupSize() const noexcept override { return workgroupSize; } @@ -46,13 +59,15 @@ class GpuComputePipelineGL final : public GpuComputePipeline GLuint getProgram() const noexcept { return glProgram; } private: + GpuDevice::Ptr device; GLuint glProgram; GpuWorkgroupSize workgroupSize; }; //============================================================================== -ResultValue yup_constructComputePipelineGL (const GpuShaderSource& source, +ResultValue yup_constructComputePipelineGL (GpuDevice::Ptr device, + const GpuShaderSource& source, const GpuWorkgroupSize& workgroupSize) { if (source.code == nullptr || source.codeSize == 0) @@ -114,7 +129,7 @@ ResultValue yup_constructComputePipelineGL (const GpuSh glDeleteShader (shader); - return makeResultValueOk (GpuComputePipeline::Ptr (new GpuComputePipelineGL (program, workgroupSize))); + return makeResultValueOk (GpuComputePipeline::Ptr (new GpuComputePipelineGL (std::move (device), program, workgroupSize))); } } // namespace yup diff --git a/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp b/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp index 51de34230..ef7dedffc 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_d3d.cpp @@ -57,10 +57,8 @@ class GpuDeviceD3D : public GpuDevice 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(); } //============================================================================== @@ -99,7 +97,7 @@ class GpuDeviceD3D : public GpuDevice if (FAILED (hr) || uav == nullptr) return nullptr; - return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, std::move (d3dBuffer), std::move (uav) }); + return GpuBuffer::createWithImpl (GpuBuffer::Impl { .type = type, .byteSize = byteSize, .d3dStorageBuffer = std::move (d3dBuffer), .d3dUav = std::move (uav) }); } return GpuDevice::createBuffer (type, data, byteSize); @@ -120,8 +118,6 @@ class GpuDeviceD3D : public GpuDevice 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 {}; @@ -137,10 +133,6 @@ class GpuDeviceD3D : public GpuDevice 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 {}; @@ -164,7 +156,6 @@ class GpuDeviceD3D : public GpuDevice 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); @@ -332,8 +323,6 @@ class GpuDeviceD3D : public GpuDevice flushDesc.renderTarget = target.getRenderTarget(); renderContext->flush (flushDesc); - if (auto* renderTarget = static_cast (target.getRenderTarget())) - gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); target.contextSlot->frameActive = false; } @@ -367,11 +356,9 @@ class GpuDeviceD3D : public GpuDevice if (dstSize < bytesPerRow * static_cast (target.height)) return false; - if (target.getRenderContext() == nullptr) - { - if (auto* renderTarget = static_cast (target.getRenderTarget())) + if (auto* renderTarget = static_cast (target.getRenderTarget())) + if (renderTarget->targetTexture() != nullptr) gpuContext->CopyResource (target.stagingTexture.Get(), renderTarget->targetTexture()); - } D3D11_MAPPED_SUBRESOURCE mapped {}; HRESULT hr = gpuContext->Map (target.stagingTexture.Get(), 0, D3D11_MAP_READ, 0, &mapped); @@ -457,7 +444,6 @@ std::unique_ptr yup_constructDirect3DGpuDevice (GpuDevice::Options fi contextOptions.disableTypedUAVLoadStore = true; } - // Create a temporary factory just to enumerate adapters ComPtr factory; VERIFY_OK (CreateDXGIFactory (__uuidof (IDXGIFactory2), reinterpret_cast (factory.ReleaseAndGetAddressOf()))); diff --git a/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp b/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp index 6bf196631..3545e2f5b 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_dawn.cpp @@ -158,7 +158,7 @@ class GpuDeviceDawn : public GpuDevice queue.WriteBuffer (wgpuBuffer, 0, data, byteSize); - return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, std::move (wgpuBuffer) }); + return GpuBuffer::createWithImpl (GpuBuffer::Impl { .type = type, .byteSize = byteSize, .webgpuStorageBuffer = std::move (wgpuBuffer) }); } return GpuDevice::createBuffer (type, data, byteSize); diff --git a/modules/yup_rhi/native/yup_GpuDevice_metal.cpp b/modules/yup_rhi/native/yup_GpuDevice_metal.cpp index ef6c56ada..a0d353e11 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_metal.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_metal.cpp @@ -78,7 +78,7 @@ class GpuDeviceMetal : public GpuDevice if (mtlBuffer == nil) return nullptr; - return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, mtlBuffer }); + return GpuBuffer::createWithImpl (GpuBuffer::Impl { .type = type, .byteSize = byteSize, .mtlStorageBuffer = mtlBuffer }); } return GpuDevice::createBuffer (type, data, byteSize); diff --git a/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp b/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp index a5a79468e..a44cada82 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_opengl.cpp @@ -25,8 +25,16 @@ #include "rive/renderer/gl/render_target_gl.hpp" #include "rive/renderer/ore/ore_context_gl.hpp" +#if YUP_EMSCRIPTEN +#include +#endif + +#include #include #include +#include +#include +#include namespace yup { @@ -42,8 +50,8 @@ static void GLAPIENTRY err_msg_callback (GLenum source, { if (type == GL_DEBUG_TYPE_ERROR_KHR) { - printf ("GL ERROR: %s\n", message); - fflush (stdout); + fprintf (stderr, "GL ERROR: %s\n", message); + fflush (stderr); assert (false); } else if (type == GL_DEBUG_TYPE_PERFORMANCE_KHR) @@ -52,10 +60,12 @@ static void GLAPIENTRY err_msg_callback (GLenum source, "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); + + fprintf (stderr, "GL PERF: %s\n", message); + fflush (stderr); } } #endif @@ -76,6 +86,14 @@ class GpuDeviceGL : public GpuDevice } #endif +#if YUP_EMSCRIPTEN + if (emscripten_webgl_get_current_context() == 0) + { + fprintf (stderr, "No current WebGL context, GL device unavailable.\n"); + return; + } +#endif + renderContext = rive::gpu::RenderContextGLImpl::MakeContext (renderContextOptions); if (! renderContext) { @@ -133,26 +151,32 @@ class GpuDeviceGL : public GpuDevice bool isComputeAvailable() const noexcept override { - // GL 4.3+ and GLES 3.1+ support compute shaders natively. - // Probe the version string at runtime. - const auto* version = (const char*) glGetString (GL_VERSION); - if (version == nullptr) + if (renderContext == nullptr) return false; - // GLES: "OpenGL ES 3.1" or higher - if (strstr (version, "OpenGL ES") != nullptr) + return withGLContext ([&]() -> bool { - int major = 0, minor = 0; - if (sscanf (version, "OpenGL ES %d.%d", &major, &minor) == 2) - return major > 3 || (major == 3 && minor >= 1); - } + // GL 4.3+ and GLES 3.1+ support compute shaders natively. + // Probe the version string at runtime. + const auto* version = (const char*) glGetString (GL_VERSION); + if (version == nullptr) + return false; + + // GLES: "OpenGL ES 3.1" or higher + if (strstr (version, "OpenGL ES") != nullptr) + { + int major = 0, minor = 0; + if (sscanf (version, "OpenGL ES %d.%d", &major, &minor) == 2) + return major > 3 || (major == 3 && minor >= 1); + } - // Desktop GL: "4.3" or higher - int major = 0, minor = 0; - if (sscanf (version, "%d.%d", &major, &minor) == 2) - return major > 4 || (major == 4 && minor >= 3); + // Desktop GL: "4.3" or higher + int major = 0, minor = 0; + if (sscanf (version, "%d.%d", &major, &minor) == 2) + return major > 4 || (major == 4 && minor >= 3); - return false; + return false; + }); } //============================================================================== @@ -161,23 +185,29 @@ class GpuDeviceGL : public GpuDevice { if (type == GpuBufferType::storage) { - jassert (data != nullptr && byteSize > 0); - if (data == nullptr || byteSize == 0) - return nullptr; + return withComputeContext ([&]() -> ReferenceCountedObjectPtr + { + jassert (data != nullptr && byteSize > 0); + if (data == nullptr || byteSize == 0) + return nullptr; - GLuint buf = 0; - glGenBuffers (1, &buf); - if (buf == 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); + 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 GpuBuffer::createWithImpl (GpuBuffer::Impl { .type = type, .byteSize = byteSize, .glStorageBuffer = { buf, this } }); + }); } - return GpuDevice::createBuffer (type, data, byteSize); + return withGLContext ([&]() -> ReferenceCountedObjectPtr + { + return GpuDevice::createBuffer (type, data, byteSize); + }); } //============================================================================== @@ -186,29 +216,35 @@ class GpuDeviceGL : public GpuDevice { #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); + return withGLContext ([&]() -> bool + { + return GpuDevice::readBuffer (std::move (buffer), dst, dstSize); + }); #else - if (buffer == nullptr || dst == nullptr) - return false; + return withComputeContext ([&]() -> bool + { + if (buffer == nullptr || dst == nullptr) + return false; - auto* impl = buffer->getImpl(); - if (impl == nullptr || impl->glBuffer == 0) - return false; + auto* impl = buffer->getImpl(); + if (impl == nullptr || impl->glStorageBuffer.id == 0) + return false; - const auto byteSize = buffer->getSizeInBytes(); - if (dstSize < byteSize) - 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; + glFinish(); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, impl->glStorageBuffer.id); + 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 } @@ -223,20 +259,26 @@ class GpuDeviceGL : public GpuDevice if (impl == nullptr) return false; - // For native GL storage buffers, use glBufferSubData. - if (impl->glBuffer != 0) + // For native GL storage buffers, use glBufferSubData on the compute context. + if (impl->glStorageBuffer.id != 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; + return withComputeContext ([&]() -> bool + { + if (byteSize > buffer->getSizeInBytes()) + return false; + + glBindBuffer (GL_SHADER_STORAGE_BUFFER, impl->glStorageBuffer.id); + 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); + return withGLContext ([&]() -> bool + { + return GpuDevice::updateBuffer (buffer, data, byteSize); + }); } //============================================================================== @@ -287,6 +329,7 @@ class GpuDeviceGL : public GpuDevice { if (renderCanvas == nullptr) return nullptr; + return renderCanvas->renderImage()->refTexture(); } @@ -295,11 +338,14 @@ class GpuDeviceGL : public GpuDevice #if defined(ORE_BACKEND_GL) && defined(RIVE_CANVAS) if (sampledMirrorTex != nullptr) return sampledMirrorTex; + if (mirrorContext == nullptr || renderCanvas == nullptr) return nullptr; + auto renderImage = renderCanvas->renderImage(); if (renderImage == nullptr) return nullptr; + if (auto sourceTex = renderImage->refTexture()) { auto mirrorImage = rive::getCanvasImportMirrorGL ( @@ -307,6 +353,7 @@ class GpuDeviceGL : public GpuDevice if (mirrorImage != nullptr) sampledMirrorTex = mirrorImage->refTexture(); } + return sampledMirrorTex; #else return nullptr; @@ -321,134 +368,216 @@ class GpuDeviceGL : public GpuDevice std::unique_ptr createOffscreenTarget (int width, int height) override { - if (width <= 0 || height <= 0 || renderContext == nullptr) - return nullptr; + return withGLContext ([&]() -> std::unique_ptr + { + if (width <= 0 || height <= 0 || renderContext == nullptr) + return nullptr; - auto renderCanvas = renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); - if (renderCanvas == nullptr) - return nullptr; + auto renderCanvas = renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); + if (renderCanvas == nullptr) + return nullptr; - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = nullptr; - target->mirrorContext = renderContext.get(); - target->contextSlot = nullptr; - target->renderCanvas = std::move (renderCanvas); - return target; + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = nullptr; + target->mirrorContext = renderContext.get(); + target->contextSlot = nullptr; + target->renderCanvas = std::move (renderCanvas); + return target; + }); } std::unique_ptr createRenderableTarget (int width, int height) override { - if (width <= 0 || height <= 0) - return nullptr; + return withGLContext ([&]() -> std::unique_ptr + { + if (width <= 0 || height <= 0) + return nullptr; - auto* contextSlot = acquireOffscreenContext(); - if (contextSlot == nullptr) - return nullptr; + auto* contextSlot = acquireOffscreenContext(); + if (contextSlot == nullptr) + return nullptr; - auto target = std::make_unique(); - target->width = width; - target->height = height; - target->renderContext = contextSlot->renderContext.get(); - target->mirrorContext = contextSlot->renderContext.get(); - target->contextSlot = contextSlot; + auto target = std::make_unique(); + target->width = width; + target->height = height; + target->renderContext = contextSlot->renderContext.get(); + target->mirrorContext = contextSlot->renderContext.get(); + target->contextSlot = contextSlot; - target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); - if (target->renderCanvas == nullptr) - return nullptr; + target->renderCanvas = target->renderContext->makeRenderCanvas (static_cast (width), static_cast (height)); + if (target->renderCanvas == nullptr) + return nullptr; - return target; + return target; + }); } void beginOffscreen (OffscreenTarget& baseTarget, const rive::gpu::RenderContext::FrameDescriptor& frameDesc) override { - auto& target = static_cast (baseTarget); - auto renderContext = target.getRenderContext(); - if (renderContext == nullptr || target.contextSlot == nullptr || target.contextSlot->frameActive) - return; + withGLContext ([&] + { + auto& target = static_cast (baseTarget); - renderContext->static_impl_cast()->invalidateGLState(); - renderContext->beginFrame (frameDesc); - target.contextSlot->frameActive = true; + auto renderContext = target.getRenderContext(); + if (renderContext == nullptr || target.contextSlot == nullptr || target.contextSlot->frameActive) + return; + + renderContext->static_impl_cast()->invalidateGLState(); + renderContext->beginFrame (frameDesc); + target.contextSlot->frameActive = true; + }); } void endOffscreen (OffscreenTarget& baseTarget) override { - auto& target = static_cast (baseTarget); - auto renderContext = target.getRenderContext(); - if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) - return; + withGLContext ([&] + { + auto& target = static_cast (baseTarget); - renderContext->static_impl_cast()->invalidateGLState(); - renderContext->flush ({ target.getRenderTarget() }); - renderContext->static_impl_cast()->unbindGLInternalResources(); - target.contextSlot->frameActive = false; + auto renderContext = target.getRenderContext(); + if (renderContext == nullptr || target.contextSlot == nullptr || ! target.contextSlot->frameActive) + return; + + renderContext->static_impl_cast()->invalidateGLState(); + renderContext->flush ({ target.getRenderTarget() }); + renderContext->static_impl_cast()->unbindGLInternalResources(); + target.contextSlot->frameActive = false; + }); } bool clearOffscreen (OffscreenTarget& baseTarget, GpuColor color) override { - auto& target = static_cast (baseTarget); + return withGLContext ([&]() -> bool + { + auto& target = static_cast (baseTarget); - auto* renderTarget = static_cast (target.getRenderTarget()); - if (renderTarget == nullptr) - return false; + 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); + // 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); + 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); + 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); + if (scissorWasEnabled) + glEnable (GL_SCISSOR_TEST); - glBindFramebuffer (GL_DRAW_FRAMEBUFFER, static_cast (previousFramebuffer)); + glBindFramebuffer (GL_DRAW_FRAMEBUFFER, static_cast (previousFramebuffer)); - return true; + return true; + }); } bool readOffscreenPixels (OffscreenTarget& baseTarget, void* dst, size_t dstSize) override { - auto& target = static_cast (baseTarget); - if (target.getRenderTarget() == nullptr || dst == nullptr) - return false; + return withGLContext ([&]() -> bool + { + auto& target = static_cast (baseTarget); + if (target.getRenderTarget() == nullptr || dst == nullptr) + return false; - const size_t bytesPerRow = static_cast (target.width) * 4u; - if (dstSize < bytesPerRow * static_cast (target.height)) - return false; + const size_t bytesPerRow = static_cast (target.width) * 4u; + if (dstSize < bytesPerRow * static_cast (target.height)) + return false; - auto* renderTarget = static_cast (target.getRenderTarget()); - renderTarget->bindDestinationFramebuffer (GL_READ_FRAMEBUFFER); - glReadPixels (0, 0, target.width, target.height, GL_RGBA, GL_UNSIGNED_BYTE, dst); - glBindFramebuffer (GL_READ_FRAMEBUFFER, 0); + auto* renderTarget = static_cast (target.getRenderTarget()); + renderTarget->bindDestinationFramebuffer (GL_READ_FRAMEBUFFER); + glReadPixels (0, 0, target.width, target.height, GL_RGBA, GL_UNSIGNED_BYTE, dst); + glBindFramebuffer (GL_READ_FRAMEBUFFER, 0); + + // Flip vertically: OpenGL framebuffer origin is bottom-left. + offscreenPixelsRow.resize (bytesPerRow); + auto* bytes = static_cast (dst); + 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 (offscreenPixelsRow.data(), top, bytesPerRow); + std::memcpy (top, bottom, bytesPerRow); + std::memcpy (bottom, offscreenPixelsRow.data(), bytesPerRow); + } - // Flip vertically: OpenGL framebuffer origin is bottom-left. - offscreenPixelsRow.resize (bytesPerRow); - auto* bytes = static_cast (dst); - const int halfHeight = target.height / 2; - for (int i = 0; i < halfHeight; ++i) + return true; + }); + } + + //============================================================================== + + void runOnComputeContext (const std::function& fn) const override + { + withComputeContext ([&] + { + fn(); + }); + } + +private: + /** Runs @a fn with the given context activator current on this thread. With a + null activator, @a fn runs directly — the caller is responsible for having + a current context, as before. */ + template + static std::invoke_result_t withActivator (const std::function&)>& activator, Fn&& fn) + { + using Result = std::invoke_result_t; + + if (! activator) + return std::invoke (std::forward (fn)); + + if constexpr (std::is_void_v) { - uint8_t* top = bytes + static_cast (i) * bytesPerRow; - uint8_t* bottom = bytes + static_cast (target.height - 1 - i) * bytesPerRow; - std::memcpy (offscreenPixelsRow.data(), top, bytesPerRow); - std::memcpy (top, bottom, bytesPerRow); - std::memcpy (bottom, offscreenPixelsRow.data(), bytesPerRow); + activator ([&] + { + std::invoke (std::forward (fn)); + }); } + else + { + std::optional result; + activator ([&] + { + result.emplace (std::invoke (std::forward (fn))); + }); + if (! result.has_value()) + return Result {}; - return true; + return std::move (*result); + } + } + + /** Runs @a fn with the GL rendering context current on this thread + (see GpuDevice::Options::contextActivator). */ + template + std::invoke_result_t withGLContext (Fn&& fn) const + { + return withActivator (options.contextActivator, std::forward (fn)); + } + + /** Runs @a fn with the dedicated GL compute context current on this thread + (see GpuDevice::Options::computeContextActivator), falling back to the + rendering context when no compute activator was provided. */ + template + std::invoke_result_t withComputeContext (Fn&& fn) const + { + if (options.computeContextActivator) + return withActivator (options.computeContextActivator, std::forward (fn)); + + return withActivator (options.contextActivator, std::forward (fn)); } -private: OffscreenContextSlot* acquireOffscreenContext() { for (const auto& slot : offscreenContextPool) diff --git a/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp b/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp index 2805ef616..6479dd1a8 100644 --- a/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp +++ b/modules/yup_rhi/native/yup_GpuDevice_webgpu.cpp @@ -98,7 +98,7 @@ class GpuDeviceWebGPU : public GpuDevice queue.WriteBuffer (wgpuBuffer, 0, data, byteSize); - return GpuBuffer::createWithImpl (GpuBuffer::Impl { type, byteSize, {}, std::move (wgpuBuffer) }); + return GpuBuffer::createWithImpl (GpuBuffer::Impl { .type = type, .byteSize = byteSize, .webgpuStorageBuffer = std::move (wgpuBuffer) }); } return GpuDevice::createBuffer (type, data, byteSize); diff --git a/modules/yup_rhi/rhi/yup_GpuBuffer.cpp b/modules/yup_rhi/rhi/yup_GpuBuffer.cpp index 14346521f..91ef42c07 100644 --- a/modules/yup_rhi/rhi/yup_GpuBuffer.cpp +++ b/modules/yup_rhi/rhi/yup_GpuBuffer.cpp @@ -22,42 +22,99 @@ namespace yup { +namespace +{ + +//============================================================================== + +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) +struct GlStorageBuffer +{ + GlStorageBuffer() = default; + + GlStorageBuffer (GLuint id, ReferenceCountedObjectPtr device) + : id (id) + , device (std::move (device)) + { + } + + GlStorageBuffer (GlStorageBuffer&& other) noexcept + : id (std::exchange (other.id, 0)) + , device (std::move (other.device)) + { + } + + GlStorageBuffer& operator= (GlStorageBuffer&& other) noexcept + { + if (this != &other) + { + release(); + + id = std::exchange (other.id, 0); + device = std::move (other.device); + } + + return *this; + } + + ~GlStorageBuffer() + { + release(); + } + + GLuint id = 0; + ReferenceCountedObjectPtr device; + + GlStorageBuffer (const GlStorageBuffer&) = delete; + GlStorageBuffer& operator= (const GlStorageBuffer&) = delete; + +private: + void release() noexcept + { + if (id == 0) + return; + + if (device != nullptr) + { + device->runOnComputeContext ([id = id] + { + GLuint bufferToDelete = id; + glDeleteBuffers (1, &bufferToDelete); + }); + } + else + { + glDeleteBuffers (1, &id); + } + } +}; +#endif + +} // namespace + //============================================================================== struct GpuBuffer::Impl { GpuBufferType type = GpuBufferType::vertex; size_t byteSize = 0; - - // 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; +#endif - ~Impl() = default; -#elif YUP_RIVE_USE_D3D && YUP_WINDOWS +#if 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; +#endif - ~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. - */ +#if (YUP_EMSCRIPTEN && RIVE_WEBGPU) || YUP_RIVE_USE_DAWN struct ReadbackSlot { wgpu::Buffer staging; - uint64_t serial = 0; ///< Submission order, so snapshots are consumed oldest-first. + uint64_t serial = 0; bool mapPending = false; bool mapped = false; @@ -68,28 +125,15 @@ struct GpuBuffer::Impl } }; - /** 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. */ + wgpu::Buffer webgpuStorageBuffer; 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; +#endif - ~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; +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) + GlStorageBuffer glStorageBuffer; #endif }; @@ -132,16 +176,22 @@ bool GpuBuffer::isValid() const noexcept 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; + if (i->mtlStorageBuffer != nil) + return true; +#endif +#if YUP_RIVE_USE_D3D && YUP_WINDOWS + if (i->d3dStorageBuffer != nullptr) + return true; +#endif +#if (YUP_EMSCRIPTEN && RIVE_WEBGPU) || YUP_RIVE_USE_DAWN + if (i->webgpuStorageBuffer != nullptr) + return true; #endif +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_ANDROID || (YUP_WASM && RIVE_WEBGL && ! RIVE_WEBGPU) + if (i->glStorageBuffer.id != 0) + return true; +#endif + return false; } return i->oreBuffer != nullptr; @@ -164,6 +214,7 @@ 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 5151526dd..03ff0ef59 100644 --- a/modules/yup_rhi/rhi/yup_GpuBuffer.h +++ b/modules/yup_rhi/rhi/yup_GpuBuffer.h @@ -32,8 +32,8 @@ class GpuDevice; or non-indexed geometry rendering. The underlying GPU resource lives for as long as at least one GpuBuffer::Ptr exists. - Buffers are immutable by default: the data provided at creation time is - uploaded once and cannot be updated afterwards. + Buffers are created with the data provided at creation time and may be + rewritten in place afterwards via GpuDevice::updateBuffer(). @see GpuRenderPass, GraphicsContext::Options */ diff --git a/modules/yup_rhi/rhi/yup_GpuComputePass.cpp b/modules/yup_rhi/rhi/yup_GpuComputePass.cpp index 2b26d1868..079304a35 100644 --- a/modules/yup_rhi/rhi/yup_GpuComputePass.cpp +++ b/modules/yup_rhi/rhi/yup_GpuComputePass.cpp @@ -22,8 +22,6 @@ namespace yup { -//============================================================================== -// Backend factory functions — defined in native/yup_GpuComputePass_*.cpp //============================================================================== #if YUP_RIVE_USE_METAL && (YUP_MAC || YUP_IOS) @@ -39,8 +37,6 @@ std::unique_ptr yup_createComputePassImplWebGPU (GpuDevice std::unique_ptr yup_createComputePassImplGL (GpuDevice&); #endif -//============================================================================== -// GpuComputePass::Impl — base with common bindings, virtual dispatch / finish //============================================================================== struct GpuComputePass::Impl @@ -74,6 +70,7 @@ struct GpuComputePass::Impl 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; @@ -94,11 +91,13 @@ GpuComputePass GpuComputePass::begin (GpuDevice::Ptr ctx) 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); @@ -108,12 +107,14 @@ GpuComputePass GpuComputePass::begin (GpuDevice::Ptr ctx) 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; } diff --git a/modules/yup_rhi/rhi/yup_GpuComputePipeline.cpp b/modules/yup_rhi/rhi/yup_GpuComputePipeline.cpp index f2e8e33bd..b78fc41e5 100644 --- a/modules/yup_rhi/rhi/yup_GpuComputePipeline.cpp +++ b/modules/yup_rhi/rhi/yup_GpuComputePipeline.cpp @@ -40,10 +40,12 @@ ResultValue GpuComputePipeline::compile (GpuDevice::Ptr 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); @@ -51,11 +53,24 @@ ResultValue GpuComputePipeline::compile (GpuDevice::Ptr 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); + { + std::optional> result; + ctx->runOnComputeContext ([&] + { + result.emplace (yup_constructComputePipelineGL (ctx, source, workgroupSize)); + }); + + if (! result.has_value()) + return makeResultValueFail ("GL compute context is not available"); + + return std::move (*result); + } #endif + default: return makeResultValueFail ("Unsupported GPU platform for compute pipelines"); } diff --git a/modules/yup_rhi/rhi/yup_GpuFrame.cpp b/modules/yup_rhi/rhi/yup_GpuFrame.cpp index 21712b930..0f9e1733f 100644 --- a/modules/yup_rhi/rhi/yup_GpuFrame.cpp +++ b/modules/yup_rhi/rhi/yup_GpuFrame.cpp @@ -34,16 +34,6 @@ struct GpuFrame::Impl 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); }; diff --git a/modules/yup_rhi/rhi/yup_GpuPipeline.cpp b/modules/yup_rhi/rhi/yup_GpuPipeline.cpp index f4a1d0f71..0151ace49 100644 --- a/modules/yup_rhi/rhi/yup_GpuPipeline.cpp +++ b/modules/yup_rhi/rhi/yup_GpuPipeline.cpp @@ -231,7 +231,6 @@ 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; @@ -242,18 +241,8 @@ struct GpuPipeline::Impl 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> layouts; 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 - // outlive the pipeline. std::vector> vertexAttrStorage; std::vector vertexLayoutStorage; }; @@ -297,10 +286,6 @@ ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, if (fs.bindingMap == nullptr || fs.bindingMapSize == 0) return makeResultValueFail ("Fragment shader binding-map sidecar is required but not provided"); - // Populates an ore ShaderModuleDesc from a GpuShaderSource. The D3D11/D3D12 - // backends compile HLSL from source at first use (AMD drivers crash on - // cross-process DXBC), so HLSL sources must be routed through the dedicated - // hlslSource fields rather than the generic code pointer. auto fillModuleDesc = [] (rive::ore::ShaderModuleDesc& desc, const GpuShaderSource& src, rive::ore::ShaderStage stage, @@ -375,10 +360,13 @@ ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, if (m.group == e.group && m.binding == e.binding) { m.stageMask |= e.stageMask; + if (e.backendSlot[0] != rive::ore::BindingMap::kAbsent) m.slotVS = e.backendSlot[0]; + if (e.backendSlot[1] != rive::ore::BindingMap::kAbsent) m.slotFS = e.backendSlot[1]; + return; } } @@ -521,10 +509,7 @@ ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, for (uint32_t g = 0; g < numGroups; ++g) layoutPtrs[g] = layouts[g].get(); - // Create the pipeline up-front so the vertex-layout storage that backs - // PipelineDesc's raw pointers lives inside the object that owns the - // pipeline. The ore Pipeline copies PipelineDesc by value but keeps the - // vertexBuffers / attributes pointers, reading them at draw time. + // Create the pipeline up-front. auto pipe = GpuPipeline::Ptr { new GpuPipeline() }; pipe->impl = TypeErasedObject (GpuPipeline::Impl {}); @@ -563,8 +548,7 @@ ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, pipeDesc.cullMode = toOreCullMode (pipelineOptions.cullMode); pipeDesc.winding = toOreWinding (pipelineOptions.winding); - // Color targets. Default to a single alpha-blended rgba8unorm target when - // none are specified, matching the classic fullscreen post-process pipeline. + // Color targets. if (pipelineOptions.colorTargetCount == 0) { pipeDesc.colorCount = 1; @@ -596,7 +580,7 @@ ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, } } - // Depth/stencil. rgba8unorm is the ore sentinel for "no depth/stencil". + // Depth/stencil. if (pipelineOptions.depthStencil.enabled) { pipeDesc.depthStencil.format = toOreTextureFormat (pipelineOptions.depthStencil.format); @@ -633,8 +617,6 @@ ResultValue GpuPipeline::compile (GpuDevice::Ptr ctx, 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) @@ -733,11 +715,9 @@ ResultValue GpuPipeline::compileFromBundle (GpuDevice::Ptr ctx auto fsSource = fsInfo->source.toRawUTF8(); // GL / GLES bind UBO blocks and sampler units by name after linking, so - // build the name→slot fixup table for the GLSL/ESSL targets. Empty (and - // ignored) for every other backend. - const bool isGLTarget = (gpuLang == GpuShaderLanguage::glsl); + // build the name→slot fixup table for the GLSL/ESSL targets. std::vector vsFixup, fsFixup; - if (isGLTarget) + if (gpuLang == GpuShaderLanguage::glsl) { vsFixup = makeGLFixupBlob (vsInfo->reflection); fsFixup = makeGLFixupBlob (fsInfo->reflection); @@ -748,6 +728,7 @@ ResultValue GpuPipeline::compileFromBundle (GpuDevice::Ptr ctx { if (gpuLang == GpuShaderLanguage::msl && info.entryPoint == "main") return "main0"; + return info.entryPoint; }; @@ -823,7 +804,6 @@ ResultValue GpuPipeline::compileFromGlsl (GpuDevice::Ptr ctx, if (fsBundle.failed()) return makeResultValueFail ("Fragment shader compile failed: " + fsBundle.getErrorMessage()); - // Merge both stages into a single bundle for compileFromBundle(). ShaderBundle bundle; for (const auto& info : vsBundle.getReference().getShaders()) bundle.addShader (info); diff --git a/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp b/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp index a25af4f2b..2bef50491 100644 --- a/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp +++ b/modules/yup_rhi/rhi/yup_GpuRenderPass.cpp @@ -55,8 +55,6 @@ struct GpuRenderPass::Impl int height = 0; GpuRenderOptions options; - // Resolved from the bound GpuPipeline by GpuRenderPass::setPipeline (which - // is a friend of GpuPipeline). Kept alive by pipelineRef. GpuPipeline::Ptr pipelineRef; rive::ore::Pipeline* orePipeline = nullptr; const std::vector>* oreLayouts = nullptr; @@ -74,24 +72,6 @@ struct GpuRenderPass::Impl bool encode (uint32_t count, bool indexed); - // Creates an ore TextureView for a GpuTexture. The correct view kind depends - // on how the texture is used, and the preference order differs between color - // attachments and sampled inputs: - // - // - Color attachments must be bound through a render-target view. On D3D the - // canvas wrapper (wrapCanvasTexture) exposes the RTV; binding an SRV-backed - // rive-texture view instead leaves no RTV bound and the draw is discarded - // (DEVICE_DRAW_RENDERTARGETVIEW_NOT_SET). Prefer wrapCanvasTexture, and - // fall back to the underlying GPU texture. - // - Sampled inputs must be bound through an SRV-backed view. wrapCanvasTexture - // only exposes a render-target view, which has no shader-resource view on - // D3D - sampling it reads nothing. Prefer the underlying GPU texture, which - // wrapRiveTexture() wraps with a proper SRV, and fall back to the canvas - // view. - // - // This is a member of the nested Impl so it can read GpuTexture internals: - // GpuRenderPass is a friend of GpuTexture, and a nested class shares the - // enclosing class's access rights (C++11). static rive::rcp createView (rive::ore::Context& oreCtx, const GpuTexture& tex, bool forRenderTarget) { if (forRenderTarget) @@ -144,8 +124,6 @@ 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) @@ -154,8 +132,6 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) 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]; @@ -167,14 +143,11 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) // UBO entries for this group. std::vector uboEntries; - for (const auto& ub : uboBindings) { if (ub.group != (int) groupIdx) continue; - // 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; @@ -189,7 +162,6 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) // Texture entries for this group. std::vector texEntries; - for (const auto& tb : textureBindings) { if (tb.group != (int) groupIdx || tb.texture == nullptr) @@ -206,12 +178,8 @@ bool GpuRenderPass::Impl::encode (uint32_t count, bool indexed) framePools->liveViews.push_back (std::move (view)); } - // 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. + // Sampler entries. std::vector sampEntries; - if (groupSamplers != nullptr) { for (const auto& sb : *groupSamplers) diff --git a/modules/yup_rhi/rhi/yup_GpuTarget.cpp b/modules/yup_rhi/rhi/yup_GpuTarget.cpp index 0795ed46f..6e7addaa6 100644 --- a/modules/yup_rhi/rhi/yup_GpuTarget.cpp +++ b/modules/yup_rhi/rhi/yup_GpuTarget.cpp @@ -78,8 +78,6 @@ GpuTexture::Ptr GpuTarget::asTexture() if (auto canvas = target.getRenderCanvas()) { cachedTexture = GpuTexture::fromRenderCanvas (std::move (canvas), w, h); - // Only attach the Y-flip mirror if it was already created; GPU - // render-pass targets never create one. cachedTexture->sampledTexture = target.getSampledTexture(); } else if (auto tex = target.adoptAsTexture()) diff --git a/modules/yup_rhi/yup_rhi.cpp b/modules/yup_rhi/yup_rhi.cpp index 0a27688d7..b91ddc4b1 100644 --- a/modules/yup_rhi/yup_rhi.cpp +++ b/modules/yup_rhi/yup_rhi.cpp @@ -24,10 +24,16 @@ //============================================================================== #if YUP_WINDOWS #if YUP_RIVE_USE_D3D +#include #include +#include + +#include #include + #include #endif + #if YUP_RIVE_USE_OPENGL #include #endif @@ -47,7 +53,9 @@ #include #include #endif + #include + #endif #if YUP_RIVE_USE_DAWN diff --git a/tests/yup_gui/yup_ComponentNative.cpp b/tests/yup_gui/yup_ComponentNative.cpp index b16778816..e2f727357 100644 --- a/tests/yup_gui/yup_ComponentNative.cpp +++ b/tests/yup_gui/yup_ComponentNative.cpp @@ -92,14 +92,20 @@ class StubComponentNative final : public ComponentNative void enableWireframe (bool) override {} - void repaint() override {} + void repaint() override + { + repaintAreas.clearQuick(); + repaintAreas.add (component.getBounds()); + } - void repaint (const Rectangle&) override {} + void repaint (const Rectangle& rect) override + { + repaintAreas.add (rect); + } const RectangleList& getRepaintAreas() const override { - static RectangleList r; - return r; + return repaintAreas; } void startTextInput (Component&) override {} @@ -124,6 +130,8 @@ class StubComponentNative final : public ComponentNative Flags getFlags() const { return flags; } + RectangleList repaintAreas; + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StubComponentNative) }; @@ -380,3 +388,47 @@ TEST_F (ComponentNativeConstructionTests, DestructorDoesNotCrash) } SUCCEED(); } + +// ============================================================================== +// ComponentNative — repaint-area contract +// +// The render thread consumes the repaint areas produced by repaint()/repaint(rect) +// on the message thread, so the accumulation/clear semantics must be predictable. +// ============================================================================== + +class ComponentNativeRepaintTests : public ::testing::Test +{ +protected: + void SetUp() override + { + comp.setBounds (0, 0, 100, 100); + } + + Component comp; +}; + +TEST_F (ComponentNativeRepaintTests, RectRepaintAccumulatesIntoRepaintAreas) +{ + StubComponentNative native (comp, ComponentNative::defaultFlags); + + const Rectangle area (10.0f, 20.0f, 30.0f, 40.0f); + native.repaint (area); + + EXPECT_EQ (1, native.getRepaintAreas().getNumRectangles()); + EXPECT_TRUE (native.getRepaintAreas().contains (area)); +} + +TEST_F (ComponentNativeRepaintTests, FullRepaintResetsPendingAreas) +{ + StubComponentNative native (comp, ComponentNative::defaultFlags); + + native.repaint (Rectangle (1.0f, 2.0f, 3.0f, 4.0f)); + native.repaint (Rectangle (5.0f, 6.0f, 7.0f, 8.0f)); + EXPECT_EQ (2, native.getRepaintAreas().getNumRectangles()); + + // A full repaint replaces the accumulated region with the component bounds. + native.repaint(); + + EXPECT_EQ (1, native.getRepaintAreas().getNumRectangles()); + EXPECT_TRUE (native.getRepaintAreas().contains (comp.getBounds())); +} diff --git a/tests/yup_rhi/yup_GpuComputePass.cpp b/tests/yup_rhi/yup_GpuComputePass.cpp index 8c65ecb9e..ef8ba3c0a 100644 --- a/tests/yup_rhi/yup_GpuComputePass.cpp +++ b/tests/yup_rhi/yup_GpuComputePass.cpp @@ -60,6 +60,17 @@ TEST_F (GpuComputePassHeadlessTests, IsValidReturnsFalseForDefaultConstructed) EXPECT_FALSE (pass.isValid()); } +TEST_F (GpuComputePassHeadlessTests, RunOnComputeContextRunsWorkSynchronously) +{ + // Backends without a dedicated compute context run the work directly. + bool ran = false; + device->runOnComputeContext ([&] + { + ran = true; + }); + EXPECT_TRUE (ran); +} + TEST_F (GpuComputePassHeadlessTests, SetPipelineOnInvalidPassDoesNotCrash) { auto pass = GpuComputePass::begin (device); diff --git a/thirdparty/rive_renderer/rive_renderer.h b/thirdparty/rive_renderer/rive_renderer.h index 811a4a7c2..210badee4 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 3 (OpenGL 4.3+, required for compute shaders). + Enables a speficic OpenGL minor version. Must be at least 5 (OpenGL 4.5+, required for compute shaders). */ #ifndef YUP_RIVE_OPENGL_MINOR -#define YUP_RIVE_OPENGL_MINOR 3 +#define YUP_RIVE_OPENGL_MINOR 5 #endif //============================================================================== diff --git a/thirdparty/rive_renderer/rive_renderer_windows.cpp b/thirdparty/rive_renderer/rive_renderer_windows.cpp index fc7f09e4f..543b79849 100644 --- a/thirdparty/rive_renderer/rive_renderer_windows.cpp +++ b/thirdparty/rive_renderer/rive_renderer_windows.cpp @@ -52,6 +52,7 @@ #include "source/gl/load_store_actions_ext.cpp" #include "source/gl/pls_impl_ext_native.cpp" #include "source/gl/pls_impl_rw_texture.cpp" +#include "source/gl/pls_impl_webgl.cpp" #include "source/gl/render_buffer_gl_impl.cpp" #include "source/gl/render_context_gl_impl.cpp" #include "source/gl/render_target_gl.cpp" diff --git a/thirdparty/rive_renderer/source/ore/gl/ore_context_gl.cpp b/thirdparty/rive_renderer/source/ore/gl/ore_context_gl.cpp index 52da6a4d5..aae7186b6 100644 --- a/thirdparty/rive_renderer/source/ore/gl/ore_context_gl.cpp +++ b/thirdparty/rive_renderer/source/ore/gl/ore_context_gl.cpp @@ -961,6 +961,8 @@ std::unique_ptr ContextGL::beginRenderPass( pass->m_ownsFBO = true; glBindFramebuffer(GL_FRAMEBUFFER, pass->m_glFBO); + glDisable(GL_SCISSOR_TEST); + // Attach color targets. GLenum drawBuffers[4] = {}; for (uint32_t i = 0; i < desc.colorCount; ++i) @@ -1066,6 +1068,10 @@ std::unique_ptr ContextGL::beginRenderPass( "Ore GL FBO incomplete"); // Handle clear ops. + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + glStencilMask(0xFF); + for (uint32_t i = 0; i < desc.colorCount; ++i) { const auto& ca = desc.colorAttachments[i]; diff --git a/tools/rive_update_manifest.json b/tools/rive_update_manifest.json index 40a59650d..e7b7119e5 100644 --- a/tools/rive_update_manifest.json +++ b/tools/rive_update_manifest.json @@ -201,6 +201,50 @@ "}" ] } + }, + { + "path": "source/ore/gl/ore_context_gl.cpp", + "note": "Ore GL render pass: disable the scissor test for the duration of the pass, since the host renderer may leave a stale scissor rect enabled that would clip the pass's clears and draws", + "already_contains": " glBindFramebuffer(GL_FRAMEBUFFER, pass->m_glFBO);\n\n glDisable(GL_SCISSOR_TEST);", + "replace": { + "from": [ + " // Create an FBO for this render pass.", + " glGenFramebuffers(1, &pass->m_glFBO);", + " pass->m_ownsFBO = true;", + " glBindFramebuffer(GL_FRAMEBUFFER, pass->m_glFBO);", + "", + " // Attach color targets." + ], + "to": [ + " // Create an FBO for this render pass.", + " glGenFramebuffers(1, &pass->m_glFBO);", + " pass->m_ownsFBO = true;", + " glBindFramebuffer(GL_FRAMEBUFFER, pass->m_glFBO);", + "", + " glDisable(GL_SCISSOR_TEST);", + "", + " // Attach color targets." + ] + } + }, + { + "path": "source/ore/gl/ore_context_gl.cpp", + "note": "Ore GL render pass: force the color/depth/stencil write masks on before the clears — the host renderer may leave them disabled, which silently discards glClearBuffer* and leaves stale content in reused render targets (trails/ghosting)", + "already_contains": " glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);\n glDepthMask(GL_TRUE);\n glStencilMask(0xFF);", + "replace": { + "from": [ + " // Handle clear ops.", + " for (uint32_t i = 0; i < desc.colorCount; ++i)" + ], + "to": [ + " // Handle clear ops.", + " glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);", + " glDepthMask(GL_TRUE);", + " glStencilMask(0xFF);", + "", + " for (uint32_t i = 0; i < desc.colorCount; ++i)" + ] + } } ], "shader_generation": {