Skip to content

vello_hybrid: Add render_to_atlas and write_to_atlas APIs for glyph caching#1458

Merged
grebmeg merged 2 commits into
mainfrom
gemberg/glyph-cache-hybrid-support
Feb 25, 2026
Merged

vello_hybrid: Add render_to_atlas and write_to_atlas APIs for glyph caching#1458
grebmeg merged 2 commits into
mainfrom
gemberg/glyph-cache-hybrid-support

Conversation

@grebmeg
Copy link
Copy Markdown
Collaborator

@grebmeg grebmeg commented Feb 19, 2026

Adds public APIs to both the wgpu and webgl renderers for rendering Scene content directly into atlas layers and writing pixel data to pre-allocated atlas regions. This enables a glyph caching workflow where vector glyphs are rasterized into the atlas once and reused as cached images in subsequent passes.

The key additions are render_to_atlas, which runs the scheduler targeting a specific atlas layer (using a placeholder dummy texture to avoid read-write hazards), and write_to_atlas, extracted from upload_image, which writes pixel data to an already-allocated region.

@grebmeg grebmeg force-pushed the gemberg/glyph-cache-hybrid-support branch from ddb4221 to 1ece907 Compare February 19, 2026 06:30
@LaurenzV
Copy link
Copy Markdown
Collaborator

Hmm... What I did is to change add an OutputTarget that either refers to the final view or an intermediate texture (

/// The output target for the final composite draw in a round.
///
/// This distinguishes between rendering to the user-visible surface and
/// rendering to an intermediate texture for a filter layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OutputTarget {
/// Render to the final output view/surface.
FinalView,
/// Render to an intermediate texture for a layer with filter effects.
IntermediateTexture(LayerId),
}
/// Specifies the target for a render pass.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenderTarget {
/// Render to the final output or an intermediate texture.
Output(OutputTarget),
/// Render to one of the slot textures used for clipping/blending.
SlotTexture(u8),
}
), and then the selection can happen at runtime instead of having to duplicate the whole render method, would that work for you? Otherwise, seems fine at a glance, but it might be worth extracting all of the setup stuff (e.g. for encoded paints) into a wrapper function that does setup/teardown and takes a closure for the "main action". What do you think?

@grebmeg grebmeg force-pushed the gemberg/glyph-cache-hybrid-support branch 2 times, most recently from 4cc5876 to be7396b Compare February 23, 2026 06:28
@grebmeg
Copy link
Copy Markdown
Collaborator Author

grebmeg commented Feb 23, 2026

Hmm... What I did is to change add an OutputTarget that either refers to the final view or an intermediate texture (

/// The output target for the final composite draw in a round.
///
/// This distinguishes between rendering to the user-visible surface and
/// rendering to an intermediate texture for a filter layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OutputTarget {
/// Render to the final output view/surface.
FinalView,
/// Render to an intermediate texture for a layer with filter effects.
IntermediateTexture(LayerId),
}
/// Specifies the target for a render pass.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenderTarget {
/// Render to the final output or an intermediate texture.
Output(OutputTarget),
/// Render to one of the slot textures used for clipping/blending.
SlotTexture(u8),
}

), and then the selection can happen at runtime instead of having to duplicate the whole render method, would that work for you? Otherwise, seems fine at a glance, but it might be worth extracting all of the setup stuff (e.g. for encoded paints) into a wrapper function that does setup/teardown and takes a closure for the "main action". What do you think?

@laurenz-canva render_to_atlas doesn't need to redirect strips inside the scheduler, the scheduler already sends strips to the same idx == 2 target. The difference is what GPU object backs that target: in render it's the caller's TextureView, in render_to_atlas it's an atlas layer view you create. That choice happens outside the scheduler, in the RendererContext that holds the view reference. I don't think OutputTarget can help here because it operates one level too deep — it switches targets inside flush(), but those two methods already diverge before the scheduler even runs (atlas resize, bind group swap, encoder creation, immediate submit).

That’s at least what I discovered after looking into OutputTarget. I suggest taking another look at those methods once your PR is ready, I’ll have a better understanding of how they’re applied. For now, I’ve done a bit of deduplication you recommended.

Comment thread sparse_strips/vello_hybrid/src/render/wgpu.rs
Comment thread sparse_strips/vello_hybrid/src/render/wgpu.rs
Comment on lines +212 to +219
let result = self.render_scene(
scene,
device,
queue,
&mut encoder,
&atlas_render_size,
&layer_view,
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there might be a problem, which I faced when implementing filters: Our current render strips pipeline expects the color attachment to be in the native pixel format, but the atlas texture array is always RGBA8. Therefore, if the system uses a different pixel format (for example BGRA on MacOS), this will crash, right?

The solution I chose is to create two render strips pipelines, one for the native pixel format and one for always RGBA8, and use the latter when rendering into the image atlas array.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so, and that’s why we pass Rgba8Unorm into the render config explicitly. But I don’t think this issue is new or introduced by the changes here; it seems like something we already had. The better approach is probably to adopt your fix once your PR lands, rather than implementing a parallel solution now.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like something we already had.

Is it? I'm not sure, because this is the first PR that introduces the ability to draw onto the image atlas texture array, no? Up until now, we've only drawn into the final surface or the slot textures, which always use the native format IIRC. I guess it's fine to ignore for now, but maybe add a TODO?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you’re right. I’ll leave the TODO for now and come back to it later. It might be automatically covered once you introduce the filter effects functionality, but if there are any integration issues, I’ll take care of fixing them if you want.

Comment thread sparse_strips/vello_hybrid/src/render/wgpu.rs
Comment thread sparse_strips/vello_hybrid/src/render/webgl.rs
@grebmeg grebmeg force-pushed the gemberg/glyph-cache-hybrid-support branch from be7396b to 0284ce9 Compare February 24, 2026 05:12
@grebmeg grebmeg requested a review from LaurenzV February 24, 2026 05:21
Comment thread sparse_strips/vello_hybrid/src/render/webgl.rs Outdated
@grebmeg grebmeg force-pushed the gemberg/glyph-cache-hybrid-support branch from 0284ce9 to b861095 Compare February 25, 2026 00:17
@grebmeg grebmeg requested a review from taj-p February 25, 2026 00:28
Copy link
Copy Markdown
Contributor

@taj-p taj-p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Just skimmed the code

@grebmeg grebmeg added this pull request to the merge queue Feb 25, 2026
Merged via the queue into main with commit de21b5f Feb 25, 2026
17 checks passed
@grebmeg grebmeg deleted the gemberg/glyph-cache-hybrid-support branch February 25, 2026 02:36
Comment on lines +290 to +291
let load_op = if clear { LoadOp::Clear } else { LoadOp::Load };
ctx.render_strips(&self.fast_path_gpu_strips, 2, load_op);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @grebmeg - I'm not sure the clear member is honoured in the else branch of this block below on line 302

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants