feat(ore): scripts render GPU passes into our render target (#14436) cf39effc4d * feat(ore): scripts render GPU passes into Rive's render target under its content * fix(ore): utf8 dash in the target GM wrapper * feat(ore): scripts render into our Metal target and top down GL texture targets * feat(ore): our iOS, Flutter Metal, Android Vulkan, recorder and cli hosts expose their target * fix(ore): leave the Vulkan host target an attachment after a pass, and add its goldens * feat(ore): our Windows Flutter host exposes its target to scripts * feat(ore): our Unreal host exposes its render target to scripts * fix(scripting): move GPUTarget off the userdata tag Text now uses * test(ore): target gm goldens for web, android, iossim and streamer lanes * refactor(ore): share target descriptions across hosts and drop the lazy target claims * fix(ore): release the target after each frame, skip a resized one, refuse sampling it and keep its view while hidden * fix(scripting): report stale target views and share one target object per script * perf(ore): leave the D3D12 target a render target across script passes * test(ore): run the target parity family, cover a failed wrap and expose the Android Vulkan target * fix(renderer): skip the D3D12 target transition when Ore already left it a render target * fix(ore): free the D3D12 host target wrap on release so the host can resize its swapchain * fix(scripting): stop leaking the target view when a target pass raises a Lua error * fix(rive_native): acquire the Windows swapchain texture before Ore replays so both draw into it * test(unreal): expose the gm window's target so the target gm draws through Ore * test(unreal): keep the gm window's target hidden until preserve works on our Unreal gm lane * test(ore): overshoot the target GM fallback so a short edge cannot blend the last frame * test(ore): android vulkan target goldens now that the lane draws through Ore * fix(ore): clear a target resized since a script drew into it * fix(evi): read window size and format safely off the render thread and keep srgb targets hidden * fix(ore): expose the current back buffer from the d3d12 fiddle * fix(scripting): keep the AS pass desc intact, hold the target from init, and match Luau and AS on hidden and stale targets * fix(scripting): drop the target view rcp before a pass can raise, and regenerate the wasm host stubs * docs(scripting): read the target view each frame since a kept one errors after a resize * chore(rasc_site): rebuild the repl Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head index e2f3294..1b5a40f 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -f42f02a06d63bc4ca64b2ceb223ec4475784b289 +cf39effc4d5fb53f3a1c0230d3f9aa3b5b6a26be
diff --git a/include/rive/lua/rive_lua_libs.hpp b/include/rive/lua/rive_lua_libs.hpp index 9f19b37..2cd51aa 100644 --- a/include/rive/lua/rive_lua_libs.hpp +++ b/include/rive/lua/rive_lua_libs.hpp
@@ -341,6 +341,8 @@ features, shader, format, + gpuTarget, + sampleCount, // Promise andThen, @@ -937,6 +939,15 @@ uint32_t pendingHeight = 0; }; +// Rive's render target, read live from the Ore context on every access. +class ScriptedGPUTarget +{ +public: + static constexpr uint8_t luaTag = LUA_T_COUNT + 73; + static constexpr const char* luaName = "GPUTarget"; + static constexpr bool hasMetatable = true; +}; + #endif // RIVE_ORE enum class CanvasState
diff --git a/include/rive/wasm/rive_bindings_v1.h b/include/rive/wasm/rive_bindings_v1.h index f0bbb8d..e065559 100644 --- a/include/rive/wasm/rive_bindings_v1.h +++ b/include/rive/wasm/rive_bindings_v1.h
@@ -588,6 +588,8 @@ uint32_t rive_gpu_canvas_image(uint32_t canvas); RIVE_BINDING_IMPORT("rive_gpu_v1", "canvas_resize") uint32_t rive_gpu_canvas_resize(uint32_t canvas, uint32_t width, uint32_t height, uint32_t* props, uint32_t propCount); +RIVE_BINDING_IMPORT("rive_gpu_v1", "target_view") +uint32_t rive_gpu_target_view(uint32_t current, uint32_t* props, uint32_t propCount); RIVE_BINDING_IMPORT("rive_gpu_v1", "pass_begin") uint32_t rive_gpu_pass_begin(const rive_gpu_pass_desc_v1* desc, uint32_t descByteCount, const rive_gpu_pass_color_attachment_v1* colors, uint32_t colorByteCount); RIVE_BINDING_IMPORT("rive_gpu_v1", "pass_set_pipeline")
diff --git a/renderer/include/rive/renderer/cmd/deferred_host.hpp b/renderer/include/rive/renderer/cmd/deferred_host.hpp index 3a27ac0..100ac08 100644 --- a/renderer/include/rive/renderer/cmd/deferred_host.hpp +++ b/renderer/include/rive/renderer/cmd/deferred_host.hpp
@@ -58,6 +58,10 @@ // GL, Metal), where zero serials also disable backing reuse. virtual rive::ore::Context::FrameDescriptor oreFrame() { return {}; } void afterOreFrame() override; + void setTargetPreserved(bool preserved) override + { + m_targetPreserved = preserved; + } // The interlock choice a frame is opened with. A canvas frame takes the // screen's unless canvasMode says otherwise, so a canvas never lands in a // mode the host did not choose. @@ -94,6 +98,7 @@ private: bool m_clear; + bool m_targetPreserved = false; uint32_t m_color; uint64_t m_target; // One sink serves every target of one replayed frame, so whether Ore runs @@ -128,8 +133,12 @@ : m_session->screenRenderer(m_target); } - // Frame open: stash the clear policy and mark the Ore replay point. - void beginRecord(bool clear, uint32_t color); + // Frame open: stash the clear policy, declare the render target scripts + // may draw into, and mark the Ore replay point. A zero width target keeps + // it hidden. + void beginRecord(bool clear, + uint32_t color, + const ore::Context::TargetDesc& target = {}); bool doClear() const { return m_doClear; } uint32_t clearColor() const { return m_color; }
diff --git a/renderer/include/rive/renderer/cmd/deferred_replayer.hpp b/renderer/include/rive/renderer/cmd/deferred_replayer.hpp index bbab986..620436a 100644 --- a/renderer/include/rive/renderer/cmd/deferred_replayer.hpp +++ b/renderer/include/rive/renderer/cmd/deferred_replayer.hpp
@@ -10,6 +10,7 @@ #include "rive/renderer/cmd/render_replay.hpp" #include "rive/renderer/ore/cmd/ore_replay.hpp" #include "rive/renderer/render_context_impl.hpp" +#include "rive/renderer/render_target.hpp" #include <algorithm> #include <deque> #include <mutex> @@ -58,6 +59,13 @@ // Backend state fix up after an Ore frame, such as a GL state invalidate. virtual void afterOreFrame() {} + // The render target Ore's target view wraps this frame, null where the + // host exposes none to scripts. + virtual gpu::RenderTarget* targetRenderTarget() { return nullptr; } + // Script GPU work drew into the target under Rive's content, so the + // screen frame loads it rather than clearing. Told before it opens. + virtual void setTargetPreserved(bool) {} + // Open the canvas's own frame and return the renderer its draws route // into, or null to drop them. endCanvasContent flushes it. virtual Renderer* beginCanvasContent(gpu::RenderCanvas* /*canvas*/, @@ -92,6 +100,8 @@ // The caps the recording answered with, so a replay far from the session // can still verify it runs on the device the stream was declared for. ore::ReplayCaps oreCaps; + // The packed size of the target scripts drew into, or 0. + uint32_t oreTargetSize = 0; }; inline DeferredFrame snapshotFrame(DeferredSession& session) @@ -113,6 +123,7 @@ f.oreReals = session.oreContext().realResources(); f.segments = session.schedulerSegments(); f.oreCaps = session.oreContext().caps(); + f.oreTargetSize = session.oreContext().drawnTargetSize(); return f; } @@ -145,7 +156,8 @@ session.oreContext().realResources(), sink, session.schedulerSegments(), - session.oreContext().caps()); + session.oreContext().caps(), + session.oreContext().drawnTargetSize()); } // Snapshot form: replay an owned frame, independent of the session. @@ -169,7 +181,8 @@ frame.oreReals, sink, frame.segments, - frame.oreCaps); + frame.oreCaps, + frame.oreTargetSize); } // Drop the resident tables so the next replay recreates everything. @@ -202,9 +215,20 @@ const std::vector<rcp<rive::gpu::GPUResource>>& oreReals, DeferredFrameSink& sink, const std::vector<DeferredSegment>& segments, - const ore::ReplayCaps& oreCaps) + const ore::ReplayCaps& oreCaps, + uint32_t oreTargetSize) { m_stats = ReplayStats{}; + // With no Ore context or a target resized since recording the passes + // drop, and a load would show whatever the target held. + gpu::RenderTarget* target = + oreTargetSize != 0 && sink.oreContext() != nullptr + ? sink.targetRenderTarget() + : nullptr; + sink.setTargetPreserved( + target != nullptr && + ore::cmd::packTargetSize(target->width(), target->height()) == + oreTargetSize); m_2d.clearVersionAliases(); ReplayHooks hooks; hooks.stats = &m_stats; @@ -279,6 +303,7 @@ assert(false && "replay device does not match declared caps"); } sink.beginOreFrame(); + realOre->setRenderTarget(sink.targetRenderTarget()); ore::cmd::replayOreStream( *realOre, oreCommands, @@ -310,7 +335,9 @@ // or an earlier frame. return m_2d.images.get(imageId); }); + realOre->setRenderTarget(nullptr); sink.endOreFrame(); + m_ore.releaseFrameTargets(); sink.afterOreFrame(); }; @@ -353,12 +380,10 @@ } const std::vector<uint64_t>& canvasOrder = schedule.order; // Ore replays once for the whole session frame, inside the first - // screen frame opened. The scripting GPU surface is canvas scoped - - // render passes exist only as canvas:beginRenderPass, with no screen - // or frame target type - so a script writes canvases and never - // screens, and its output reaches a target indirectly through 2D - // draws that sample canvas textures. Running it in whichever screen - // frame opens first therefore orders it ahead of every target's draws + // screen frame opened. A script writes canvases, which reach a target + // through 2D draws that sample them, and the host's one target view, + // which lands under every draw. Running it in whichever screen frame + // opens first therefore orders it ahead of every target's draws // without attributing it to any one of them. The decision is owned // here, at session frame scope, so per target sinks cannot disagree // about whether Ore ran at all.
diff --git a/renderer/include/rive/renderer/ore/cmd/ore_commands.hpp b/renderer/include/rive/renderer/ore/cmd/ore_commands.hpp index cb8c37e..fb364bd 100644 --- a/renderer/include/rive/renderer/ore/cmd/ore_commands.hpp +++ b/renderer/include/rive/renderer/ore/cmd/ore_commands.hpp
@@ -7,6 +7,7 @@ #include "rive/renderer/ore/ore_types.hpp" #include "rive/renderer/ore/cmd/ore_handle.hpp" #include "rive/renderer/ore/cmd/ore_resource_commands.hpp" +#include <cassert> #include <cstddef> #include <cstdint> @@ -88,14 +89,24 @@ colorView = 0, // the canvas's own render target view sampleView = 1, // sampling wrap, on GL needs the top up mirror imageView = 2, // decoded image, canvasId carries the 2D image id + targetView = 3, // the replaying context's renderTarget, wrapped per frame }; +// A script recorded against this size, so replay skips a target that differs. +inline uint32_t packTargetSize(uint32_t width, uint32_t height) +{ + assert(width <= 0xffff && height <= 0xffff); + return width << 16 | height; +} + struct WrapCanvasViewPOD { ResourceHandle id; uint32_t generation; - uint32_t canvasId; // canvas id, or the 2D image id for imageView - uint32_t mode; // WrapCanvasViewMode + // Canvas id, the 2D image id for imageView, or the recorded size packed + // by packTargetSize for targetView. + uint32_t canvasId; + uint32_t mode; // WrapCanvasViewMode }; // The consumer clears the slot only when the generation matches, so a stale
diff --git a/renderer/include/rive/renderer/ore/cmd/ore_deferred_context.hpp b/renderer/include/rive/renderer/ore/cmd/ore_deferred_context.hpp index 6e9b773..8b968c6 100644 --- a/renderer/include/rive/renderer/ore/cmd/ore_deferred_context.hpp +++ b/renderer/include/rive/renderer/ore/cmd/ore_deferred_context.hpp
@@ -180,6 +180,15 @@ } for (uint32_t i = 0; i < desc.textureCount; ++i) { + // The target is rewrapped every frame and may not be sampleable. + auto* d = rive::lite_rtti_cast<DeferredTextureView*>( + desc.textures[i].view); + if (d != nullptr && d->hostTarget) + { + setLastError("makeBindGroup: the target view cannot be " + "sampled"); + return nullptr; + } texs[i] = handleFor(desc.textures[i].view); } for (uint32_t i = 0; i < desc.samplerCount; ++i) @@ -207,9 +216,85 @@ const RenderPassDesc& desc, std::string* /*outError*/ = nullptr) override { + bool drawsTarget = false; + for (uint32_t i = 0; i < desc.colorCount; ++i) + { + const ColorAttachment& c = desc.colorAttachments[i]; + for (TextureView* view : {c.view, c.resolveTarget}) + { + auto* d = rive::lite_rtti_cast<DeferredTextureView*>(view); + if (d == nullptr || !d->hostTarget) + { + continue; + } + // A retired target view names an image that may be gone. + if (d != m_targetView.get()) + { + setLastError("beginRenderPass: stale target view"); + return nullptr; + } + // Like a frame with no target, the pass drops without error. + if (m_targetHidden) + { + return nullptr; + } + drawsTarget = true; + } + } + if (drawsTarget && !m_targetWrapped) + { + // The host may hand over a different image every frame. + recordWrapTargetView(); + } + m_targetDrawn = m_targetDrawn || drawsTarget; return std::make_unique<RenderPassRecording>(this, &m_render, desc); } + // The host declares its target before scripts record. A changed target + // gets a new view, since the view carries its size and format. A host + // that hides its target for a frame keeps the view for when it returns. + void setTarget(const TargetDesc& desc) + { + m_targetHidden = desc.width == 0; + if (!m_targetHidden && desc != m_target) + { + m_target = desc; + m_targetView = nullptr; + } + } + + rcp<TextureView> targetView() override + { + if (m_targetHidden) + { + return nullptr; + } + if (m_targetView == nullptr) + { + auto a = m_ids.alloc(); + m_targetView = makeProxyView(a.id, + a.generation, + m_target.width, + m_target.height, + m_target.format, + m_target.sampleCount); + m_targetView->hostTarget = true; + // Replay fills ids in mint order, so the slot is claimed now. + recordWrapTargetView(); + } + return m_targetView; + } + + // Script GPU work drew into the target this frame, so Rive's frame must + // load it rather than clear it. + bool targetDrawn() const { return m_targetDrawn; } + // The packed size scripts drew the target at this frame, or 0. + uint32_t drawnTargetSize() const + { + return m_targetDrawn ? packTargetSize(m_target.width, m_target.height) + : 0; + } + // Maps a canvas to its shared canvas id for replay. Set by the // DeferredSession; when set wrapCanvasTexture never touches the device. std::function<uint32_t(gpu::RenderCanvas*)> canvasIdProvider; @@ -262,14 +347,28 @@ uint32_t width, uint32_t height) { + return makeProxyView(id, + generation, + width, + height, + m_caps.canvasTargetFormat, + 1); + } + rcp<DeferredTextureView> makeProxyView(ResourceHandle id, + uint32_t generation, + uint32_t width, + uint32_t height, + TextureFormat format, + uint32_t sampleCount) + { TextureDesc texDesc{}; texDesc.width = width; texDesc.height = height; - texDesc.format = m_caps.canvasTargetFormat; + texDesc.format = format; texDesc.type = TextureType::texture2D; texDesc.renderTarget = true; texDesc.numMipmaps = 1; - texDesc.sampleCount = 1; + texDesc.sampleCount = sampleCount; auto proxyTex = make_rcp<DeferredTexture>(0u, 0u, nullptr, nullptr, texDesc); TextureViewDesc viewDesc{}; @@ -482,6 +581,8 @@ // A pass still open would append its finish into the next frame. finishOpenRenderPassesFrom(0); m_render.reset(); + m_targetWrapped = false; + m_targetDrawn = false; // Cross thread destroys drain on the recording thread, landing at the // new frame's stream head. m_render.drainDestroys(); @@ -541,6 +642,16 @@ } } + void recordWrapTargetView() + { + recordWrapCanvasView(m_render, + m_targetView->clientHandle(), + m_targetView->generation(), + packTargetSize(m_target.width, m_target.height), + WrapCanvasViewMode::targetView); + m_targetWrapped = true; + } + // Resolves a flagged real id to its retained real object. rive::gpu::GPUResource* resolveReal(ResourceHandle h) { @@ -602,6 +713,12 @@ // addressed by flagged id. Cleared every resetFrame. PtrHandleMap m_realPtrToHandle; std::vector<rcp<rive::gpu::GPUResource>> m_realResources; + TargetDesc m_target; + rcp<DeferredTextureView> m_targetView; + // This frame's stream already wraps the target. + bool m_targetWrapped = false; + bool m_targetDrawn = false; + bool m_targetHidden = true; }; } // namespace rive::ore::cmd
diff --git a/renderer/include/rive/renderer/ore/cmd/ore_deferred_resource.hpp b/renderer/include/rive/renderer/ore/cmd/ore_deferred_resource.hpp index f179bc2..488d69a 100644 --- a/renderer/include/rive/renderer/ore/cmd/ore_deferred_resource.hpp +++ b/renderer/include/rive/renderer/ore/cmd/ore_deferred_resource.hpp
@@ -52,6 +52,7 @@ // recycling: an address only names this handle for as long as this object // occupies it. ResourceHandle clientHandle() const { return m_clientHandle; } + uint32_t generation() const { return m_generation; } // A handle resolves against the table this resource's stream feeds, so a // reader that writes a different stream must not use it. @@ -159,6 +160,9 @@ desc), DeferredResource(handle, generation, stream, allocator) {} + + // Stands in for the host render target, wrapped anew each frame. + bool hostTarget = false; }; class DeferredSampler : public LITE_RTTI_OVERRIDE(Sampler, DeferredSampler),
diff --git a/renderer/include/rive/renderer/ore/cmd/ore_make_replay.hpp b/renderer/include/rive/renderer/ore/cmd/ore_make_replay.hpp index d960f58..ab365fa 100644 --- a/renderer/include/rive/renderer/ore/cmd/ore_make_replay.hpp +++ b/renderer/include/rive/renderer/ore/cmd/ore_make_replay.hpp
@@ -108,6 +108,18 @@ return id < objects.size() && objects[id] != nullptr && generations[id] == generation; } + + // A wrapped host target must not outlive its frame: the host may resize + // or present the image once the frame ends. + std::vector<ResourceHandle> frameTargets; + void releaseFrameTargets() + { + for (ResourceHandle id : frameTargets) + { + objects[id] = nullptr; + } + frameTargets.clear(); + } }; // A flagged id is an already real resource; any other id indexes the resident @@ -550,6 +562,30 @@ { // The consumer performs the real wrap reserved at record time. auto pod = reader.read<WrapCanvasViewPOD>(); + if (pod.mode == + static_cast<uint32_t>(WrapCanvasViewMode::targetView)) + { + rive::gpu::RenderTarget* target = ctx.renderTarget(); + rcp<TextureView> wrapped = + target != nullptr && + packTargetSize(target->width(), target->height()) == + pod.canvasId + ? ctx.wrapRenderTarget(target) + : nullptr; + if (wrapped == nullptr) + { + return skipUnresolvedMake(pod.id, + pod.generation, + "targetView", + nullptr); + } + table.set(pod.id, + std::move(wrapped), + pod.generation, + OreKind::textureView); + table.frameTargets.push_back(pod.id); + return true; + } if (table.alive(pod.id, pod.generation)) { return true;
diff --git a/renderer/include/rive/renderer/ore/ore_context.hpp b/renderer/include/rive/renderer/ore/ore_context.hpp index a2186e2..1be061f 100644 --- a/renderer/include/rive/renderer/ore/ore_context.hpp +++ b/renderer/include/rive/renderer/ore/ore_context.hpp
@@ -32,6 +32,7 @@ namespace rive::gpu { class RenderCanvas; +class RenderTarget; class Texture; } // namespace rive::gpu @@ -184,6 +185,48 @@ virtual rcp<TextureView> wrapCanvasTexture(gpu::RenderCanvas* canvas) = 0; + // What a script sees of the host's render target; it builds its + // pipelines against these. Zero width means no target is exposed. + struct TargetDesc + { + uint32_t width = 0; + uint32_t height = 0; + TextureFormat format = TextureFormat::rgba8unorm; + uint32_t sampleCount = 1; + + bool operator==(const TargetDesc& o) const + { + return width == o.width && height == o.height && + format == o.format && sampleCount == o.sampleCount; + } + bool operator!=(const TargetDesc& o) const { return !(*this == o); } + + // The single sampled 8 bit target a window host exposes. + static TargetDesc color8(uint32_t width, uint32_t height, bool bgra) + { + return {width, + height, + bgra ? TextureFormat::bgra8unorm + : TextureFormat::rgba8unorm, + 1}; + } + }; + + // Backends that cannot render into a host's own render target return + // null, which drops the passes into it. + virtual rcp<TextureView> wrapRenderTarget(gpu::RenderTarget*) + { + return nullptr; + } + + // The host render target scripts draw into under Rive's content. Only the + // recording context hands one out, wrapped again each frame at replay. + virtual rcp<TextureView> targetView() { return nullptr; } + + // Set by the host around each Ore frame it replays. + void setRenderTarget(gpu::RenderTarget* target) { m_renderTarget = target; } + gpu::RenderTarget* renderTarget() const { return m_renderTarget; } + // Color format makeRenderCanvas allocates. A backend that allocates // anything other than rgba8 must override or canvas draws fail the // pipeline compat check at replay. @@ -427,6 +470,8 @@ bool m_deferredRecording = false; + gpu::RenderTarget* m_renderTarget = nullptr; + std::unordered_map<uint64_t, rcp<BindGroupLayout>> m_internedLayouts; cmd::OreCommandBuffer m_pendingFrame;
diff --git a/renderer/include/rive/renderer/ore/ore_context_d3d11.hpp b/renderer/include/rive/renderer/ore/ore_context_d3d11.hpp index 48a8142..4913bea 100644 --- a/renderer/include/rive/renderer/ore/ore_context_d3d11.hpp +++ b/renderer/include/rive/renderer/ore/ore_context_d3d11.hpp
@@ -41,6 +41,7 @@ void waitForGPU() override; rcp<TextureView> wrapCanvasTexture(gpu::RenderCanvas* canvas) override; + rcp<TextureView> wrapRenderTarget(gpu::RenderTarget* target) override; rcp<TextureView> wrapRiveTexture(gpu::Texture* gpuTex, uint32_t width, uint32_t height) override; @@ -70,7 +71,7 @@ const BindGroupLayoutDesc& desc); std::unique_ptr<RenderPass> d3d11BeginRenderPass(const RenderPassDesc& desc, std::string* outError); - rcp<TextureView> d3d11WrapCanvasTexture(gpu::RenderCanvas* canvas); + rcp<TextureView> d3d11WrapTarget(gpu::RenderTarget* target, bool canvas); rcp<TextureView> d3d11WrapRiveTexture(gpu::Texture* gpuTex, uint32_t w, uint32_t h);
diff --git a/renderer/include/rive/renderer/ore/ore_context_d3d12.hpp b/renderer/include/rive/renderer/ore/ore_context_d3d12.hpp index 82d3291..6d9aec5 100644 --- a/renderer/include/rive/renderer/ore/ore_context_d3d12.hpp +++ b/renderer/include/rive/renderer/ore/ore_context_d3d12.hpp
@@ -51,6 +51,7 @@ uint64_t safeFrameNumber() const { return m_safeFrameNumber; } rcp<TextureView> wrapCanvasTexture(gpu::RenderCanvas* canvas) override; + rcp<TextureView> wrapRenderTarget(gpu::RenderTarget* target) override; rcp<TextureView> wrapRiveTexture(gpu::Texture* gpuTex, uint32_t width, uint32_t height) override; @@ -89,7 +90,7 @@ const BindGroupLayoutDesc& desc); std::unique_ptr<RenderPass> d3d12BeginRenderPass(const RenderPassDesc& desc, std::string* outError); - rcp<TextureView> d3d12WrapCanvasTexture(gpu::RenderCanvas* canvas); + rcp<TextureView> d3d12WrapTarget(gpu::RenderTarget* target, bool canvas); rcp<TextureView> d3d12WrapRiveTexture(gpu::Texture* gpuTex, uint32_t w, uint32_t h); @@ -125,6 +126,8 @@ Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> m_d3dCpuSamplerHeap; UINT m_d3dCpuSrvAllocated = 0; UINT m_d3dCpuRtvAllocated = 0; + // Host targets are wrapped anew every frame, so they share one RTV slot. + UINT m_d3dTargetRtvIndex = UINT_MAX; UINT m_d3dCpuDsvAllocated = 0; UINT m_d3dCpuSamplerAllocated = 0; UINT m_d3dSrvDescSize = 0;
diff --git a/renderer/include/rive/renderer/ore/ore_context_gl.hpp b/renderer/include/rive/renderer/ore/ore_context_gl.hpp index 1a472f8..7402976 100644 --- a/renderer/include/rive/renderer/ore/ore_context_gl.hpp +++ b/renderer/include/rive/renderer/ore/ore_context_gl.hpp
@@ -8,6 +8,11 @@ #include <unordered_map> +namespace rive::gpu +{ +class TextureRenderTargetGL; +} // namespace rive::gpu + // Note: load_gles_extensions.hpp (glad) is intentionally NOT included here. // The private GL state only needs 'int' (GLint is always int), keeping this // header free of glad so it can be included without glad in the search path. @@ -60,6 +65,7 @@ bool usesDeferredFrameReplay() const override { return false; } rcp<TextureView> wrapCanvasTexture(gpu::RenderCanvas* canvas) override; + rcp<TextureView> wrapRenderTarget(gpu::RenderTarget* target) override; rcp<TextureView> wrapRiveTexture(gpu::Texture* gpuTex, uint32_t width, uint32_t height) override; @@ -76,6 +82,8 @@ ContextGL() : Context(nullptr) {} + rcp<TextureView> glWrapTarget(gpu::TextureRenderTargetGL* target); + // An incomplete framebuffer repeats every frame; print each message once. std::string m_lastReportedError;
diff --git a/renderer/include/rive/renderer/ore/ore_context_metal.hpp b/renderer/include/rive/renderer/ore/ore_context_metal.hpp index 6ea6c2f..b257f16 100644 --- a/renderer/include/rive/renderer/ore/ore_context_metal.hpp +++ b/renderer/include/rive/renderer/ore/ore_context_metal.hpp
@@ -49,6 +49,7 @@ bool usesDeferredFrameReplay() const override { return true; } rcp<TextureView> wrapCanvasTexture(gpu::RenderCanvas* canvas) override; + rcp<TextureView> wrapRenderTarget(gpu::RenderTarget* target) override; rcp<TextureView> wrapRiveTexture(gpu::Texture* gpuTex, uint32_t width, uint32_t height) override; @@ -88,7 +89,7 @@ rcp<BindGroup> mtlMakeBindGroup(const BindGroupDesc& desc); std::unique_ptr<RenderPass> mtlBeginRenderPass(const RenderPassDesc& desc, std::string* outError); - rcp<TextureView> mtlWrapCanvasTexture(gpu::RenderCanvas* canvas); + rcp<TextureView> mtlWrapTarget(gpu::RenderTarget* target); id<MTLDevice> m_mtlDevice = nil; id<MTLCommandQueue> m_mtlQueue = nil;
diff --git a/renderer/include/rive/renderer/ore/ore_context_vulkan.hpp b/renderer/include/rive/renderer/ore/ore_context_vulkan.hpp index bb207f4..48518db 100644 --- a/renderer/include/rive/renderer/ore/ore_context_vulkan.hpp +++ b/renderer/include/rive/renderer/ore/ore_context_vulkan.hpp
@@ -5,6 +5,7 @@ #pragma once #include "rive/renderer/ore/ore_context.hpp" +#include "rive/renderer/vulkan/render_target_vulkan.hpp" #include "rive/renderer/vulkan/vulkan_context.hpp" #include <functional> #include <utility> @@ -14,6 +15,23 @@ { class TextureVulkan; +// What a host exposes of a Vulkan target to scripts, hidden unless its format +// is one Ore reports. +inline Context::TargetDesc targetDescFor(const gpu::RenderTargetVulkan& target) +{ + switch (target.framebufferFormat()) + { + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_B8G8R8A8_UNORM: + return Context::TargetDesc::color8(target.width(), + target.height(), + target.framebufferFormat() == + VK_FORMAT_B8G8R8A8_UNORM); + default: + return {}; + } +} + // Refcounted VkDescriptorPool. Dies in one shot when its last ref // (ContextVulkan or any BindGroupVulkan) is released. class DescriptorPoolGeneration : public RefCnt<DescriptorPoolGeneration> @@ -109,6 +127,7 @@ void waitForGPU() override; rcp<TextureView> wrapCanvasTexture(gpu::RenderCanvas* canvas) override; + rcp<TextureView> wrapRenderTarget(gpu::RenderTarget*) override; rcp<TextureView> wrapRiveTexture(gpu::Texture* gpuTex, uint32_t width, uint32_t height) override; @@ -133,6 +152,11 @@ Context(vk), m_vk(vk) {} + rcp<TextureView> wrapTargetImage(gpu::RenderTarget*, + uint32_t width, + uint32_t height, + VkImageLayout layout); + const rcp<rive::gpu::VulkanContext> m_vk; VkQueue m_vkQueue = VK_NULL_HANDLE;
diff --git a/renderer/include/rive/renderer/ore/ore_context_wgpu.hpp b/renderer/include/rive/renderer/ore/ore_context_wgpu.hpp index c63a539..a37b19d 100644 --- a/renderer/include/rive/renderer/ore/ore_context_wgpu.hpp +++ b/renderer/include/rive/renderer/ore/ore_context_wgpu.hpp
@@ -45,6 +45,7 @@ void waitForGPU() override; rcp<TextureView> wrapCanvasTexture(gpu::RenderCanvas* canvas) override; + rcp<TextureView> wrapRenderTarget(gpu::RenderTarget* target) override; rcp<TextureView> wrapRiveTexture(gpu::Texture* gpuTex, uint32_t width, uint32_t height) override;
diff --git a/renderer/path_fiddle/fiddle_context_d3d.cpp b/renderer/path_fiddle/fiddle_context_d3d.cpp index e83803d..f915aab 100644 --- a/renderer/path_fiddle/fiddle_context_d3d.cpp +++ b/renderer/path_fiddle/fiddle_context_d3d.cpp
@@ -111,6 +111,25 @@ void toggleZoomWindow() override {} + void bindTargetTexture() + { + if (m_renderTarget->targetTexture() != nullptr) + { + return; + } + if (m_isHeadless) + { + m_renderTarget->setTargetTexture(m_headlessDrawTexture); + return; + } + ComPtr<ID3D11Texture2D> backbuffer; + VERIFY_OK(m_swapchain->GetBuffer( + 0, + __uuidof(ID3D11Texture2D), + reinterpret_cast<void**>(backbuffer.ReleaseAndGetAddressOf()))); + m_renderTarget->setTargetTexture(backbuffer); + } + std::unique_ptr<Renderer> makeRenderer(int width, int height) override { return std::make_unique<RiveRenderer>(m_renderContext.get()); @@ -122,27 +141,18 @@ m_renderContext->beginFrame(frameDescriptor); } + // Ore may draw into the target before Rive's first flush. + void beginOreFrame(rive::ore::Context* oreContext) override + { + bindTargetTexture(); + FiddleContext::beginOreFrame(oreContext); + } + void flushPLSContext(RenderTarget* offscreenRenderTarget) final { RIVE_PROF_SCOPE_L(0) - if (m_renderTarget->targetTexture() == nullptr) - { - if (m_isHeadless) - { - m_renderTarget->setTargetTexture(m_headlessDrawTexture); - } - else - { - ComPtr<ID3D11Texture2D> backbuffer; - VERIFY_OK(m_swapchain->GetBuffer( - 0, - __uuidof(ID3D11Texture2D), - reinterpret_cast<void**>( - backbuffer.ReleaseAndGetAddressOf()))); - m_renderTarget->setTargetTexture(backbuffer); - } - } + bindTargetTexture(); m_renderContext->flush({ .renderTarget = offscreenRenderTarget != nullptr ? offscreenRenderTarget
diff --git a/renderer/path_fiddle/fiddle_context_d3d12.cpp b/renderer/path_fiddle/fiddle_context_d3d12.cpp index fbd6a42..c6a1f1b 100644 --- a/renderer/path_fiddle/fiddle_context_d3d12.cpp +++ b/renderer/path_fiddle/fiddle_context_d3d12.cpp
@@ -342,15 +342,18 @@ return m_renderContext->static_impl_cast<RenderContextD3D12Impl>(); } + // Asked before this frame's flush refreshes m_frameIndex, which still + // names the buffer just presented. rive::gpu::RenderTarget* renderTargetOrNull() override { - return m_renderTargets[m_frameIndex].get(); + return m_renderTargets[getFrameIndex()].get(); } void* getCommandBuffer() override { return m_commandList.Get(); } void beginOreFrame(rive::ore::Context* oreContext) override { + m_frameIndex = getFrameIndex(); beginFrame(); oreContext->beginFrame({.externalCommandBuffer = m_commandList.Get(), .safeFrameNumber = m_safeFrame,
diff --git a/renderer/path_fiddle/fiddle_context_metal.mm b/renderer/path_fiddle/fiddle_context_metal.mm index 5f98dce..4e533c3 100644 --- a/renderer/path_fiddle/fiddle_context_metal.mm +++ b/renderer/path_fiddle/fiddle_context_metal.mm
@@ -35,6 +35,9 @@ } m_renderContext = RenderContextMetalImpl::MakeContext(m_gpu, metalOptions); + // Ore commits on this queue so its passes order ahead of our flush. + m_renderContext->static_impl_cast<RenderContextMetalImpl>() + ->setCommandQueue(m_queue); printf("==== MTLDevice: %s (%s) ====\n", m_gpu.name.UTF8String, // MTLDebugDevice when API validation is on, the real driver @@ -99,22 +102,15 @@ void begin(const RenderContext::FrameDescriptor& frameDescriptor) override { m_renderContext->beginFrame(frameDescriptor); + // Ore replays into the target before the first flush. + acquireDrawable(); } void flushPLSContext(RenderTarget* offscreenRenderTarget) final { @autoreleasepool { - if (m_currentFrameSurface == nil) - { - m_currentFrameSurface = [m_swapchain nextDrawable]; - assert(m_currentFrameSurface.texture.width == - m_renderTarget->width()); - assert(m_currentFrameSurface.texture.height == - m_renderTarget->height()); - m_renderTarget->setTargetTexture(m_currentFrameSurface.texture); - } - + acquireDrawable(); id<MTLCommandBuffer> flushCommandBuffer = [m_queue commandBuffer]; m_renderContext->flush({ .renderTarget = offscreenRenderTarget != nullptr @@ -198,6 +194,19 @@ } private: + void acquireDrawable() + { + if (m_currentFrameSurface == nil) + { + m_currentFrameSurface = [m_swapchain nextDrawable]; + assert(m_currentFrameSurface.texture.width == + m_renderTarget->width()); + assert(m_currentFrameSurface.texture.height == + m_renderTarget->height()); + m_renderTarget->setTargetTexture(m_currentFrameSurface.texture); + } + } + const FiddleContextOptions m_fiddleOptions; id<MTLDevice> m_gpu = MTLCreateSystemDefaultDevice(); id<MTLCommandQueue> m_queue = [m_gpu newCommandQueue];
diff --git a/renderer/src/d3d12/render_context_d3d12_impl.cpp b/renderer/src/d3d12/render_context_d3d12_impl.cpp index fab8212..113f0bb 100644 --- a/renderer/src/d3d12/render_context_d3d12_impl.cpp +++ b/renderer/src/d3d12/render_context_d3d12_impl.cpp
@@ -1596,9 +1596,15 @@ RIVE_PROF_GPUNAME_L(1, "clearPLSTextures"); if (desc.fixedFunctionColorOutput) { - m_resourceManager->transition(cmdList, - targetTexture, - D3D12_RESOURCE_STATE_RENDER_TARGET); + // Ore leaves a host target it drew into as a render target. + if (targetTexture->lastState() != + D3D12_RESOURCE_STATE_RENDER_TARGET) + { + m_resourceManager->transition( + cmdList, + targetTexture, + D3D12_RESOURCE_STATE_RENDER_TARGET); + } auto rtvHandle = m_rtvHeap->cpuHandleForIndex(TARGET_RTV_HEAP_OFFSET); @@ -1724,9 +1730,12 @@ if (renderPassHasCoalescedResolveAndTransfer) { - m_resourceManager->transition(cmdList, - targetTexture, - D3D12_RESOURCE_STATE_RENDER_TARGET); + if (targetTexture->lastState() != D3D12_RESOURCE_STATE_RENDER_TARGET) + { + m_resourceManager->transition(cmdList, + targetTexture, + D3D12_RESOURCE_STATE_RENDER_TARGET); + } auto rtvHandle = m_rtvHeap->cpuHandleForIndex(TARGET_RTV_HEAP_OFFSET); cmdList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
diff --git a/renderer/src/deferred_host.cpp b/renderer/src/deferred_host.cpp index 80f41e3..ed41ec8 100644 --- a/renderer/src/deferred_host.cpp +++ b/renderer/src/deferred_host.cpp
@@ -57,7 +57,7 @@ } if (m_screen == nullptr) { - m_screen = beginScreen(target, m_clear, m_color); + m_screen = beginScreen(target, m_clear && !m_targetPreserved, m_color); } return m_screen; } @@ -130,10 +130,13 @@ m_activeCanvas = nullptr; } -void DeferredInlineHost::beginRecord(bool clear, uint32_t color) +void DeferredInlineHost::beginRecord(bool clear, + uint32_t color, + const ore::Context::TargetDesc& target) { m_doClear = clear; m_color = color; + m_session->oreContext().setTarget(target); m_session->recordOreReplayMarker(); }
diff --git a/renderer/src/ore/d3d11/ore_context_d3d11.cpp b/renderer/src/ore/d3d11/ore_context_d3d11.cpp index 8481c42..9d753e5 100644 --- a/renderer/src/ore/d3d11/ore_context_d3d11.cpp +++ b/renderer/src/ore/d3d11/ore_context_d3d11.cpp
@@ -1347,17 +1347,20 @@ } // ============================================================================ -// d3d11WrapCanvasTexture +// d3d11WrapTarget // ============================================================================ -rcp<TextureView> ContextD3D11::d3d11WrapCanvasTexture(gpu::RenderCanvas* canvas) +rcp<TextureView> ContextD3D11::d3d11WrapTarget(gpu::RenderTarget* target, + bool canvas) { - assert(canvas != nullptr); + assert(target != nullptr); - auto* d3dTarget = - static_cast<gpu::RenderTargetD3D*>(canvas->renderTarget()); + auto* d3dTarget = static_cast<gpu::RenderTargetD3D*>(target); ID3D11Texture2D* d3dTex = d3dTarget->targetTexture(); - assert(d3dTex != nullptr); + if (d3dTex == nullptr) + { + return nullptr; + } D3D11_TEXTURE2D_DESC d3dDesc{}; d3dTex->GetDesc(&d3dDesc); @@ -1379,8 +1382,8 @@ } TextureDesc texDesc{}; - texDesc.width = canvas->width(); - texDesc.height = canvas->height(); + texDesc.width = target->width(); + texDesc.height = target->height(); texDesc.format = oreFormat; texDesc.type = TextureType::texture2D; texDesc.renderTarget = true; @@ -1404,6 +1407,11 @@ // Borrow the existing RTV from the D3D render target (AddRefs via ComPtr). view->m_d3dRTV = d3dTarget->targetRTV(); + if (!canvas) + { + return view; + } + // SRV so a later pass can sample the canvas after rendering into it; // without it a bind group samples an unbound view and reads black. D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; @@ -1603,7 +1611,13 @@ rcp<TextureView> ContextD3D11::wrapCanvasTexture(gpu::RenderCanvas* canvas) { - return d3d11WrapCanvasTexture(canvas); + assert(canvas != nullptr); + return d3d11WrapTarget(canvas->renderTarget(), true); +} + +rcp<TextureView> ContextD3D11::wrapRenderTarget(gpu::RenderTarget* target) +{ + return target != nullptr ? d3d11WrapTarget(target, false) : nullptr; } rcp<TextureView> ContextD3D11::wrapRiveTexture(gpu::Texture* gpuTex,
diff --git a/renderer/src/ore/d3d12/ore_context_d3d12.cpp b/renderer/src/ore/d3d12/ore_context_d3d12.cpp index 5588a45..f46006f 100644 --- a/renderer/src/ore/d3d12/ore_context_d3d12.cpp +++ b/renderer/src/ore/d3d12/ore_context_d3d12.cpp
@@ -1694,10 +1694,10 @@ // Record resource + final state for finish() to transition back. pass->m_d3dColorResources[i] = tex->m_d3dTexture.Get(); pass->m_d3dColorTextures[i] = tex; - // External (canvas) textures go back to COMMON; ORE-owned go to + // External textures go back to their owner's state; ORE-owned go to // PIXEL_SHADER_RESOURCE. const D3D12_RESOURCE_STATES colourFinalState = - tex->m_d3dIsExternal ? D3D12_RESOURCE_STATE_COMMON + tex->m_d3dIsExternal ? tex->m_d3dExternalState : D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; pass->m_d3dColorFinalStates[i] = colourFinalState; pass->m_d3dColorCount++; @@ -1725,7 +1725,7 @@ r.resolvePriorState = resolveTex->m_d3dCurrentState; r.resolveFinalState = resolveTex->m_d3dIsExternal - ? D3D12_RESOURCE_STATE_COMMON + ? resolveTex->m_d3dExternalState : D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; } } @@ -1844,18 +1844,21 @@ } // ============================================================================ -// d3d12WrapCanvasTexture +// d3d12WrapTarget // ============================================================================ -rcp<TextureView> ContextD3D12::d3d12WrapCanvasTexture(gpu::RenderCanvas* canvas) +rcp<TextureView> ContextD3D12::d3d12WrapTarget(gpu::RenderTarget* target, + bool canvas) { #if defined(ORE_BACKEND_D3D12) - assert(canvas != nullptr); + assert(target != nullptr); - auto* d3dTarget = - static_cast<gpu::RenderTargetD3D12*>(canvas->renderTarget()); + auto* d3dTarget = static_cast<gpu::RenderTargetD3D12*>(target); gpu::D3D12Texture* d3dTex = d3dTarget->targetTexture(); - assert(d3dTex != nullptr); + if (d3dTex == nullptr) + { + return nullptr; + } DXGI_FORMAT dxgiFmt = d3dTex->format(); @@ -1877,18 +1880,34 @@ } TextureDesc texDesc{}; - texDesc.width = canvas->width(); - texDesc.height = canvas->height(); + texDesc.width = target->width(); + texDesc.height = target->height(); texDesc.format = oreFormat; texDesc.type = TextureType::texture2D; texDesc.renderTarget = true; texDesc.numMipmaps = 1; texDesc.sampleCount = 1; - auto texture = rcp<TextureD3D12>(new TextureD3D12(m_manager, texDesc)); + // A host target frees on its last release, not in purgatory, since its + // back buffer reference would fail the host's ResizeBuffers. + auto manager = canvas ? m_manager : nullptr; + auto texture = rcp<TextureD3D12>(new TextureD3D12(manager, texDesc)); texture->m_d3dTexture = d3dTex->resource(); // Borrow — RenderCanvas owns it. - texture->m_d3dCurrentState = D3D12_RESOURCE_STATE_COMMON; + // Rive tracks a host target's state and draws into it after us, so it + // moves to and stays a render target through Rive's own tracking. + if (!canvas && m_d3dCmdList != nullptr && + d3dTex->lastState() != D3D12_RESOURCE_STATE_RENDER_TARGET) + { + static_cast<gpu::D3D12ResourceManager*>(d3dTex->manager()) + ->transition(m_d3dCmdList, + d3dTex, + D3D12_RESOURCE_STATE_RENDER_TARGET); + } + const D3D12_RESOURCE_STATES ownerState = + canvas ? D3D12_RESOURCE_STATE_COMMON : d3dTex->lastState(); + texture->m_d3dCurrentState = ownerState; + texture->m_d3dExternalState = ownerState; texture->m_d3dIsExternal = true; texture->m_d3dDevice = m_d3dDevice.Get(); texture->m_d3dOreContext = this; @@ -1902,12 +1921,12 @@ viewDesc.layerCount = 1; auto view = rcp<TextureViewD3D12>( - new TextureViewD3D12(m_manager, std::move(texture), viewDesc)); + new TextureViewD3D12(manager, std::move(texture), viewDesc)); // SRV so a later pass can sample the canvas after rendering into it; // without it a bind group copies a null descriptor and the debug layer // faults. - if (m_d3dCpuSrvAllocated < 1024) + if (canvas && m_d3dCpuSrvAllocated < 1024) { D3D12_CPU_DESCRIPTOR_HANDLE srvHandle = m_d3dCpuSrvHeap->GetCPUDescriptorHandleForHeapStart(); @@ -1926,10 +1945,24 @@ view->m_d3dSrvHandle = srvHandle; } - // Create the RTV in our CPU RTV heap. + // Create the RTV in our CPU RTV heap. Passes copy RTVs as they record, + // so last frame's target slot is free to rewrite. + UINT rtvIndex; + if (canvas) + { + rtvIndex = m_d3dCpuRtvAllocated++; + } + else + { + if (m_d3dTargetRtvIndex == UINT_MAX) + { + m_d3dTargetRtvIndex = m_d3dCpuRtvAllocated++; + } + rtvIndex = m_d3dTargetRtvIndex; + } D3D12_CPU_DESCRIPTOR_HANDLE handle = m_d3dCpuRtvHeap->GetCPUDescriptorHandleForHeapStart(); - handle.ptr += (SIZE_T)m_d3dCpuRtvAllocated++ * m_d3dRtvDescSize; + handle.ptr += (SIZE_T)rtvIndex * m_d3dRtvDescSize; D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {}; rtvDesc.Format = dxgiFmt; @@ -1941,6 +1974,7 @@ return view; #else + (void)target; (void)canvas; return {}; #endif @@ -2110,7 +2144,13 @@ rcp<TextureView> ContextD3D12::wrapCanvasTexture(gpu::RenderCanvas* canvas) { - return d3d12WrapCanvasTexture(canvas); + assert(canvas != nullptr); + return d3d12WrapTarget(canvas->renderTarget(), true); +} + +rcp<TextureView> ContextD3D12::wrapRenderTarget(gpu::RenderTarget* target) +{ + return target != nullptr ? d3d12WrapTarget(target, false) : nullptr; } rcp<TextureView> ContextD3D12::wrapRiveTexture(gpu::Texture* gpuTex,
diff --git a/renderer/src/ore/d3d12/ore_render_pass_d3d12.cpp b/renderer/src/ore/d3d12/ore_render_pass_d3d12.cpp index 2d9d484..f2b1412 100644 --- a/renderer/src/ore/d3d12/ore_render_pass_d3d12.cpp +++ b/renderer/src/ore/d3d12/ore_render_pass_d3d12.cpp
@@ -484,14 +484,17 @@ } } - D3D12_RESOURCE_BARRIER barrier = {}; - barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; - barrier.Transition.pResource = m_d3dColorResources[i]; - barrier.Transition.StateBefore = stateBefore; - barrier.Transition.StateAfter = m_d3dColorFinalStates[i]; - barrier.Transition.Subresource = - D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; - m_d3dCmdList->ResourceBarrier(1, &barrier); + if (stateBefore != m_d3dColorFinalStates[i]) + { + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = m_d3dColorResources[i]; + barrier.Transition.StateBefore = stateBefore; + barrier.Transition.StateAfter = m_d3dColorFinalStates[i]; + barrier.Transition.Subresource = + D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + m_d3dCmdList->ResourceBarrier(1, &barrier); + } if (m_d3dColorTextures[i] != nullptr) m_d3dColorTextures[i]->m_d3dCurrentState = m_d3dColorFinalStates[i];
diff --git a/renderer/src/ore/d3d12/ore_texture_d3d12.hpp b/renderer/src/ore/d3d12/ore_texture_d3d12.hpp index a1f1517..3df5d65 100644 --- a/renderer/src/ore/d3d12/ore_texture_d3d12.hpp +++ b/renderer/src/ore/d3d12/ore_texture_d3d12.hpp
@@ -23,7 +23,9 @@ friend class RenderPassD3D12; Microsoft::WRL::ComPtr<ID3D12Resource> m_d3dTexture; D3D12_RESOURCE_STATES m_d3dCurrentState = D3D12_RESOURCE_STATE_COMMON; - bool m_d3dIsExternal = false; // true for wrapCanvasTexture. + bool m_d3dIsExternal = false; // true for wrapCanvasTexture. + // The state its owner expects an external texture back in after a pass. + D3D12_RESOURCE_STATES m_d3dExternalState = D3D12_RESOURCE_STATE_COMMON; ID3D12Device* m_d3dDevice = nullptr; // Weak ref. // ContextD3D12. Weak ref. actually used for barriers ContextD3D12* m_d3dOreContext = nullptr;
diff --git a/renderer/src/ore/gl/ore_context_gl.cpp b/renderer/src/ore/gl/ore_context_gl.cpp index dc7cb96..c22155e 100644 --- a/renderer/src/ore/gl/ore_context_gl.cpp +++ b/renderer/src/ore/gl/ore_context_gl.cpp
@@ -1338,17 +1338,16 @@ // wrapCanvasTexture // ============================================================================ -rcp<TextureView> ContextGL::wrapCanvasTexture(gpu::RenderCanvas* canvas) +rcp<TextureView> ContextGL::glWrapTarget(gpu::TextureRenderTargetGL* target) { - assert(canvas != nullptr); - - auto* glTarget = - static_cast<gpu::TextureRenderTargetGL*>(canvas->renderTarget()); - GLuint texID = glTarget->externalTextureID(); - assert(texID != 0); + GLuint texID = target->externalTextureID(); + if (texID == 0) + { + return nullptr; + } TextureDesc texDesc{}; - texDesc.width = canvas->width(); - texDesc.height = canvas->height(); + texDesc.width = target->width(); + texDesc.height = target->height(); texDesc.format = TextureFormat::rgba8unorm; texDesc.type = TextureType::texture2D; texDesc.renderTarget = true; @@ -1356,7 +1355,7 @@ texDesc.sampleCount = 1; auto texture = rcp<TextureGL>(new TextureGL(texDesc)); - texture->m_glTexture = texID; // Borrow — RenderCanvas owns it. + texture->m_glTexture = texID; // Borrow — the host owns it. texture->m_glTarget = GL_TEXTURE_2D; texture->m_glOwnsTexture = false; @@ -1371,6 +1370,23 @@ return rcp<TextureViewGL>(new TextureViewGL(std::move(texture), viewDesc)); } +rcp<TextureView> ContextGL::wrapCanvasTexture(gpu::RenderCanvas* canvas) +{ + assert(canvas != nullptr); + return glWrapTarget( + static_cast<gpu::TextureRenderTargetGL*>(canvas->renderTarget())); +} + +rcp<TextureView> ContextGL::wrapRenderTarget(gpu::RenderTarget* target) +{ + // Our GLSL flips Y at bake time, so passes land upright only in top down + // textures. Framebuffers and bottom up textures wait for an offscreen copy. + auto* glTarget = lite_rtti_cast<gpu::TextureRenderTargetGL*>( + static_cast<gpu::RenderTargetGL*>(target)); + return glTarget != nullptr && !glTarget->bottomUp() ? glWrapTarget(glTarget) + : nullptr; +} + rcp<TextureView> ContextGL::wrapRiveTexture(gpu::Texture* gpuTex, uint32_t w, uint32_t h)
diff --git a/renderer/src/ore/metal/ore_context_metal.mm b/renderer/src/ore/metal/ore_context_metal.mm index a9bbc70..e65e1ee 100644 --- a/renderer/src/ore/metal/ore_context_metal.mm +++ b/renderer/src/ore/metal/ore_context_metal.mm
@@ -1090,18 +1090,19 @@ return pass; } -inline rcp<TextureView> ContextMetal::mtlWrapCanvasTexture( - gpu::RenderCanvas* canvas) +inline rcp<TextureView> ContextMetal::mtlWrapTarget(gpu::RenderTarget* target) { - assert(canvas != nullptr); + assert(target != nullptr); - auto* metalTarget = - static_cast<gpu::RenderTargetMetal*>(canvas->renderTarget()); - id<MTLTexture> mtlTexture = metalTarget->targetTexture(); - assert(mtlTexture != nil); + id<MTLTexture> mtlTexture = + static_cast<gpu::RenderTargetMetal*>(target)->targetTexture(); + if (mtlTexture == nil) + { + return nullptr; + } - uint32_t w = canvas->width(); - uint32_t h = canvas->height(); + uint32_t w = target->width(); + uint32_t h = target->height(); TextureDesc texDesc{}; texDesc.width = w; @@ -1311,7 +1312,13 @@ rcp<TextureView> ContextMetal::wrapCanvasTexture(gpu::RenderCanvas* canvas) { - return mtlWrapCanvasTexture(canvas); + assert(canvas != nullptr); + return mtlWrapTarget(canvas->renderTarget()); +} + +rcp<TextureView> ContextMetal::wrapRenderTarget(gpu::RenderTarget* target) +{ + return target != nullptr ? mtlWrapTarget(target) : nullptr; } rcp<TextureView> ContextMetal::wrapRiveTexture(gpu::Texture* gpuTex,
diff --git a/renderer/src/ore/vulkan/ore_context_vulkan.cpp b/renderer/src/ore/vulkan/ore_context_vulkan.cpp index 93c9ed4..7d40f29 100644 --- a/renderer/src/ore/vulkan/ore_context_vulkan.cpp +++ b/renderer/src/ore/vulkan/ore_context_vulkan.cpp
@@ -1649,12 +1649,57 @@ // wrapCanvasTexture // ============================================================================ +static TextureFormat oreFormatFor(VkFormat vkFmt) +{ + switch (vkFmt) + { + case VK_FORMAT_B8G8R8A8_UNORM: + return TextureFormat::bgra8unorm; + case VK_FORMAT_R16G16B16A16_SFLOAT: + return TextureFormat::rgba16float; + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: + return TextureFormat::rgb10a2unorm; + default: + return TextureFormat::rgba8unorm; + } +} + rcp<TextureView> ContextVulkan::wrapCanvasTexture(gpu::RenderCanvas* canvas) { assert(canvas != nullptr); + return wrapTargetImage(canvas->renderTarget(), + canvas->width(), + canvas->height(), + VK_IMAGE_LAYOUT_UNDEFINED); +} - auto* vkTarget = - static_cast<gpu::RenderTargetVulkan*>(canvas->renderTarget()); +// A host target may change image every frame, so its view starts in the +// layout Rive's tracker left it in and a load keeps what is there. +rcp<TextureView> ContextVulkan::wrapRenderTarget(gpu::RenderTarget* target) +{ + auto* vkTarget = static_cast<gpu::RenderTargetVulkan*>(target); + if (vkTarget == nullptr) + { + return nullptr; + } + rcp<TextureView> view = + wrapTargetImage(target, + target->width(), + target->height(), + vkTarget->targetLastAccess().layout); + if (view != nullptr) + { + static_cast<TextureVulkan*>(view->texture())->m_vkSampleable = false; + } + return view; +} + +rcp<TextureView> ContextVulkan::wrapTargetImage(gpu::RenderTarget* target, + uint32_t width, + uint32_t height, + VkImageLayout layout) +{ + auto* vkTarget = static_cast<gpu::RenderTargetVulkan*>(target); VkImage image = vkTarget->targetImage(); VkImageView imageView = vkTarget->targetImageView(); @@ -1664,28 +1709,10 @@ // Derive the ore format from the actual Vulkan surface format so any MSAA // texture created from this descriptor matches the resolve target exactly // (Vulkan requires identical formats for MSAA resolve). - auto vkFmt = vkTarget->framebufferFormat(); - TextureFormat oreFormat; - switch (vkFmt) - { - case VK_FORMAT_B8G8R8A8_UNORM: - oreFormat = TextureFormat::bgra8unorm; - break; - case VK_FORMAT_R16G16B16A16_SFLOAT: - oreFormat = TextureFormat::rgba16float; - break; - case VK_FORMAT_A2B10G10R10_UNORM_PACK32: - oreFormat = TextureFormat::rgb10a2unorm; - break; - default: - oreFormat = TextureFormat::rgba8unorm; - break; - } - TextureDesc texDesc{}; - texDesc.width = canvas->width(); - texDesc.height = canvas->height(); - texDesc.format = oreFormat; + texDesc.width = width; + texDesc.height = height; + texDesc.format = oreFormatFor(vkTarget->framebufferFormat()); texDesc.type = TextureType::texture2D; texDesc.renderTarget = true; texDesc.numMipmaps = 1; @@ -1697,7 +1724,7 @@ texture->m_vk = m_vk; // Mark as not VMA-owned so the destructor skips the VMA free. texture->m_vmaAllocation = VK_NULL_HANDLE; - texture->m_vkLayout = VK_IMAGE_LAYOUT_UNDEFINED; + texture->m_vkLayout = layout; // Rive's tracker still says undefined until something draws the canvas if (vkTarget->targetLastAccess().layout != VK_IMAGE_LAYOUT_UNDEFINED) {
diff --git a/renderer/src/ore/vulkan/ore_render_pass_vulkan.cpp b/renderer/src/ore/vulkan/ore_render_pass_vulkan.cpp index d8c1d0f..6644625 100644 --- a/renderer/src/ore/vulkan/ore_render_pass_vulkan.cpp +++ b/renderer/src/ore/vulkan/ore_render_pass_vulkan.cpp
@@ -213,6 +213,27 @@ m_vkContext->m_vk->CmdEndRenderPass(m_vkCmdBuf); + // A target that is never sampled stays in the layout the pass left it in, + // and Rive's tracker hears the write so its own barrier starts from there. + auto keepAsAttachment = [](Texture* texture, + gpu::RenderTargetVulkan* renderTarget) { + auto* tex = lite_rtti_cast<TextureVulkan*>(texture); + if (tex == nullptr || tex->m_vkSampleable) + { + return false; + } + tex->m_vkLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + if (renderTarget != nullptr) + { + renderTarget->updateLastAccess({ + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }); + } + return true; + }; + // Transition color attachments COLOR_ATTACHMENT_OPTIMAL → // SHADER_READ_ONLY_OPTIMAL so callers (e.g. Rive drawImage) can sample. // @@ -233,6 +254,9 @@ { if (m_vkColorImages[i] == VK_NULL_HANDLE) continue; + if (keepAsAttachment(m_vkColorTextures[i].get(), + m_vkColorRenderTargets[i])) + continue; VkImageMemoryBarrier barrier{}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; @@ -275,7 +299,8 @@ // attachment in COLOR_ATTACHMENT_OPTIMAL. for (auto& resolve : m_vkResolveTargets) { - if (resolve.image == VK_NULL_HANDLE) + if (resolve.image == VK_NULL_HANDLE || + keepAsAttachment(resolve.texture.get(), resolve.renderTarget)) { continue; }
diff --git a/renderer/src/ore/vulkan/ore_texture_vulkan.hpp b/renderer/src/ore/vulkan/ore_texture_vulkan.hpp index fb7afa8..d53aa1a 100644 --- a/renderer/src/ore/vulkan/ore_texture_vulkan.hpp +++ b/renderer/src/ore/vulkan/ore_texture_vulkan.hpp
@@ -27,6 +27,9 @@ VkImage m_vkImage = VK_NULL_HANDLE; VmaAllocation m_vmaAllocation = VK_NULL_HANDLE; VkImageLayout m_vkLayout = VK_IMAGE_LAYOUT_UNDEFINED; + // A host's render target may lack sampled usage, so passes leave it an + // attachment. + bool m_vkSampleable = true; // One flag per mip and layer, set once anything gives that subresource // contents, pending uploads included. std::vector<bool> m_vkWritten;
diff --git a/renderer/src/ore/wgpu/ore_context_wgpu.cpp b/renderer/src/ore/wgpu/ore_context_wgpu.cpp index c889314..bab08d5 100644 --- a/renderer/src/ore/wgpu/ore_context_wgpu.cpp +++ b/renderer/src/ore/wgpu/ore_context_wgpu.cpp
@@ -1155,9 +1155,16 @@ rcp<TextureView> ContextWGPU::wrapCanvasTexture(gpu::RenderCanvas* canvas) { assert(canvas != nullptr); + return wrapRenderTarget(canvas->renderTarget()); +} - auto* wgpuTarget = - static_cast<gpu::RenderTargetWebGPU*>(canvas->renderTarget()); +rcp<TextureView> ContextWGPU::wrapRenderTarget(gpu::RenderTarget* target) +{ + auto* wgpuTarget = static_cast<gpu::RenderTargetWebGPU*>(target); + if (wgpuTarget == nullptr || !wgpuTarget->targetTextureView()) + { + return nullptr; + } // Derive the ore format from the actual WebGPU surface format so any MSAA // texture created from this descriptor matches the resolve target exactly. @@ -1180,8 +1187,8 @@ } TextureDesc texDesc{}; - texDesc.width = canvas->width(); - texDesc.height = canvas->height(); + texDesc.width = target->width(); + texDesc.height = target->height(); texDesc.format = oreFormat; texDesc.type = TextureType::texture2D; texDesc.renderTarget = true;
diff --git a/src/lua/lua_atoms.cpp b/src/lua/lua_atoms.cpp index c7d5e81..5658e86 100644 --- a/src/lua/lua_atoms.cpp +++ b/src/lua/lua_atoms.cpp
@@ -279,6 +279,8 @@ {"features", (int16_t)LuaAtoms::features}, {"shader", (int16_t)LuaAtoms::shader}, {"format", (int16_t)LuaAtoms::format}, + {"gpuTarget", (int16_t)LuaAtoms::gpuTarget}, + {"sampleCount", (int16_t)LuaAtoms::sampleCount}, {"andThen", (int16_t)LuaAtoms::andThen}, {"catch", (int16_t)LuaAtoms::catch_}, {"finally", (int16_t)LuaAtoms::finally_},
diff --git a/src/lua/lua_scripted_context.cpp b/src/lua/lua_scripted_context.cpp index 34473c5..17ec613 100644 --- a/src/lua/lua_scripted_context.cpp +++ b/src/lua/lua_scripted_context.cpp
@@ -658,6 +658,33 @@ return 1; #endif } + case (int)LuaAtoms::gpuTarget: + { +#if defined(RIVE_CANVAS) && defined(RIVE_ORE) + auto* oreCtx = static_cast<ore::Context*>( + static_cast<ScriptingContext*>(lua_getthreaddata(L)) + ->oreContext()); + // Available whenever a host could expose its target, so + // init can hold it before the first frame declares one. + if (oreCtx != nullptr && oreCtx->isRecording()) + { + // The target holds no state, so one userdata serves + // every call. + static const char* kTargetKey = "rive.gpuTarget"; + lua_rawgetfield(L, LUA_REGISTRYINDEX, kTargetKey); + if (lua_isnil(L, -1)) + { + lua_pop(L, 1); + lua_newrive<ScriptedGPUTarget>(L); + lua_pushvalue(L, -1); + lua_rawsetfield(L, LUA_REGISTRYINDEX, kTargetKey); + } + return 1; + } +#endif + lua_pushnil(L); + return 1; + } case (int)LuaAtoms::features: return lua_push_gpu_features(L);
diff --git a/src/lua/renderer/lua_gpu.cpp b/src/lua/renderer/lua_gpu.cpp index 5b9ff77..e999164 100644 --- a/src/lua/renderer/lua_gpu.cpp +++ b/src/lua/renderer/lua_gpu.cpp
@@ -2166,14 +2166,17 @@ // GPURenderPass // ============================================================================ -static void validate_render_pass(lua_State* L, ScriptedGPURenderPass* self) +// False for a pass that drops its commands, like one begun on a frame with no +// target. +static bool validate_render_pass(lua_State* L, ScriptedGPURenderPass* self) { // The pass finishes without the wrapper when the pass it was begun // inside finishes first, or when the script call that began it returns. - if (self->m_finished || !self->pass || self->pass->isFinished()) + if (self->m_finished || (self->pass && self->pass->isFinished())) { luaL_error(L, "render pass expired: it was already finished"); } + return self->pass != nullptr; } static void validate_pipeline_set(lua_State* L, ScriptedGPURenderPass* self) @@ -2187,7 +2190,10 @@ static int gpurenderpass_setpipeline(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } auto* pipeline = lua_torive<ScriptedGPUPipeline>(L, 2); if (pipeline->sampleCount != self->sampleCount) { @@ -2216,7 +2222,10 @@ static int gpurenderpass_setvertexbuffer(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } // Note: WebGPU / Metal / Vulkan / D3D11 all permit `setVertexBuffer` // before `setPipeline` — vertex-buffer state is layered onto whatever // pipeline is current at draw time. Don't gate on `m_pipelineSet`. @@ -2236,7 +2245,10 @@ static int gpurenderpass_setindexbuffer(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } auto* buffer = lua_torive<ScriptedGPUBuffer>(L, 2); IndexFormat fmt = IndexFormat::uint16; if (lua_isstring(L, 3)) @@ -2252,7 +2264,10 @@ static int gpurenderpass_setbindgroup(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } uint32_t groupIndex = static_cast<uint32_t>(luaL_checkunsigned(L, 2)); if (groupIndex >= ore::kMaxBindGroups) { @@ -2325,7 +2340,10 @@ static int gpurenderpass_setviewport(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } float x = static_cast<float>(luaL_checknumber(L, 2)); float y = static_cast<float>(luaL_checknumber(L, 3)); float w = static_cast<float>(luaL_checknumber(L, 4)); @@ -2337,7 +2355,10 @@ static int gpurenderpass_setscissorrect(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } uint32_t x = static_cast<uint32_t>(luaL_checkunsigned(L, 2)); uint32_t y = static_cast<uint32_t>(luaL_checkunsigned(L, 3)); uint32_t w = static_cast<uint32_t>(luaL_checkunsigned(L, 4)); @@ -2349,7 +2370,10 @@ static int gpurenderpass_setstencilreference(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } uint32_t ref = static_cast<uint32_t>(luaL_checkunsigned(L, 2)); self->pass->setStencilReference(ref); return 0; @@ -2358,7 +2382,10 @@ static int gpurenderpass_setblendcolor(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } float r = static_cast<float>(luaL_checknumber(L, 2)); float g = static_cast<float>(luaL_checknumber(L, 3)); float b = static_cast<float>(luaL_checknumber(L, 4)); @@ -2372,7 +2399,10 @@ static int gpurenderpass_draw(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } validate_pipeline_set(L, self); uint32_t vertexCount = static_cast<uint32_t>(luaL_checkunsigned(L, 2)); uint32_t instanceCount = @@ -2396,7 +2426,10 @@ static int gpurenderpass_drawindexed(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); + if (!validate_render_pass(L, self)) + { + return 0; + } validate_pipeline_set(L, self); uint32_t indexCount = static_cast<uint32_t>(luaL_checkunsigned(L, 2)); uint32_t instanceCount = @@ -2432,8 +2465,10 @@ static int gpurenderpass_finish(lua_State* L) { auto* self = lua_torive<ScriptedGPURenderPass>(L, 1); - validate_render_pass(L, self); - self->pass->finish(); + if (validate_render_pass(L, self)) + { + self->pass->finish(); + } self->m_finished = true; return 0; } @@ -2523,25 +2558,23 @@ return StoreOp::store; } -// canvas:beginRenderPass(desc) — open a render pass against this canvas. +// target:beginRenderPass(desc) — open a render pass against a GPUTarget. // // `desc` is required: at least one color attachment or a depthStencil // attachment must be supplied. Every attachment carries its own view, and // sampleCount is derived from those views (all attachments must share one // sampleCount, matching WebGPU validation). // -// Color attachment `view` is optional: when omitted it defaults to the -// receiving canvas's own colorView. Provide an explicit view for cases +// Color attachment `view` is optional: when omitted it defaults to +// targetView, the receiver's own view. Provide an explicit view for cases // where the target differs (e.g. MSAA: view = msaa:view(), // resolveTarget = canvas:colorView()). -int gpucanvas_beginrenderpass(lua_State* L) +static Context* check_beginrenderpass(lua_State* L, const char* typeName) { - auto* self = lua_torive<ScriptedGPUCanvas>(L, 1); - Context* oreCtx = getOreContext(L); if (oreCtx == nullptr) { - luaL_error(L, "GPUCanvas:beginRenderPass() requires a GPU context"); + luaL_error(L, "%s:beginRenderPass() requires a GPU context", typeName); } auto* scriptingContext = @@ -2551,14 +2584,26 @@ if (scriptingContext == nullptr || !oreCtx->isRecording()) { luaL_error(L, - "GPUCanvas:beginRenderPass() requires the deferred " - "recorder"); + "%s:beginRenderPass() requires the deferred recorder", + typeName); } luaL_checktype(L, 2, LUA_TTABLE); + return oreCtx; +} + +// A host target that is hidden this frame has no view, so a pass that omits +// one drops after validating, as the module lane does. +static int gputarget_beginrenderpass(lua_State* L, + const char* typeName, + ore::TextureView* targetView, + bool hostTarget = false) +{ + Context* oreCtx = check_beginrenderpass(L, typeName); RenderPassDesc passDesc{}; passDesc.colorCount = 0; + bool dropped = false; // Tracks the first attachment we see (any color slot or depth) and // becomes the pass's authoritative sampleCount. Subsequent attachments @@ -2605,10 +2650,14 @@ ore::TextureView* viewPtr = nullptr; if (lua_isnil(L, -1)) { - // Sugar: omitting `view` defaults to the receiving canvas's - // own colorView. Errors only if the canvas is deferred - // (zero-sized — no backing texture). - if (!self->oreColorView) + // Sugar: omitting `view` defaults to the receiver's own + // view. Errors only if a canvas is deferred (zero-sized — no + // backing texture). + if (targetView == nullptr && hostTarget) + { + dropped = true; + } + else if (targetView == nullptr) { luaL_error(L, "beginRenderPass: color[%d].view omitted but " @@ -2617,7 +2666,7 @@ "before drawing, or pass an explicit view.", slot + 1); } - viewPtr = self->oreColorView.get(); + viewPtr = targetView; } else { @@ -2632,9 +2681,13 @@ viewPtr = tv->view.get(); } passDesc.colorAttachments[slot].view = viewPtr; - char colorLabel[32]; - snprintf(colorLabel, sizeof(colorLabel), "color[%d]", slot + 1); - recordSampleCount(viewPtr->texture()->sampleCount(), colorLabel); + if (viewPtr != nullptr) + { + char colorLabel[32]; + snprintf(colorLabel, sizeof(colorLabel), "color[%d]", slot + 1); + recordSampleCount(viewPtr->texture()->sampleCount(), + colorLabel); + } lua_pop(L, 1); // view // resolveTarget (optional — for MSAA). Source view must be @@ -2799,8 +2852,20 @@ "color attachment or a depthStencil attachment"); } + if (dropped) + { + lua_newrive<ScriptedGPURenderPass>(L); + return 1; + } + oreCtx->clearLastError(); + auto pass = ore::cmd::beginRecordedRenderPass(*oreCtx, passDesc); + // A pass refused without an error drops, like one with no target. + if (pass == nullptr && !oreCtx->lastError().empty()) + { + luaL_error(L, "beginRenderPass: %s", oreCtx->lastError().c_str()); + } auto* rp = lua_newrive<ScriptedGPURenderPass>(L); - rp->pass = ore::cmd::beginRecordedRenderPass(*oreCtx, passDesc); + rp->pass = std::move(pass); rp->m_finished = false; rp->sampleCount = passSampleCount < 1 ? 1u : static_cast<uint32_t>(passSampleCount); @@ -2809,6 +2874,97 @@ return 1; } +static int gpucanvas_beginrenderpass(lua_State* L) +{ + auto* self = lua_torive<ScriptedGPUCanvas>(L, 1); + return gputarget_beginrenderpass(L, + ScriptedGPUCanvas::luaName, + self->oreColorView.get()); +} + +static int hosttarget_beginrenderpass(lua_State* L) +{ + lua_torive<ScriptedGPUTarget>(L, 1); + Context* oreCtx = check_beginrenderpass(L, ScriptedGPUTarget::luaName); + // A Lua error longjmps past any rcp still on the stack, so drop ours before + // the pass can raise; the context keeps the view alive. + TextureView* view = oreCtx->targetView().get(); + return gputarget_beginrenderpass(L, ScriptedGPUTarget::luaName, view, true); +} + +// The fields every GPUTarget answers from the view it renders into, or 0 when +// the atom is not one of them. +static int gputarget_pushfield(lua_State* L, + int atom, + const rcp<TextureView>& view) +{ + Texture* texture = view != nullptr ? view->texture() : nullptr; + switch (atom) + { + case (int)LuaAtoms::width: + lua_pushnumber(L, texture != nullptr ? texture->width() : 0); + return 1; + case (int)LuaAtoms::height: + lua_pushnumber(L, texture != nullptr ? texture->height() : 0); + return 1; + case (int)LuaAtoms::format: + // A deferred canvas reports the format makeRenderCanvas allocates. + lua_pushstring(L, + lua_totextureformatstring( + texture != nullptr ? texture->format() + : TextureFormat::rgba8unorm)); + return 1; + case (int)LuaAtoms::sampleCount: + lua_pushnumber(L, texture != nullptr ? texture->sampleCount() : 1); + return 1; + case (int)LuaAtoms::view: + if (view == nullptr) + { + lua_pushnil(L); + return 1; + } + lua_newrive<ScriptedGPUTextureView>(L)->view = view; + return 1; + } + return 0; +} + +static int hosttarget_index(lua_State* L) +{ + int atom; + const char* key = lua_tostringatom(L, 2, &atom); + if (!key) + { + luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING)); + } + lua_torive<ScriptedGPUTarget>(L, 1); + Context* oreCtx = getOreContext(L); + if (int pushed = gputarget_pushfield( + L, + atom, + oreCtx != nullptr ? oreCtx->targetView() : nullptr)) + { + return pushed; + } + luaL_error(L, "'%s' is not a valid index of GPUTarget", key); + return 0; +} + +static int hosttarget_namecall(lua_State* L) +{ + int atom; + const char* str = lua_namecallatom(L, &atom); + if (str != nullptr && atom == (int)LuaAtoms::beginRenderPass) + { + return hosttarget_beginrenderpass(L); + } + luaL_error(L, + "%s is not a valid method of %s", + str, + ScriptedGPUTarget::luaName); + return 0; +} + rcp<gpu::RenderCanvas> rive::allocScriptRenderCanvas(gpu::RenderContext* rc, ScriptingContext* ctx, uint32_t width, @@ -3033,18 +3189,10 @@ self->canvas ? self->canvas->height() : self->pendingHeight); return 1; - case (int)LuaAtoms::format: - // Realized canvas reports its texture format. Deferred canvas - // reports the format makeRenderCanvas always allocates. - if (self->oreColorView && self->oreColorView->texture()) - lua_pushstring(L, - lua_totextureformatstring( - self->oreColorView->texture()->format())); - else - lua_pushstring( - L, - lua_totextureformatstring(TextureFormat::rgba8unorm)); - return 1; + } + if (int pushed = gputarget_pushfield(L, atom, self->oreColorView)) + { + return pushed; } luaL_error(L, "'%s' is not a valid index of GPUCanvas", key); return 0; @@ -3587,6 +3735,18 @@ gpucanvashandle_direct_height); } + // GPUTarget (no public constructor — created via context:gpuTarget()) + { + luaL_register(L, ScriptedGPUTarget::luaName, empty); + lua_register_rive<ScriptedGPUTarget>(L); + lua_pushcfunction(L, hosttarget_namecall, nullptr); + lua_setfield(L, -2, "__namecall"); + lua_pushcfunction(L, hosttarget_index, nullptr); + lua_setfield(L, -2, "__index"); + lua_setreadonly(L, -1, true); + lua_pop(L, 1); + } + // Canvas (no public constructor — created via context:canvas()) { luaL_register(L, ScriptedCanvas::luaName, empty);
diff --git a/src/wasm/idl/bindings.py b/src/wasm/idl/bindings.py index 3851ebf..9d7f45c 100644 --- a/src/wasm/idl/bindings.py +++ b/src/wasm/idl/bindings.py
@@ -663,6 +663,13 @@ u32('height'), mutbuf('uint32_t', 'props', 'propCount'), ], ret='u32'), + # A view over the host render target, which renders under Rive's + # content; props mirror canvas_color_view. Returns current while it + # still names the target, 0 when the host exposes none. + op('target_view', [ + u32('current'), + mutbuf('uint32_t', 'props', 'propCount'), + ], ret='u32'), op('pass_begin', [ podref('gpu_pass_desc', 'desc'), buf('rive_gpu_pass_color_attachment_v1', 'colors',
diff --git a/src/wasm/module/gpu_proxy.cpp b/src/wasm/module/gpu_proxy.cpp index 8d96d41..0e4210f 100644 --- a/src/wasm/module/gpu_proxy.cpp +++ b/src/wasm/module/gpu_proxy.cpp
@@ -346,6 +346,24 @@ uint32_t m_handle; }; +// A view over a host color target, described by the props the host fills: +// width, height, format and sample count. The texture is metadata only, so +// attachment validation sees the real format and sample count. +rcp<ore::TextureView> makeModuleColorView(uint32_t handle, + const uint32_t props[4]) +{ + ore::TextureDesc textureDesc; + textureDesc.width = props[0]; + textureDesc.height = props[1]; + textureDesc.format = (ore::TextureFormat)props[2]; + textureDesc.sampleCount = props[3]; + textureDesc.renderTarget = true; + ore::TextureViewDesc viewDesc; + auto texture = make_rcp<ModuleOreTexture>(0, textureDesc); + viewDesc.texture = texture.get(); + return make_rcp<ModuleOreTextureView>(handle, std::move(texture), viewDesc); +} + class ModuleOreContext : public ore::Context { public: @@ -732,6 +750,24 @@ { return notPortedYet<ore::TextureView>("canvas wraps"); } + + // The host hands back the same handle until its target changes. + rcp<ore::TextureView> targetView() override + { + uint32_t current = + m_targetView != nullptr + ? static_cast<ModuleOreTextureView*>(m_targetView.get()) + ->handle() + : 0; + uint32_t props[4] = {}; + uint32_t handle = rive_gpu_target_view(current, props, 4); + if (handle != current) + { + m_targetView = + handle != 0 ? makeModuleColorView(handle, props) : nullptr; + } + return m_targetView; + } rcp<ore::TextureView> wrapRiveTexture(gpu::Texture*, uint32_t, uint32_t) override @@ -814,6 +850,7 @@ mutable ore::ShaderTarget m_shaderTarget = ore::ShaderTarget::wgsl; mutable bool m_shaderTargetKnown = false; + rcp<ore::TextureView> m_targetView; }; } // namespace @@ -858,17 +895,6 @@ { return {}; } - ore::TextureDesc textureDesc; - textureDesc.width = props[0]; - textureDesc.height = props[1]; - textureDesc.format = (ore::TextureFormat)props[2]; - textureDesc.sampleCount = props[3]; - textureDesc.renderTarget = true; - ore::TextureViewDesc viewDesc; - // Metadata-only texture wrapper so attachment validation sees the - // canvas's real format and sample count. - auto texture = make_rcp<ModuleOreTexture>(0, textureDesc); - viewDesc.texture = texture.get(); WasmModuleCanvas out; // Backing installs unbacked (null image texture, module semantics); // construction alone no longer carries the render target. @@ -876,9 +902,7 @@ out.canvas->setBacking( nullptr, make_rcp<ModuleRenderTarget>(canvasHandle, props[0], props[1])); - out.colorView = make_rcp<ModuleOreTextureView>(viewHandle, - std::move(texture), - viewDesc); + out.colorView = makeModuleColorView(viewHandle, props); return out; } @@ -898,24 +922,13 @@ { return {}; } - ore::TextureDesc textureDesc; - textureDesc.width = props[0]; - textureDesc.height = props[1]; - textureDesc.format = (ore::TextureFormat)props[2]; - textureDesc.sampleCount = props[3]; - textureDesc.renderTarget = true; - ore::TextureViewDesc viewDesc; - auto texture = make_rcp<ModuleOreTexture>(0, textureDesc); - viewDesc.texture = texture.get(); WasmModuleCanvas out; out.canvas = make_rcp<gpu::RenderCanvas>(props[0], props[1]); out.canvas->setBacking(nullptr, make_rcp<ModuleRenderTarget>(target->disownHandle(), props[0], props[1])); - out.colorView = make_rcp<ModuleOreTextureView>(viewHandle, - std::move(texture), - viewDesc); + out.colorView = makeModuleColorView(viewHandle, props); return out; }
diff --git a/src/wasm/wasm_natives_gen.hpp b/src/wasm/wasm_natives_gen.hpp index 51c11d4..08d3b4e 100644 --- a/src/wasm/wasm_natives_gen.hpp +++ b/src/wasm/wasm_natives_gen.hpp
@@ -383,6 +383,7 @@ uint32_t gpuCanvasColorViewImpl(WasmScriptingVM* vm, uint32_t canvas, uint32_t* props, uint32_t propCount); uint32_t gpuCanvasImageImpl(WasmScriptingVM* vm, uint32_t canvas); uint32_t gpuCanvasResizeImpl(WasmScriptingVM* vm, uint32_t canvas, uint32_t width, uint32_t height, uint32_t* props, uint32_t propCount); +uint32_t gpuTargetViewImpl(WasmScriptingVM* vm, uint32_t current, uint32_t* props, uint32_t propCount); uint32_t gpuPassBeginImpl(WasmScriptingVM* vm, const rive_gpu_pass_desc_v1* desc, uint32_t descByteCount, const rive_gpu_pass_color_attachment_v1* colors, uint32_t colorByteCount); void gpuPassSetPipelineImpl(WasmScriptingVM* vm, uint32_t pass, uint32_t pipeline); void gpuPassSetVertexBufferImpl(WasmScriptingVM* vm, uint32_t pass, uint32_t slot, uint32_t buffer, uint32_t offset); @@ -1512,6 +1513,15 @@ } return gpuCanvasResizeImpl(vm, canvas, width, height, props, propCount); } +uint32_t gpuTargetView(wasm_exec_env_t env, uint32_t current, uint32_t* props, uint32_t propCount) +{ + WasmScriptingVM* vm = vmFromEnv(env); + if (propCount != 0 && !wasm_runtime_validate_native_addr(wasm_runtime_get_module_inst(env), (void*)props, (uint64_t)propCount * 4)) + { + return {}; + } + return gpuTargetViewImpl(vm, current, props, propCount); +} uint32_t gpuPassBegin(wasm_exec_env_t env, const rive_gpu_pass_desc_v1* desc, uint32_t descByteCount, const rive_gpu_pass_color_attachment_v1* colors, uint32_t colorByteCount) { WasmScriptingVM* vm = vmFromEnv(env); @@ -2275,6 +2285,7 @@ {"canvas_color_view", (void*)gpuCanvasColorView, "(i*~)i", nullptr}, {"canvas_image", (void*)gpuCanvasImage, "(i)i", nullptr}, {"canvas_resize", (void*)gpuCanvasResize, "(iii*~)i", nullptr}, + {"target_view", (void*)gpuTargetView, "(i*~)i", nullptr}, {"pass_begin", (void*)gpuPassBegin, "(*~*~)i", nullptr}, {"pass_set_pipeline", (void*)gpuPassSetPipeline, "(ii)", (void*)&wasm_runtime_rive_leaf_native}, {"pass_set_vertex_buffer", (void*)gpuPassSetVertexBuffer, "(iiii)", (void*)&wasm_runtime_rive_leaf_native},
diff --git a/src/wasm/wasm_natives_web_gen.hpp b/src/wasm/wasm_natives_web_gen.hpp index b85d3b2..5b27c5e 100644 --- a/src/wasm/wasm_natives_web_gen.hpp +++ b/src/wasm/wasm_natives_web_gen.hpp
@@ -1110,6 +1110,12 @@ return gpuCanvasResizeImpl(vm, canvas, width, height, props, propCount); } EMSCRIPTEN_KEEPALIVE +uint32_t rive_web_gpu_target_view(uint32_t vmHandle, uint32_t current, uint32_t* props, uint32_t propCount) +{ + auto vm = (WasmScriptingVM*)(uintptr_t)vmHandle; + return gpuTargetViewImpl(vm, current, props, propCount); +} +EMSCRIPTEN_KEEPALIVE uint32_t rive_web_gpu_pass_begin(uint32_t vmHandle, const rive_gpu_pass_desc_v1* desc, uint32_t descByteCount, const rive_gpu_pass_color_attachment_v1* colors, uint32_t colorByteCount) { auto vm = (WasmScriptingVM*)(uintptr_t)vmHandle;
diff --git a/src/wasm/wasm_scripting_vm.cpp b/src/wasm/wasm_scripting_vm.cpp index 573c081..d6d6731 100644 --- a/src/wasm/wasm_scripting_vm.cpp +++ b/src/wasm/wasm_scripting_vm.cpp
@@ -2258,6 +2258,42 @@ new HostGpuTextureView{host->colorView}); } +uint32_t gpuTargetViewImpl(WasmScriptingVM* vm, + uint32_t current, + uint32_t* props, + uint32_t propCount) +{ + ore::Context* oreContext = gpuOreContext(vm); + // Props stay unwritten where no host could ever expose a target. + if (vm == nullptr || oreContext == nullptr || !oreContext->isRecording() || + propCount < 4) + { + return 0; + } + rcp<ore::TextureView> view = oreContext->targetView(); + if (view == nullptr) + { + props[0] = 0; + props[1] = 0; + props[2] = (uint32_t)ore::TextureFormat::rgba8unorm; + props[3] = 1; + return 0; + } + props[0] = view->width(); + props[1] = view->height(); + props[2] = (uint32_t)view->texture()->format(); + props[3] = view->texture()->sampleCount(); + auto host = static_cast<HostGpuTextureView*>(vm->handles().resolve( + current, + WasmScriptingVM::HandleTable::Tag::gpuTextureView)); + if (host != nullptr && host->view == view) + { + return current; + } + return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuTextureView, + new HostGpuTextureView{std::move(view)}); +} + uint32_t gpuFeaturesImpl(WasmScriptingVM* vm, uint32_t* out, uint32_t outCount) { ore::Context* oreContext = gpuOreContext(vm); @@ -2349,10 +2385,14 @@ desc.depthStencil.stencilLoadOp = (ore::LoadOp)podDesc->stencilLoadOp; desc.depthStencil.stencilStoreOp = (ore::StoreOp)podDesc->stencilStoreOp; desc.depthStencil.stencilClearValue = podDesc->stencilClearValue; + oreContext->clearLastError(); auto pass = ore::cmd::beginRecordedRenderPass(*oreContext, desc); if (pass == nullptr) { - return 0; + // A pass refused without an error drops, like one with no target. + return oreContext->lastError().empty() + ? 0 + : gpuRejected(vm, oreContext, "beginRenderPass"); } return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuPass, new HostGpuPass{std::move(pass)}); @@ -3473,6 +3513,10 @@ { return 0; } +uint32_t gpuTargetViewImpl(WasmScriptingVM*, uint32_t, uint32_t*, uint32_t) +{ + return 0; +} uint32_t gpuFeaturesImpl(WasmScriptingVM*, uint32_t*, uint32_t) { return 0; } uint32_t gpuPassBeginImpl(WasmScriptingVM*, const rive_gpu_pass_desc_v1*,
diff --git a/src/wasm/web/rive_module_imports.mjs b/src/wasm/web/rive_module_imports.mjs index 310c34d..3bd33e9 100644 --- a/src/wasm/web/rive_module_imports.mjs +++ b/src/wasm/web/rive_module_imports.mjs
@@ -528,6 +528,13 @@ copyOut(props_p, props, propCount * 4); return ret; }, + target_view: (current, props, propCount) => { + stackReset(); + const props_p = stageIn(props, propCount * 4); + const ret = calls.rive_gpu_target_view(current, props_p, propCount); + copyOut(props_p, props, propCount * 4); + return ret; + }, pass_begin: (desc, descByteCount, colors, colorByteCount) => { stackReset(); const desc_p = stageIn(desc, descByteCount);
diff --git a/tests/common/testing_window.hpp b/tests/common/testing_window.hpp index 3810eff..e18b122 100644 --- a/tests/common/testing_window.hpp +++ b/tests/common/testing_window.hpp
@@ -382,6 +382,16 @@ virtual void beginOreFrame() {} virtual void endOreFrame() {} + // Whether scripts may draw into renderTarget() under Rive's content, and + // its format, answered without touching it so the recording thread can + // declare it. + struct OreTarget + { + bool exposed = false; + bool bgra = false; + }; + virtual OreTarget oreTarget() const { return {}; } + // Whether beginFrame through endFrame may run off the thread that made the // window. A current GL context or GLFW event polling in endFrame says no. virtual bool supportsRenderThread() const { return false; }
diff --git a/tests/common/testing_window_android_vulkan.cpp b/tests/common/testing_window_android_vulkan.cpp index 620a326..49bdee0 100644 --- a/tests/common/testing_window_android_vulkan.cpp +++ b/tests/common/testing_window_android_vulkan.cpp
@@ -169,16 +169,7 @@ { auto oreContext = static_cast<rive::ore::Context*>(m_renderContext->getOreContext()); - if (!m_swapchain->isFrameStarted()) - { - VK_ABORT_ON_FAIL(m_swapchain->beginFrame()); - - m_renderTarget->setTargetImageView( - m_swapchain->currentVkImageView(), - m_swapchain->currentVkImage(), - m_swapchain->currentLastAccess()); - } - + beginSwapchainFrame(); oreContext->beginFrame( {.externalCommandBuffer = m_swapchain->currentCommandBuffer(), .safeFrameNumber = m_swapchain->safeFrameNumber(), @@ -280,27 +271,41 @@ m_backendParams.clockwise || options.clockwiseFillOverride}; } + // Ore and Rive must bind the same image, since scripts draw into it too. + void beginSwapchainFrame() + { + if (m_swapchain->isFrameStarted()) + { + return; + } + VK_ABORT_ON_FAIL(m_swapchain->beginFrame()); + if (m_overflowTexture != nullptr) + { + m_renderTarget->setTargetImageView(m_overflowTexture->vkImageView(), + m_overflowTexture->vkImage(), + m_overflowTexture->lastAccess()); + } + else + { + m_renderTarget->setTargetImageView( + m_swapchain->currentVkImageView(), + m_swapchain->currentVkImage(), + m_swapchain->currentLastAccess()); + } + } + + // An srgb swapchain would not match the unorm format Ore reports. + OreTarget oreTarget() const override + { + VkFormat format = m_swapchain->imageFormat(); + return {format == VK_FORMAT_R8G8B8A8_UNORM || + format == VK_FORMAT_B8G8R8A8_UNORM, + format == VK_FORMAT_B8G8R8A8_UNORM}; + } + void flushPLSContext(RenderTarget* offscreenRenderTarget) override { - if (!m_swapchain->isFrameStarted()) - { - VK_ABORT_ON_FAIL(m_swapchain->beginFrame()); - - if (m_overflowTexture != nullptr) - { - m_renderTarget->setTargetImageView( - m_overflowTexture->vkImageView(), - m_overflowTexture->vkImage(), - m_overflowTexture->lastAccess()); - } - else - { - m_renderTarget->setTargetImageView( - m_swapchain->currentVkImageView(), - m_swapchain->currentVkImage(), - m_swapchain->currentLastAccess()); - } - } + beginSwapchainFrame(); m_renderContext->flush({ .renderTarget = offscreenRenderTarget != nullptr ? offscreenRenderTarget
diff --git a/tests/common/testing_window_fiddle_context.cpp b/tests/common/testing_window_fiddle_context.cpp index 1852ae0..eaf0294 100644 --- a/tests/common/testing_window_fiddle_context.cpp +++ b/tests/common/testing_window_fiddle_context.cpp
@@ -556,6 +556,21 @@ return m_fiddleContext->renderTargetOrNull(); } + OreTarget oreTarget() const override + { + switch (backend()) + { + case Backend::d3d: + case Backend::d3d12: + case Backend::dawn: + return {true, false}; + case Backend::metal: + return {true, true}; + default: + return {}; + } + } + void flushPLSContext(RenderTarget* offscreenRenderTarget) override { m_fiddleContext->flushPLSContext(offscreenRenderTarget);
diff --git a/tests/common/testing_window_metal_layer.mm b/tests/common/testing_window_metal_layer.mm index 5772543..3fd410b 100644 --- a/tests/common/testing_window_metal_layer.mm +++ b/tests/common/testing_window_metal_layer.mm
@@ -108,11 +108,6 @@ .synthesizedFailureType = options.synthesizedFailureType, }; m_renderContext->beginFrame(frameDescriptor); - return std::make_unique<RiveRenderer>(m_renderContext.get()); - } - - void flushPLSContext(RenderTarget* offscreenRenderTarget) final - { if (!m_renderTarget || m_renderTarget->width() != m_width || m_renderTarget->height() != m_height) { @@ -123,7 +118,18 @@ } m_renderTarget->setTargetTexture(m_drawable != nil ? m_drawable.texture : nil); + return std::make_unique<RiveRenderer>(m_renderContext.get()); + } + rive::gpu::RenderTarget* renderTarget() const override + { + return m_renderTarget.get(); + } + + OreTarget oreTarget() const override { return {true, true}; } + + void flushPLSContext(RenderTarget* offscreenRenderTarget) final + { // A deferred replay flushes canvases before beginFrame, so the buffer // may not exist yet. if (m_flushCommandBuffer == nil)
diff --git a/tests/common/testing_window_metal_texture.mm b/tests/common/testing_window_metal_texture.mm index 1407c68..fca313c 100644 --- a/tests/common/testing_window_metal_texture.mm +++ b/tests/common/testing_window_metal_texture.mm
@@ -61,32 +61,20 @@ .synthesizedFailureType = options.synthesizedFailureType, }; m_renderContext->beginFrame(frameDescriptor); + // Ore replays into the target before the first flush. + ensureRenderTarget(); return std::make_unique<RiveRenderer>(m_renderContext.get()); } + rive::gpu::RenderTarget* renderTarget() const override + { + return m_renderTarget.get(); + } + + OreTarget oreTarget() const override { return {true, true}; } + void flushPLSContext(RenderTarget* offscreenRenderTarget) final { - if (!m_renderTarget || m_renderTarget->height() != m_height || - m_renderTarget->width() != m_width) - { - m_renderTarget = - m_renderContext->static_impl_cast<RenderContextMetalImpl>() - ->makeRenderTarget( - MTLPixelFormatBGRA8Unorm, m_width, m_height); - MTLTextureDescriptor* desc = [[MTLTextureDescriptor alloc] init]; - desc.pixelFormat = MTLPixelFormatBGRA8Unorm; - desc.width = m_width; - desc.height = m_height; - desc.usage = MTLTextureUsageRenderTarget; - desc.textureType = MTLTextureType2D; - desc.mipmapLevelCount = 1; - desc.storageMode = MTLStorageModePrivate; - m_renderTarget->setTargetTexture( - [m_gpu newTextureWithDescriptor:desc]); - m_pixelReadBuff = - [m_gpu newBufferWithLength:m_height * m_width * 4 - options:MTLResourceStorageModeShared]; - } // A deferred replay flushes canvases before beginFrame, so the buffer // may not exist yet. if (m_flushCommandBuffer == nil) @@ -194,6 +182,31 @@ } private: + void ensureRenderTarget() + { + if (!m_renderTarget || m_renderTarget->height() != m_height || + m_renderTarget->width() != m_width) + { + m_renderTarget = + m_renderContext->static_impl_cast<RenderContextMetalImpl>() + ->makeRenderTarget( + MTLPixelFormatBGRA8Unorm, m_width, m_height); + MTLTextureDescriptor* desc = [[MTLTextureDescriptor alloc] init]; + desc.pixelFormat = MTLPixelFormatBGRA8Unorm; + desc.width = m_width; + desc.height = m_height; + desc.usage = MTLTextureUsageRenderTarget; + desc.textureType = MTLTextureType2D; + desc.mipmapLevelCount = 1; + desc.storageMode = MTLStorageModePrivate; + m_renderTarget->setTargetTexture( + [m_gpu newTextureWithDescriptor:desc]); + m_pixelReadBuff = + [m_gpu newBufferWithLength:m_height * m_width * 4 + options:MTLResourceStorageModeShared]; + } + } + id<MTLDevice> m_gpu = MTLCreateSystemDefaultDevice(); id<MTLCommandQueue> m_queue = [m_gpu newCommandQueue]; std::unique_ptr<RenderContext> m_renderContext;
diff --git a/tests/common/testing_window_sink.hpp b/tests/common/testing_window_sink.hpp index 6163c2d..69ab0b5 100644 --- a/tests/common/testing_window_sink.hpp +++ b/tests/common/testing_window_sink.hpp
@@ -38,11 +38,22 @@ rive::Renderer* beginScreenFrame(uint64_t target) override { assert(target == 0); - m_screen = TestingWindow::Get()->beginFrame(m_options); + FrameOptions options = m_options; + options.doClear = options.doClear && !m_targetPreserved; + m_screen = TestingWindow::Get()->beginFrame(options); return m_screen.get(); } void beginOreFrame() override { TestingWindow::Get()->beginOreFrame(); } void endOreFrame() override { TestingWindow::Get()->endOreFrame(); } + rive::gpu::RenderTarget* targetRenderTarget() override + { + TestingWindow* window = TestingWindow::Get(); + return window->oreTarget().exposed ? window->renderTarget() : nullptr; + } + void setTargetPreserved(bool preserved) override + { + m_targetPreserved = preserved; + } rive::Renderer* beginCanvasContent(rive::gpu::RenderCanvas* canvas, uint32_t clearColor) override @@ -82,6 +93,7 @@ std::unique_ptr<rive::Renderer> m_screen; std::unique_ptr<rive::RiveRenderer> m_canvasRenderer; rive::gpu::RenderCanvas* m_activeCanvas = nullptr; + bool m_targetPreserved = false; }; #endif // RIVE_CANVAS
diff --git a/tests/common/testing_window_vulkan_texture.cpp b/tests/common/testing_window_vulkan_texture.cpp index b19ad6b..0693993 100644 --- a/tests/common/testing_window_vulkan_texture.cpp +++ b/tests/common/testing_window_vulkan_texture.cpp
@@ -103,6 +103,17 @@ return m_renderContext.get(); } + rive::gpu::RenderTarget* renderTarget() const override + { + return m_renderTarget.get(); + } + + // An srgb view would not match the unorm format Ore reports. + OreTarget oreTarget() const override + { + return {!m_backendParams.srgb, !m_backendParams.core}; + } + rcp<rive_tests::OffscreenRenderTarget> makeOffscreenRenderTarget( uint32_t width, uint32_t height,
diff --git a/tests/gm/gmmain.cpp b/tests/gm/gmmain.cpp index fb7439e..6ebd0df 100644 --- a/tests/gm/gmmain.cpp +++ b/tests/gm/gmmain.cpp
@@ -262,6 +262,7 @@ MAKE_GM(ore_pipeline_switch) MAKE_GM(ore_buffer_offsets) MAKE_GM(ore_mip_render_target) + MAKE_GM(ore_deferred_target) MAKE_PARITY_GM3(ore_deferred_replay, ore_deferred_replay_immediate, ore_deferred_replay, @@ -283,6 +284,9 @@ MAKE_PARITY_GM2(render_deferred_canvas, render_deferred_canvas_immediate, render_deferred_canvas) + MAKE_PARITY_GM2(ore_deferred_target_parity, + ore_deferred_target_immediate, + ore_deferred_target) #endif #endif // 2D only so these are not gated on Ore.
diff --git a/tests/gm/ore_deferred_target.cpp b/tests/gm/ore_deferred_target.cpp new file mode 100644 index 0000000..2fa4096 --- /dev/null +++ b/tests/gm/ore_deferred_target.cpp
@@ -0,0 +1,182 @@ +/* + * Copyright 2026 Rive + * + * Ore clears Rive's own render target and draws a triangle into it, then Rive + * draws a path on top, immediately and through a DeferredSession whose target + * view is wrapped at replay. The goldens must be byte identical and show both. + */ + +#include "gm.hpp" +#include "gmutils.hpp" +#include "ore_gm_sink.hpp" +#if ORE_GM_HAS_BACKEND +#include "rive/renderer/ore/ore_buffer.hpp" +#include "rive/renderer/ore/ore_pipeline.hpp" +#include "rive/renderer/ore/ore_render_pass.hpp" +#include "rive/renderer/cmd/deferred_session.hpp" +#endif + +using namespace rivegm; +using namespace rive; +using namespace rive::gpu; + +class OreDeferredTargetGM : public GM +{ +public: + OreDeferredTargetGM(bool deferred) : GM(256, 256), m_deferred(deferred) {} + + // The first Ore pass covers the target, so Rive must not clear over it. + void updateFrameOptions(TestingWindow::FrameOptions* options) const override + { + options->doClear = false; + } + + void onDraw(rive::Renderer* originalRenderer) override + { + auto renderContext = TestingWindow::Get()->renderContext(); + TestingWindow* window = TestingWindow::Get(); + bool exposed = renderContext != nullptr && + m_ore.ensureContext(renderContext) && + window->oreTarget().exposed; +#if ORE_GM_HAS_BACKEND + if (exposed) + { + if (m_deferred) + { + drawDeferred(renderContext, originalRenderer); + } + else + { + drawImmediate(renderContext, originalRenderer); + } + return; + } +#endif + // Keep the frame defined where no target is exposed. + drawBackground(*window->factory(), originalRenderer); + drawPath(*window->factory(), originalRenderer); + } + +private: + static void drawBackground(Factory& factory, Renderer* renderer) + { + auto paint = factory.makeRenderPaint(); + paint->color(0xff1ab38c); + // Overshoots the frame since Rive loads it, and a rasterizer that + // leaves the edge short of full coverage would blend the last frame. + auto path = factory.makeEmptyRenderPath(); + path->moveTo(-1, -1); + path->lineTo(257, -1); + path->lineTo(257, 257); + path->lineTo(-1, 257); + path->close(); + renderer->drawPath(path.get(), paint.get()); + } + + static void drawPath(Factory& factory, Renderer* renderer) + { + auto paint = factory.makeRenderPaint(); + paint->color(0xc0e02040); + auto path = factory.makeEmptyRenderPath(); + path->moveTo(128, 96); + path->lineTo(224, 160); + path->lineTo(128, 224); + path->lineTo(32, 160); + path->close(); + renderer->drawPath(path.get(), paint.get()); + } + +#if ORE_GM_HAS_BACKEND + // Clears the whole target, as the contract asks of the first pass. + void recordTriangle(ore::Context& ctx, ore::TextureView* target) + { + auto shader = ore_gm::loadShader(ctx, ore_gm::kTriangle); + if (!shader.vsModule) + { + return; + } + ore::BufferDesc bd{}; + bd.usage = ore::BufferUsage::vertex; + bd.size = sizeof(ore_gm::kTriVertices); + bd.data = ore_gm::kTriVertices; + bd.label = "ore_deferred_target_vb"; + auto vb = ctx.makeBuffer(bd); + ore_gm::TrianglePipeline tri(shader, + target->texture()->format(), + "ore_deferred_target_pipeline"); + auto pipeline = ctx.makePipeline(tri.desc); + if (!vb || !pipeline) + { + return; + } + + ore::ColorAttachment ca{}; + ca.view = target; + ca.loadOp = ore::LoadOp::clear; + ca.storeOp = ore::StoreOp::store; + ca.clearColor = {0.10f, 0.70f, 0.55f, 1.0f}; + ore::RenderPassDesc rp{}; + rp.colorAttachments[0] = ca; + rp.colorCount = 1; + rp.label = "ore_deferred_target_pass"; + auto pass = ctx.beginRenderPass(rp); + pass->setPipeline(pipeline.get()); + pass->setVertexBuffer(0, vb.get()); + pass->setViewport(0, 0, 256, 256); + pass->draw(3); + pass->finish(); + } + + void drawImmediate(RenderContext* renderContext, Renderer* renderer) + { + auto& realCtx = *renderContext->getOreContext(); + m_ore.beginFrame(renderContext); + auto target = + realCtx.wrapRenderTarget(TestingWindow::Get()->renderTarget()); + if (target != nullptr) + { + recordTriangle(realCtx, target.get()); + } + m_ore.endFrame(renderContext); + ore_gm::invalidateGLStateAfterOre(renderContext); + Factory& factory = *TestingWindow::Get()->factory(); + if (target == nullptr) + { + // Rive loads the target, so an unwrapped one must still be covered. + drawBackground(factory, renderer); + } + drawPath(factory, renderer); + } + + void drawDeferred(RenderContext* renderContext, Renderer* renderer) + { + auto& realCtx = *renderContext->getOreContext(); + TestingWindow* window = TestingWindow::Get(); + rive::cmd::DeferredSession session(ore::ReplayCaps::from(realCtx)); + session.oreContext().setTarget( + ore::Context::TargetDesc::color8(window->width(), + window->height(), + window->oreTarget().bgra)); + auto target = session.oreContext().targetView(); + recordTriangle(session.oreContext(), target.get()); + session.recordOreReplayMarker(); + + auto screen = session.makeScreenRenderer(); + drawPath(session, screen.get()); + + rive::cmd::DeferredFrame frame = rive::cmd::snapshotFrame(session); + ore_gm::GMFrameSink sink(renderContext, + renderer, + &m_ore, + window->renderTarget()); + rive::cmd::DeferredReplayer replayer; + replayer.replayFrame(frame, sink); + } +#endif + + bool m_deferred; + ore_gm::OreGMContext m_ore; +}; + +GMREGISTER(ore_deferred_target_immediate, return new OreDeferredTargetGM(false)) +GMREGISTER(ore_deferred_target, return new OreDeferredTargetGM(true))
diff --git a/tests/gm/ore_deferred_target.mm b/tests/gm/ore_deferred_target.mm new file mode 100644 index 0000000..ab0ea16 --- /dev/null +++ b/tests/gm/ore_deferred_target.mm
@@ -0,0 +1,2 @@ +// Obj-C++ wrapper — ore headers pull in <Metal/Metal.h> on Apple. +#include "ore_deferred_target.cpp"
diff --git a/tests/gm/ore_gm_sink.hpp b/tests/gm/ore_gm_sink.hpp new file mode 100644 index 0000000..8cf34e1 --- /dev/null +++ b/tests/gm/ore_gm_sink.hpp
@@ -0,0 +1,48 @@ +/* + * Copyright 2026 Rive + */ + +#pragma once + +#include "ore_gm_helper.hpp" +#if ORE_GM_HAS_BACKEND +#include "rive/renderer/cmd/deferred_replayer.hpp" + +#include <cassert> + +namespace ore_gm +{ +// The GM is handed an already open screen renderer, so beginScreenFrame just +// returns it. +class GMFrameSink : public rive::cmd::DeferredFrameSink +{ +public: + GMFrameSink(rive::gpu::RenderContext* rc, + rive::Renderer* screen, + OreGMContext* oreCtx, + rive::gpu::RenderTarget* target = nullptr) : + m_rc(rc), m_screen(screen), m_ore(oreCtx), m_target(target) + {} + rive::Factory* factory() override + { + return TestingWindow::Get()->factory(); + } + rive::gpu::RenderContext* renderContext() override { return m_rc; } + rive::Renderer* beginScreenFrame(uint64_t target) override + { + assert(target == 0); + return m_screen; + } + void beginOreFrame() override { m_ore->beginFrame(m_rc); } + void endOreFrame() override { m_ore->endFrame(m_rc); } + void afterOreFrame() override { invalidateGLStateAfterOre(m_rc); } + rive::gpu::RenderTarget* targetRenderTarget() override { return m_target; } + +private: + rive::gpu::RenderContext* m_rc; + rive::Renderer* m_screen; + OreGMContext* m_ore; + rive::gpu::RenderTarget* m_target; +}; +} // namespace ore_gm +#endif
diff --git a/tests/gm/ore_render_deferred_canvas.cpp b/tests/gm/ore_render_deferred_canvas.cpp index e1a2316..3883d7e 100644 --- a/tests/gm/ore_render_deferred_canvas.cpp +++ b/tests/gm/ore_render_deferred_canvas.cpp
@@ -9,13 +9,12 @@ #include "gm.hpp" #include "gmutils.hpp" -#include "ore_gm_helper.hpp" +#include "ore_gm_sink.hpp" #if ORE_GM_HAS_BACKEND #include "rive/renderer/render_canvas.hpp" #include "rive/renderer/ore/ore_render_pass.hpp" #include "rive/renderer/ore/cmd/ore_deferred_context.hpp" #include "rive/renderer/cmd/deferred_session.hpp" -#include "rive/renderer/cmd/deferred_replayer.hpp" #endif using namespace rivegm; @@ -24,38 +23,6 @@ #if ORE_GM_HAS_BACKEND using namespace rive::ore; // Disambiguates from rive::cmd. - -// The GM is handed an already open screen renderer, so beginScreenFrame just -// returns it. -class GMCanvasSink : public rive::cmd::DeferredFrameSink -{ -public: - GMCanvasSink(rive::gpu::RenderContext* rc, - rive::Renderer* screen, - ore_gm::OreGMContext* oreCtx) : - m_rc(rc), m_screen(screen), m_ore(oreCtx) - {} - rive::Factory* factory() override - { - return TestingWindow::Get()->factory(); - } - rive::gpu::RenderContext* renderContext() override { return m_rc; } - // The GM hands over one already open screen renderer, so there is nothing - // to dispatch on. - rive::Renderer* beginScreenFrame(uint64_t target) override - { - assert(target == 0); - return m_screen; - } - void beginOreFrame() override { m_ore->beginFrame(m_rc); } - void endOreFrame() override { m_ore->endFrame(m_rc); } - void afterOreFrame() override { ore_gm::invalidateGLStateAfterOre(m_rc); } - -private: - rive::gpu::RenderContext* m_rc; - rive::Renderer* m_screen; - ore_gm::OreGMContext* m_ore; -}; #endif class RenderDeferredCanvasGM : public GM @@ -129,7 +96,7 @@ // Snapshot replay is the same path a threaded consumer takes. rive::cmd::DeferredFrame frame = rive::cmd::snapshotFrame(session); - GMCanvasSink sink(renderContext, originalRenderer, &m_ore); + ore_gm::GMFrameSink sink(renderContext, originalRenderer, &m_ore); rive::cmd::DeferredReplayer replayer; replayer.replayFrame(frame, sink); }
diff --git a/tests/player/player.cpp b/tests/player/player.cpp index bab6b9a..80df1aa 100644 --- a/tests/player/player.cpp +++ b/tests/player/player.cpp
@@ -438,6 +438,7 @@ // Bound before import so registration scripts that reach for // the device find it, like a real host. m_session->bindRenderContext(rc); + declareTarget(); m_replayer = std::make_unique<rive::cmd::DeferredReplayer>(); m_factory = m_session.get(); } @@ -565,6 +566,20 @@ m_stateMachine->submitGamepadsFromBuffer(buffer.data(), buffer.size()); } +#ifdef RIVE_CANVAS +void Player::declareTarget() +{ + TestingWindow* window = TestingWindow::Get(); + TestingWindow::OreTarget target = window->oreTarget(); + m_session->oreContext().setTarget( + target.exposed + ? rive::ore::Context::TargetDesc::color8(window->width(), + window->height(), + target.bgra) + : rive::ore::Context::TargetDesc{}); +} +#endif + void Player::presentFrame(FrameJob& job) { #ifdef RIVE_CANVAS @@ -679,6 +694,7 @@ #ifdef RIVE_CANVAS if (m_session != nullptr) { + declareTarget(); m_session->recordOreReplayMarker(); renderer = m_session->makeScreenRenderer(); }
diff --git a/tests/player/player.hpp b/tests/player/player.hpp index 174b5d2..45ff3c8 100644 --- a/tests/player/player.hpp +++ b/tests/player/player.hpp
@@ -113,6 +113,8 @@ std::unique_ptr<rive::cmd::DeferredSession> m_session; std::unique_ptr<rive::cmd::DeferredReplayer> m_replayer; uint32_t m_lastDroppedDraws = 0; + // Tells the recording what target scripts may render into. + void declareTarget(); #endif std::string m_rivName;
diff --git a/tests/unit_tests/renderer/ore_deferred_target_test.cpp b/tests/unit_tests/renderer/ore_deferred_target_test.cpp new file mode 100644 index 0000000..5860ef1 --- /dev/null +++ b/tests/unit_tests/renderer/ore_deferred_target_test.cpp
@@ -0,0 +1,299 @@ +/* + * Copyright 2026 Rive + */ + +// The host's render target reaches a recording only as plain values; the real +// target is wrapped at replay. These pin down what the recording writes for it +// and when the replayer keeps Rive from clearing over it. GPU free. + +#include "rive/renderer/cmd/deferred_replayer.hpp" +#include "rive/renderer/cmd/deferred_session.hpp" +#include "rive/renderer/ore/cmd/ore_deferred_context.hpp" +#include "deferred_test_sink.hpp" + +#include <catch.hpp> +#include <memory> +#include <vector> + +using namespace rive; +using namespace rive::ore; +using namespace rive::ore::cmd; + +namespace +{ +Context::TargetDesc target(uint32_t width, + uint32_t height, + TextureFormat format = TextureFormat::rgba8unorm, + uint32_t sampleCount = 1) +{ + return {width, height, format, sampleCount}; +} + +// The target wraps this frame's stream records, in order. +std::vector<WrapCanvasViewPOD> targetWraps(const DeferredOreContext& ctx) +{ + std::vector<WrapCanvasViewPOD> wraps; + OreCommandReader reader(ctx.stream().commandBytes(), + ctx.stream().blobBytes()); + CommandType type; + while (reader.next(type)) + { + switch (type) + { + case CommandType::wrapCanvasView: + { + auto pod = reader.read<WrapCanvasViewPOD>(); + if (pod.mode == + static_cast<uint32_t>(WrapCanvasViewMode::targetView)) + { + wraps.push_back(pod); + } + break; + } + case CommandType::beginRenderPass: + reader.read<BeginRenderPassCmd>(); + break; + case CommandType::finish: + break; + case CommandType::destroyResource: + reader.read<DestroyResourcePOD>(); + break; + default: + FAIL("unexpected command in a target only stream"); + } + } + return wraps; +} + +bool drawInto(Context& ctx, TextureView* view) +{ + RenderPassDesc rp{}; + rp.colorAttachments[0].view = view; + rp.colorAttachments[0].loadOp = LoadOp::clear; + rp.colorAttachments[0].storeOp = StoreOp::store; + rp.colorCount = 1; + auto pass = ctx.beginRenderPass(rp); + if (pass == nullptr) + { + return false; + } + pass->finish(); + return true; +} + +ResourceHandle idOf(const rcp<TextureView>& view) +{ + return static_cast<DeferredTextureView*>(view.get())->clientHandle(); +} + +class SizedTarget : public gpu::RenderTarget +{ +public: + SizedTarget(uint32_t width, uint32_t height) : + gpu::RenderTarget(width, height) + {} +}; + +// Records what the replayer tells the host about its target each frame. +class PreserveSink : public deferred_test::TestSink +{ +public: + std::vector<bool> preserved; + bool hasOre = true; + uint32_t screenWidth = 64; + void setTargetPreserved(bool value) override { preserved.push_back(value); } + Context* oreContext() override { return hasOre ? &m_ore : nullptr; } + gpu::RenderTarget* targetRenderTarget() override + { + m_screen = std::make_unique<SizedTarget>(screenWidth, 64); + return m_screen.get(); + } + +private: + DeferredOreContext m_ore{nullptr}; + std::unique_ptr<SizedTarget> m_screen; +}; +} // namespace + +TEST_CASE("a target view claims its replay slot when it is made", + "[ore][cmd][target]") +{ + DeferredOreContext ctx(nullptr); + ctx.setTarget(target(64, 32)); + + auto view = ctx.targetView(); + REQUIRE(view != nullptr); + CHECK(view->width() == 64u); + CHECK(view->height() == 32u); + + auto wraps = targetWraps(ctx); + REQUIRE(wraps.size() == 1); + CHECK(wraps[0].id == idOf(view)); + + // Asking again hands back the same view without claiming another slot. + CHECK(ctx.targetView() == view); + CHECK(targetWraps(ctx).size() == 1); +} + +TEST_CASE("a frame wraps the target once however many passes draw into it", + "[ore][cmd][target]") +{ + DeferredOreContext ctx(nullptr); + ctx.setTarget(target(64, 64)); + auto view = ctx.targetView(); + + // The wrap made with the view serves the frame it was made in. + for (int i = 0; i < 3; ++i) + { + REQUIRE(drawInto(ctx, view.get())); + } + CHECK(targetWraps(ctx).size() == 1); + CHECK(ctx.targetDrawn()); + + ctx.resetFrame(); + for (int i = 0; i < 3; ++i) + { + REQUIRE(drawInto(ctx, view.get())); + } + auto wraps = targetWraps(ctx); + REQUIRE(wraps.size() == 1); + CHECK(wraps[0].id == idOf(view)); + CHECK(ctx.targetDrawn()); + + // A frame that leaves the target alone neither wraps nor draws it. + ctx.resetFrame(); + CHECK(ctx.targetView() == view); + CHECK(targetWraps(ctx).empty()); + CHECK_FALSE(ctx.targetDrawn()); +} + +TEST_CASE("a target changing size or format mints a new view", + "[ore][cmd][target]") +{ + DeferredOreContext ctx(nullptr); + ctx.setTarget(target(64, 64)); + auto first = ctx.targetView(); + + ctx.setTarget(target(64, 64)); + CHECK(ctx.targetView() == first); + + ctx.setTarget(target(128, 64)); + auto resized = ctx.targetView(); + REQUIRE(resized != nullptr); + CHECK(resized != first); + CHECK(resized->width() == 128u); + + ctx.setTarget(target(128, 64, TextureFormat::bgra8unorm, 4)); + auto reformatted = ctx.targetView(); + REQUIRE(reformatted != nullptr); + CHECK(reformatted != resized); + CHECK(reformatted->texture()->format() == TextureFormat::bgra8unorm); + CHECK(reformatted->texture()->sampleCount() == 4u); +} + +TEST_CASE("with no target declared there is no view and passes drop", + "[ore][cmd][target]") +{ + DeferredOreContext ctx(nullptr); + CHECK(ctx.targetView() == nullptr); + + ctx.setTarget(target(64, 64)); + auto stale = ctx.targetView(); + REQUIRE(stale != nullptr); + ctx.resetFrame(); + + ctx.setTarget({}); + CHECK(ctx.targetView() == nullptr); + // A view kept from an earlier frame names an image that may be gone. + CHECK_FALSE(drawInto(ctx, stale.get())); + CHECK(targetWraps(ctx).empty()); + CHECK_FALSE(ctx.targetDrawn()); + + // The same target coming back keeps its view. + ctx.setTarget(target(64, 64)); + CHECK(ctx.targetView() == stale); + CHECK(drawInto(ctx, stale.get())); + + // Replaced by a new target, the old view is refused. + ctx.resetFrame(); + ctx.setTarget(target(128, 64)); + REQUIRE(ctx.targetView() != nullptr); + CHECK_FALSE(drawInto(ctx, stale.get())); + CHECK_FALSE(ctx.lastError().empty()); +} + +TEST_CASE("replay keeps the target only in frames a script drew into it", + "[ore][cmd][target]") +{ + rive::cmd::DeferredSession session(ReplayCaps{}); + auto& ctx = session.oreContext(); + ctx.setTarget(target(64, 64)); + auto view = ctx.targetView(); + + PreserveSink sink; + rive::cmd::DeferredReplayer replayer; + + REQUIRE(drawInto(ctx, view.get())); + replayer.replayFrame(rive::cmd::snapshotFrame(session), sink); + + session.resetFrame(); + replayer.replayFrame(rive::cmd::snapshotFrame(session), sink); + + session.resetFrame(); + REQUIRE(drawInto(ctx, view.get())); + replayer.replayFrame(rive::cmd::snapshotFrame(session), sink); + + CHECK(sink.preserved == std::vector<bool>{true, false, true}); +} + +TEST_CASE("replay clears the target when no Ore context can draw it", + "[ore][cmd][target]") +{ + rive::cmd::DeferredSession session(ReplayCaps{}); + auto& ctx = session.oreContext(); + ctx.setTarget(target(64, 64)); + auto view = ctx.targetView(); + + PreserveSink sink; + sink.hasOre = false; + rive::cmd::DeferredReplayer replayer; + + REQUIRE(drawInto(ctx, view.get())); + replayer.replayFrame(rive::cmd::snapshotFrame(session), sink); + + CHECK(sink.preserved == std::vector<bool>{false}); +} + +TEST_CASE("replay clears a target resized since the script drew into it", + "[ore][cmd][target]") +{ + rive::cmd::DeferredSession session(ReplayCaps{}); + auto& ctx = session.oreContext(); + ctx.setTarget(target(64, 64)); + auto view = ctx.targetView(); + REQUIRE(drawInto(ctx, view.get())); + rive::cmd::DeferredFrame frame = rive::cmd::snapshotFrame(session); + + PreserveSink sink; + sink.screenWidth = 128; + rive::cmd::DeferredReplayer replayer; + replayer.replayFrame(frame, sink); + + CHECK(sink.preserved == std::vector<bool>{false}); +} + +TEST_CASE("the target view cannot be sampled", "[ore][cmd][target]") +{ + DeferredOreContext ctx(nullptr); + ctx.setTarget(target(64, 64)); + auto view = ctx.targetView(); + REQUIRE(view != nullptr); + + BindGroupDesc bg{}; + BindGroupDesc::TexEntry tex{}; + tex.view = view.get(); + bg.textures = &tex; + bg.textureCount = 1; + CHECK(ctx.makeBindGroup(bg) == nullptr); + CHECK_FALSE(ctx.lastError().empty()); +}
diff --git a/tests/unit_tests/runtime/scripting/scripting_gpu_target_test.cpp b/tests/unit_tests/runtime/scripting/scripting_gpu_target_test.cpp new file mode 100644 index 0000000..3a56ccb --- /dev/null +++ b/tests/unit_tests/runtime/scripting/scripting_gpu_target_test.cpp
@@ -0,0 +1,203 @@ +#include "catch.hpp" +#include "scripting_test_utilities.hpp" +#include "rive/lua/rive_lua_libs.hpp" +#include "utils/no_op_factory.hpp" + +#if defined(RIVE_CANVAS) && defined(RIVE_ORE) +#include "rive/renderer/ore/cmd/ore_deferred_context.hpp" + +using namespace rive; + +namespace +{ +// Records into a real deferred Ore context, the one a host session hands +// scripts, with no device behind it. +class TargetFactory : public NoOpFactory +{ +public: + ore::cmd::DeferredOreContext oreContext{ore::ReplayCaps{}}; + ore::Context* ore() override { return &oreContext; } +}; + +bool runInit(ScriptingTest& vm) +{ + ScriptedObjectTest scriptedObjectTest; + lua_State* L = vm.state(); + lua_getglobal(L, "init"); + lua_pushvalue(L, -2); + lua_newrive<ScriptedContext>(L, &scriptedObjectTest); + if (lua_pcall(L, 2, 1, 0) != LUA_OK) + { + FAIL(lua_tostring(L, -1)); + } + return lua_toboolean(L, -1); +} +} // namespace + +TEST_CASE("context:gpuTarget is nil without a recording context", "[scripting]") +{ + NoOpFactory factory; + ScriptingTest vm(R"( +function init(self, context) + return context:gpuTarget() == nil +end +)", + 1, + false, + {}, + true, + &factory); + CHECK(runInit(vm)); +} + +TEST_CASE("context:gpuTarget is there in init before a target is declared", + "[scripting]") +{ + TargetFactory factory; + ScriptingTest vm(R"( +function init(self, context) + local target = context:gpuTarget() + return target ~= nil and target.view == nil and target.width == 0 +end +)", + 1, + false, + {}, + true, + &factory); + CHECK(runInit(vm)); +} + +TEST_CASE("a hidden target still validates the pass it drops", "[scripting]") +{ + TargetFactory factory; + ScriptingTest vm(R"( +function init(self, context) + local target = context:gpuTarget() + target:beginRenderPass({ color = {{ storeOp = 'store' }} }):finish() + local ok = pcall(function() + target:beginRenderPass({ color = {{ loadOp = 'clear' }} }) + end) + return not ok +end +)", + 1, + false, + {}, + true, + &factory); + CHECK(runInit(vm)); + CHECK_FALSE(factory.oreContext.targetDrawn()); +} + +TEST_CASE("a script reads the target and draws into it", "[scripting]") +{ + TargetFactory factory; + factory.oreContext.setTarget({64, 32, ore::TextureFormat::bgra8unorm, 4}); + ScriptingTest vm(R"( +function init(self, context) + local target = context:gpuTarget() + assert(target.width == 64 and target.height == 32) + assert(target.format == 'bgra8unorm' and target.sampleCount == 4) + assert(target.view ~= nil) + local pass = target:beginRenderPass({ + color = {{ loadOp = 'clear', storeOp = 'store' }}, + }) + pass:finish() + return true +end +)", + 1, + false, + {}, + true, + &factory); + CHECK(runInit(vm)); + CHECK(factory.oreContext.targetDrawn()); +} + +TEST_CASE("a pass on a frame without a target drops its commands", + "[scripting]") +{ + TargetFactory factory; + factory.oreContext.setTarget({64, 64}); + ScriptingTest vm(R"( +local target +function init(self, context) + target = context:gpuTarget() + return target ~= nil +end +function drawLater() + local pass = target:beginRenderPass({ color = {{ storeOp = 'store' }} }) + pass:setViewport(0, 0, 1, 1) + pass:finish() + return target.width == 0 and target.view == nil +end +)", + 1, + false, + {}, + true, + &factory); + REQUIRE(runInit(vm)); + + factory.oreContext.resetFrame(); + factory.oreContext.setTarget({}); + lua_State* L = vm.state(); + lua_getglobal(L, "drawLater"); + REQUIRE(lua_pcall(L, 0, 1, 0) == LUA_OK); + CHECK(lua_toboolean(L, -1)); + CHECK_FALSE(factory.oreContext.targetDrawn()); +} + +TEST_CASE("a cached target view drops while hidden and errors once replaced", + "[scripting]") +{ + TargetFactory factory; + factory.oreContext.setTarget({64, 64}); + ScriptingTest vm(R"( +local target, view +function init(self, context) + target = context:gpuTarget() + view = target.view + return view ~= nil +end +function drawCached() + target:beginRenderPass({ color = {{ view = view, storeOp = 'store' }} }) + :finish() +end +)", + 1, + false, + {}, + true, + &factory); + REQUIRE(runInit(vm)); + lua_State* L = vm.state(); + auto drawCached = [&]() { + lua_getglobal(L, "drawCached"); + bool ok = lua_pcall(L, 0, 0, 0) == LUA_OK; + if (!ok) + { + lua_pop(L, 1); + } + return ok; + }; + + // Hidden for a frame, the same target comes back with the same view. + factory.oreContext.resetFrame(); + factory.oreContext.setTarget({}); + CHECK(drawCached()); + CHECK_FALSE(factory.oreContext.targetDrawn()); + + factory.oreContext.resetFrame(); + factory.oreContext.setTarget({64, 64}); + CHECK(drawCached()); + CHECK(factory.oreContext.targetDrawn()); + + // A resized target retires the view, which a pass now reports. + factory.oreContext.resetFrame(); + factory.oreContext.setTarget({128, 64}); + CHECK_FALSE(drawCached()); +} +#endif