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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- New `GpuTarget` class (`rhi/yup_GpuTarget.h`): low-level render-pass-only offscreen GPU surface (`create`, `beginRenderPass`, `asTexture`, `asImage`, `readPixels`). Its backing texture is allocated from the context's main render context, so it does not reserve a dedicated `rive::gpu::RenderContext` — use it for custom `GpuPipeline` work (e.g. post-process passes) that needs no 2D drawing.
- New `GpuCanvas` class (`rhi/yup_GpuCanvas.h`): consolidated backend-agnostic offscreen GPU surface that now composes a `GpuTarget` (over a `RenderableTarget`) and creates a non-owning `Graphics` lazily only when 2D drawing is requested.

#### RHI module extraction & GpuDevice

- **New `yup_rhi` module**: the GPU abstraction layer extracted from `yup_graphics` into its own module (depends on `yup_core`, `yup_shading`, `rive_renderer`). All RHI classes (`GpuFrame`, `GpuPipeline`, `GpuBuffer`, `GpuTexture`, `GpuTarget`, `GpuRenderPass`, `GpuPipelineCache`) now live in `yup_rhi`. `yup_graphics` depends on `yup_rhi` for GPU access.
- **`GpuDevice`**: new reference-counted GPU device abstraction (was `GpuContext`). Owns the native GPU device and command queue without requiring a window — can be used for headless GPU compute (e.g. audio DSP on the GPU). Created via `GpuDevice::create(GpuPlatform, Options)`. All RHI factory methods (`GpuFrame::begin`, `GpuPipeline::compile*`, `GpuBuffer::create`, `GpuTarget::create`) now take `GpuDevice::Ptr` for safe shared ownership.
- **`GpuPlatform`** enum: standalone platform enum (`Headless`, `Metal`, `Direct3D`, `OpenGL`, `OpenGLES`, `WebGPU`) replacing the nested `GpuDevice::Api`. `GraphicsContext::getPlatform()` returns it directly — no typedef alias.
- **`GpuColor`** struct (`rhi/yup_GpuTypes.h`): lightweight 4-component GPU color for render options. Implicitly constructable from any type with `getRedFloat()`/`getGreenFloat()`/`getBlueFloat()`/`getAlphaFloat()` (e.g. `yup::Color`), so `GpuRenderOptions { true, Colors::transparentBlack }` works without code changes.
- **`GraphicsContext` simplified**: wraps a `GpuDevice::Ptr` (obtained via `getGpuDevice()` returning `GpuDevice::Ptr`). Offscreen target management (`createOffscreenTarget`, `beginOffscreen`, `endOffscreen`, `readOffscreenPixels`) delegated to `GpuDevice`. Factory accepts optional `GpuDevice::Ptr` to share an existing GPU device.
- **Backends**: `GpuDevice` has native implementations for all platforms (Metal, OpenGL, Direct3D 11, Dawn, WebGPU/Emscripten, Headless). OpenGL backend probes `GL_VERSION` at runtime to detect compute shader support (GL ≥4.3 / GLES ≥3.1).
- **`::Ptr` safety**: all RHI types that own resources (`GpuPipeline`, `GpuBuffer`, `GpuTexture`, `GpuTarget`, `GpuCanvas`) are reference-counted with `::Ptr`. Factory methods take `GpuDevice::Ptr` to keep the device alive for the resource's lifetime. `GpuFrame` is move-only stack RAII and takes `GpuDevice&` (no ownership).

#### Image Formats

- Added TIFF read/write support (`TiffImageFormat`) via libtiff: RGB, RGBA, Grayscale at 8/16-bit; multi-page reading; DPI and EXIF/ICC/XMP metadata extraction.
Expand Down
18 changes: 12 additions & 6 deletions docs/graphics/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The graphics stack renders 2D vector content and GPU-accelerated scenes across
Metal, Direct3D, OpenGL / OpenGL ES, WebGL and WebGPU (WASM / Emscripten), and Vulkan (in progress).
It is built on the open source [Rive](https://rive.app/) renderer.

**Modules covered:** `yup_graphics`, `yup_shading`, `yup_animation`.
**Modules covered:** `yup_rhi`, `yup_graphics`, `yup_shading`, `yup_animation`.

## In this area

Expand All @@ -29,12 +29,18 @@ own [Imaging](../imaging/index.md) area.

## Key building blocks

The `yup_graphics` module provides:
The `yup_graphics` and `yup_rhi` modules provide:

- **`GraphicsContext`** - abstracts the active rendering backend (`Api::Metal`,
`Api::Direct3D`, `Api::OpenGL`, `Api::OpenGLES`, `Api::WebGPU`, `Api::Headless`),
exposes DPI scaling, offscreen target creation, and the GPU capability probe
`isGpuAvailable()`.
- **`GpuDevice`** (`yup_rhi`) - a reference-counted GPU device abstraction that
owns the native GPU device and command queue without requiring a window.
Created via `GpuDevice::create(GpuPlatform, Options)`. Supports
`GpuPlatform::Metal`, `Direct3D`, `OpenGL`, `OpenGLES`, `WebGPU`, and `Headless`.
Use `GpuDevice` directly for GPU compute (e.g. audio DSP on the GPU) — no window needed.
- **`GraphicsContext`** (`yup_graphics`) - wraps a `GpuDevice` and adds the
window/swapchain layer plus Rive vector rendering. Created via
`GraphicsContext::createContext(GpuPlatform, Options, GpuDevice::Ptr = {})`.
When an existing `GpuDevice::Ptr` is provided, it shares the GPU device
(useful when an audio processor already owns one).
- **`Graphics`** - the immediate-mode 2D drawing API (fills, strokes, paths,
gradients, text, images, textures).
- **Primitives** - points, rectangles, sizes, affine transforms, and colors.
Expand Down
2 changes: 1 addition & 1 deletion docs/graphics/rhi/buffers-and-textures.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ buffer set on the pass via `GpuRenderPass::setUniformBuffer()` instead.
```cpp
enum class GpuBufferType : uint8_t { vertex, index, uniform };

static GpuBuffer::Ptr GpuBuffer::create (GraphicsContext& ctx,
static GpuBuffer::Ptr GpuBuffer::create (GpuDevice::Ptr ctx,
GpuBufferType type,
const void* data,
size_t byteSize);
Expand Down
22 changes: 11 additions & 11 deletions docs/graphics/rhi/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
## The GPU bridge

Every RHI type is a thin, portable wrapper over a single backend bridge: Rive's
GPU context. The `GraphicsContext` owns that context and exposes it to the
RHI layer. You never touch GPU types directly - the RHI hides them behind
YUP-native handles (`GpuPipeline`, `GpuFrame`, `GpuRenderPass`, `GpuBuffer`,
`GpuTexture`).
GPU context. The `GpuDevice` owns that context and exposes it to the RHI layer.
For windowed rendering, `GraphicsContext` wraps a `GpuDevice` and adds the
swapchain and Rive vector renderer. For headless GPU compute, use `GpuDevice`
directly — no window or Rive dependency.

Because the RHI targets one common abstraction, the same code path runs on every
backend: Metal, Direct3D, OpenGL / OpenGL ES (including WebGL2), and WebGPU.
Expand All @@ -16,7 +16,7 @@ backend: Metal, Direct3D, OpenGL / OpenGL ES (including WebGL2), and WebGPU.
Always check GPU availability before creating RHI resources:

```cpp
if (! ctx.isGpuAvailable())
if (! device.isGpuAvailable())
return; // No GPU context - fall back or bail out.
```

Expand All @@ -25,16 +25,16 @@ not reference any GPU type, so user code stays backend-clean.

RHI factory functions honor this contract:

- `GpuPipeline::compile(...)` requires `isGpuAvailable()`.
- `GpuBuffer::create(...)` returns `nullptr` if GPU context is unavailable.
- `GpuTarget::create(...)` / `GpuCanvas::create(...)` return `nullptr` if
- `GpuPipeline::compile(device, ...)` requires `isGpuAvailable()`.
- `GpuBuffer::create(device, ...)` returns `nullptr` if GPU context is unavailable.
- `GpuTarget::create(device, ...)` / `GpuCanvas::create(ctx, ...)` return `nullptr` if
offscreen GPU resources cannot be allocated.

## The frame → pass → draw model

RHI rendering follows a strict hierarchy:

1. **Begin a frame** with `GpuFrame::begin(ctx)`. The frame owns the transient
1. **Begin a frame** with `GpuFrame::begin(device)`. The frame owns the transient
GPU resources (uniform buffers, texture views, samplers) created while
encoding, and keeps them alive until submission completes.
2. **Begin one or more render passes** into a target
Expand All @@ -46,9 +46,9 @@ RHI rendering follows a strict hierarchy:
5. **Submit the frame** with `submit()` (or let it submit on destruction).

```cpp
auto frame = GpuFrame::begin (ctx);
auto frame = GpuFrame::begin (device);
auto pass = target->beginRenderPass (frame, { true, Colors::black });
pass.setPipeline (*pipeline);
pass.setPipeline (pipeline);
pass.draw (3);
pass.finish();
frame.submit();
Expand Down
14 changes: 7 additions & 7 deletions docs/graphics/rhi/frames-and-passes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ GPU resources (uniform buffers, texture views, samplers) created while encoding
its passes.

```cpp
static GpuFrame GpuFrame::begin (GraphicsContext& ctx);
static GpuFrame GpuFrame::begin (GpuDevice::Ptr device);
```

Begin a frame, encode one or more render passes into it, then submit:

```cpp
auto frame = GpuFrame::begin (ctx);
auto frame = GpuFrame::begin (device);
if (! frame.isValid())
return; // No GPU context.

Expand Down Expand Up @@ -62,7 +62,7 @@ auto pass = canvas->beginRenderPass (frame, { true, background });
if (! pass.isValid())
return;

pass.setPipeline (*pipeline);
pass.setPipeline (pipeline);
pass.setUniformBuffer (0, 0, &uniforms, sizeof uniforms);
pass.setTexture (0, 1, sceneTexture);
pass.setVertexBuffer (0, vertexBuffer);
Expand All @@ -89,7 +89,7 @@ For a fullscreen post-process that generates its vertices from the vertex index,
bind **no** vertex buffers and issue a three-vertex draw:

```cpp
pass.setPipeline (*blurPipeline);
pass.setPipeline (blurPipeline);
pass.setTexture (0, 0, sourceTexture);
pass.setUniformBuffer (0, 1, &blurParams, sizeof blurParams);
pass.draw (3); // fullscreen triangle
Expand All @@ -103,8 +103,8 @@ Controls attachment load behavior for a pass:
```cpp
struct GpuRenderOptions
{
bool clear = true; // clear vs. load existing contents
Color clearColor = Colors::transparentBlack; // used when clear == true
bool clear = true; // clear vs. load existing contents
GpuColor clearColor = Colors::transparentBlack; // used when clear == true
};
```

Expand All @@ -114,7 +114,7 @@ struct GpuRenderOptions

```cpp
// Clear to a solid background:
auto pass = target->beginRenderPass (frame, { true, Colors::cornflowerblue });
auto pass = target->beginRenderPass (frame, { true, Colors::black });

// Draw over existing contents:
auto overlay = target->beginRenderPass (frame, { false, Colors::transparentBlack });
Expand Down
15 changes: 10 additions & 5 deletions docs/graphics/rhi/index.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# RHI - GPU Rendering Hardware Interface

The **RHI** is YUP's backend-agnostic, low-level GPU layer. It sits below the 2D
`Graphics` API and above Rive's GPU abstraction, giving you direct control
over pipelines, render passes, buffers, and textures while remaining portable
across Metal, Direct3D, OpenGL and OpenGL ES, WebGL2 and WebGPU, and Vulkan (in progress).
The **RHI** is YUP's backend-agnostic, low-level GPU layer, provided by the
`yup_rhi` module. It sits below the 2D `Graphics` API and above Rive's GPU
abstraction, giving you direct control over pipelines, render passes, buffers,
and textures while remaining portable across Metal, Direct3D, OpenGL / OpenGL ES,
WebGL2, WebGPU, and Vulkan (in progress).

Use the RHI when you need custom GPU work that the 2D `Graphics` API does not
express - 3D geometry, post-process effects, compute-style fullscreen passes, or
offscreen render-to-texture pipelines.
offscreen render-to-texture pipelines. For GPU compute without any window or
graphics (e.g. audio DSP on the GPU), use `GpuDevice` directly — no
`GraphicsContext` or `yup_graphics` dependency needed.

## When to use the RHI

Expand All @@ -21,6 +24,8 @@ offscreen render-to-texture pipelines.

## Classes at a glance

- **`GpuDevice`** - a reference-counted GPU device abstraction. Owns the native
device and command queue. Factories for all RHI resources start here.
- **`GpuFrame`** - RAII scope for one frame's GPU work. Begin, encode passes,
submit.
- **`GpuRenderPass`** - records draw commands (pipeline, bindings, draws) into a
Expand Down
8 changes: 4 additions & 4 deletions docs/graphics/rhi/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ rendering** (indexed or non-indexed) with vertex buffers, culling, and
depth/stencil state.

```{note}
Compiling a pipeline requires GPU context in `GraphicsContext` to exist.
Compiling a pipeline requires a `GpuDevice` with GPU context available.
Check `ctx.isGpuAvailable()` first.
```

Expand All @@ -36,7 +36,7 @@ assignment stays consistent across all targets.

```cpp
ResultValue<GpuPipeline::Ptr> GpuPipeline::compileFromBundle (
GraphicsContext& ctx,
GpuDevice::Ptr ctx,
const ShaderBundle& bundle,
const GpuPipelineOptions& options = {});
```
Expand Down Expand Up @@ -70,7 +70,7 @@ pre-compiled RSTB binding-map blob - see [Binding maps](#binding-maps).

```cpp
ResultValue<GpuPipeline::Ptr> GpuPipeline::compile (
GraphicsContext& ctx,
GpuDevice::Ptr ctx,
const GpuShaderSource& vertexShader,
const GpuShaderSource& fragmentShader,
const GpuPipelineOptions& options = {});
Expand Down Expand Up @@ -233,5 +233,5 @@ if (result.wasOk())
| `setMaxEntries (n)` / `getMaxEntries()` | LRU eviction limit (0 = unlimited; default 256). |
| `generateCacheKey (bundle, options, api)` | Static SHA1-based key generation. |

The cache references an externally-owned `GraphicsContext` that must outlive it.
The cache references an keeps the `GpuDevice` alive via `Ptr`.
Eviction is LRU by access order.
18 changes: 9 additions & 9 deletions docs/graphics/rhi/spinning-cube.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ The cube uses a vertex + fragment shader pair. With the transpiler enabled, GLSL

```cpp
GpuPipelineOptions options;
options.vertexBuffers = &cubeLayout; // position/color/normal
options.vertexBufferCount = 1;
options.indexFormat = GpuIndexFormat::uint16;
options.cullMode = GpuCullMode::back;
options.winding = GpuFaceWinding::counterClockwise;
options.vertexBuffers = &cubeLayout; // position/color/normal
options.vertexBufferCount = 1;
options.indexFormat = GpuIndexFormat::uint16;
options.cullMode = GpuCullMode::back;
options.winding = GpuFaceWinding::counterClockwise;
options.depthStencil.enabled = true;

auto result = GpuPipeline::compileFromGlsl (ctx, vertGlsl, fragGlsl, options);
Expand Down Expand Up @@ -85,10 +85,10 @@ Each frame, begin a `GpuFrame`, open a render pass on the scene canvas, bind the
pipeline + per-frame uniforms + geometry, and issue an indexed draw:

```cpp
auto frame = GpuFrame::begin (ctx);
auto frame = GpuFrame::begin (device);

auto pass = sceneCanvas->beginRenderPass (frame, { true, Colors::black });
pass.setPipeline (*cubePipeline);
pass.setPipeline (cubePipeline);
pass.setUniformBuffer (0, 0, &mvp, sizeof mvp); // per-frame transform
pass.setTexture (0, 1, animatedTexture);
pass.setVertexBuffer (0, cubeVerts);
Expand All @@ -107,14 +107,14 @@ samples the previous result and generates its vertices from the vertex index:
GpuRenderOptions load { false, Colors::transparentBlack };

auto hPass = blurCanvasH->beginRenderPass (frame, { true, Colors::transparentBlack });
hPass.setPipeline (*blurPipeline);
hPass.setPipeline (blurPipeline);
hPass.setTexture (0, 0, sceneCanvas->asTexture());
hPass.setUniformBuffer (0, 1, &horizontalParams, sizeof horizontalParams);
hPass.draw (3);
hPass.finish();

auto vPass = blurCanvasV->beginRenderPass (frame, { true, Colors::transparentBlack });
vPass.setPipeline (*blurPipeline);
vPass.setPipeline (blurPipeline);
vPass.setTexture (0, 0, blurCanvasH->asTexture());
vPass.setUniformBuffer (0, 1, &verticalParams, sizeof verticalParams);
vPass.draw (3);
Expand Down
12 changes: 6 additions & 6 deletions docs/graphics/rhi/targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ The minimal offscreen render surface. It allocates a backing texture from the
context's main render context - no dedicated 2D context is created.

```cpp
static GpuTarget::Ptr GpuTarget::create (GraphicsContext& ctx, int width, int height);
static GpuTarget::Ptr GpuTarget::create (GpuDevice::Ptr ctx, int width, int height);
```

```cpp
auto target = GpuTarget::create (ctx, 256, 256);
if (target != nullptr)
{
auto frame = GpuFrame::begin (ctx);
auto frame = GpuFrame::begin (device);
auto pass = target->beginRenderPass (frame, { true, Colors::transparentBlack });
pass.setPipeline (*pipeline);
pass.setPipeline (pipeline);
pass.draw (3);
pass.finish();
frame.submit();
Expand All @@ -52,11 +52,11 @@ if (target != nullptr)
Builds on a `GpuTarget` but is backed by a **dedicated render context**, adding
2D `Graphics` drawing via `beginDraw()` / `commit()`. It consolidates creation,
rendering, and readback of an offscreen surface into one object, replacing the
lower-level `GraphicsContext::createOffscreenTarget` / `beginOffscreen` /
lower-level `GpuDevice::createOffscreenTarget` / `beginOffscreen` /
`endOffscreen` API.

```cpp
static GpuCanvas::Ptr GpuCanvas::create (GraphicsContext& ctx, int width, int height);
static GpuCanvas::Ptr GpuCanvas::create (GpuDevice::Ptr ctx, int width, int height);
```

### 2D drawing path
Expand Down Expand Up @@ -85,7 +85,7 @@ A canvas can also be the target of a `GpuRenderPass`, exactly like `GpuTarget`:

```cpp
auto pass = canvas->beginRenderPass (frame, { true, background });
pass.setPipeline (*pipeline);
pass.setPipeline (pipeline);
pass.drawIndexed (indexCount);
pass.finish();
```
Expand Down
32 changes: 28 additions & 4 deletions docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,37 @@ flowchart LR
classDef opt fill:#fff7ed,color:#9a3412,stroke:#fb923c,stroke-dasharray:2 2;
```

### yup_rhi

The low-level GPU abstraction layer: device management, compute and render
pipelines, frames, render passes, buffers, textures, and offscreen targets.
This is the foundation for GPU work that does not require a window or Rive
vector rendering — use it directly for GPU compute (audio DSP, FFT) without
pulling in the 2D graphics stack.

```mermaid
flowchart LR
yup_rhi:::self --> yup_core
yup_rhi --> yup_shading
yup_rhi --> rive_renderer:::ext
classDef self fill:#6366f1,color:#fff,stroke:#4f46e5;
classDef ext fill:#f3f4f6,color:#374151,stroke:#9ca3af,stroke-dasharray:4 3;
```

### yup_graphics

The 2D drawing stack and the low-level GPU RHI, rendered through the Rive
renderer. Covers the graphics context, primitives, paths, fonts, SVG, imaging,
and GPU pipelines. Image-codec support is optional: link `libpng`, `libjpeg`,
`libwebp`, and/or `libgif` to enable the corresponding [image formats](imaging/loading.md#available-formats).
The 2D drawing stack and windowed rendering, layered on `yup_rhi` and the Rive
renderer. Covers the `GraphicsContext` (window + swapchain), `Graphics` (2D
drawing API), primitives, paths, fonts, SVG, imaging, and `GpuCanvas` (offscreen
2D surfaces). Image-codec support is optional: link `libpng`, `libjpeg`,
`libwebp`, `libgif`, and/or `libtiff` to enable the corresponding
[image formats](imaging/loading.md#available-formats).

```mermaid
flowchart LR
yup_graphics:::self --> yup_core
yup_graphics --> yup_simd
yup_graphics --> yup_rhi
yup_graphics --> yup_shading
yup_graphics --> rive:::ext
yup_graphics --> rive_renderer:::ext
Expand All @@ -157,6 +177,7 @@ flowchart LR
yup_graphics -. optional .-> libjpeg:::opt
yup_graphics -. optional .-> libwebp:::opt
yup_graphics -. optional .-> libgif:::opt
yup_graphics -. optional .-> libtiff:::opt
classDef self fill:#6366f1,color:#fff,stroke:#4f46e5;
classDef ext fill:#f3f4f6,color:#374151,stroke:#9ca3af,stroke-dasharray:4 3;
classDef opt fill:#fff7ed,color:#9a3412,stroke:#fb923c,stroke-dasharray:2 2;
Expand Down Expand Up @@ -387,12 +408,15 @@ flowchart TD
simd[yup_simd] --> core[yup_core]
events[yup_events] --> core
shading[yup_shading] --> core
rhi[yup_rhi] --> core
rhi --> shading
python[yup_python] --> core
ai[yup_ai] --> core
ai --> events

graphics[yup_graphics] --> core
graphics --> simd
graphics --> rhi
graphics --> shading

animation[yup_animation] --> core
Expand Down
Loading
Loading