Replaces the bindless `binding_array<texture_2d<f32>>` the renderer bound every texture through. That array needs `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack, so the old shape did not run there at all. The two things being bound want opposite treatment, so they are now split: - **Glyph atlas pages become layers of one `texture_2d_array`.** A glyph primitive carries a `layer` instead of a view/sampler index pair. A layer index is an ordinary sampling operand, so this needs nothing beyond plain Vulkan 1.0 / GLES. Growing the atlas recreates the array with headroom and `copy_texture_to_texture`s the old layers across, no readback. - **A standalone image gets its own texture and its own bind group,** and draws in its own call. It no longer needs a per-instance entry in `PrimitiveData`: the bind group has already picked the texture. `Primitives` keeps images in a list of their own as a result, with `PrimitiveChange::is_image` naming which list a renumbering belongs to -- the two have independent index spaces, so `(layer, inst_idx)` alone would collide between them. Two notes on judgement calls, since this slice was rebuilt on top of `main` rather than transplanted: - The source version renamed `GlyphEntry::is_colored` to `is_color` and added a second `IS_COLOR` flag constant beside the existing `GlyphEntry::IS_COLORED`. Both dropped: #10's naming and its `flags()` are kept, and UVs stay `Vec2` rather than going back to `[f32; 2]`. - `ImageGpu` no longer holds the `Texture` behind its view, which removes an `#[allow(dead_code)]`. A `TextureView` keeps its own reference to the texture, checked by rendering rather than assumed -- see below. ### Verification ``` cargo fmt --all --check cargo clippy --workspace --all-targets --locked -- -D warnings cargo test --workspace --locked ``` All clean; the 4 text-edit tests pass. The only clippy output is the pre-existing future-incompatibility notice about `naga`/`wgpu`/`winit`. Because this is a rendering change, it was also run for real rather than only compiled. The `tabs` example was rendered on this machine's GPU -- Venus onto an RX 7900 XT, confirmed from the loaded ICD (`libvulkan_virtio.so` on `/dev/dri/renderD128`) rather than assumed, since a failed Vulkan init here silently falls back to llvmpipe and would make the screenshots meaningless. Screenshots before and after the change are **byte-identical** (same md5) in two scenes: the default tab, which exercises text (the atlas path) and rects, and the image tab with a standalone image pushed at startup, which exercises the per-image bind group. The image-tab scene needed a temporary local edit to the example to push the image without a click; that edit is not part of this branch. The same comparison, re-run after dropping the `Texture` field, is still byte-identical -- which is the check that the view alone keeps it alive. --------- Co-authored-by: iris <2+iris@noreply.localhost> Reviewed-on: iris/iris#11 Reviewed-by: iris <2+iris@noreply.localhost> Co-authored-by: AIris <4+iris-ai@noreply.localhost>
157 lines
4.7 KiB
Rust
157 lines
4.7 KiB
Rust
use wgpu::*;
|
|
|
|
use crate::{GlyphAtlas, UiData};
|
|
|
|
use super::{
|
|
atlas::PAGE,
|
|
primitive::{ListDraw, PrimitiveRender},
|
|
texture::{default_sampler, sampled_group, sampled_layout, write_region},
|
|
};
|
|
|
|
/// Draws glyphs from the atlas, which it owns: one array texture bound once
|
|
/// for a whole list, since every glyph in it reads the same pages.
|
|
pub struct GlyphRender {
|
|
pages: GpuPages,
|
|
layout: BindGroupLayout,
|
|
sampler: Sampler,
|
|
}
|
|
|
|
impl GlyphRender {
|
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
|
let layout = sampled_layout(device, TextureViewDimension::D2Array, "ui atlas");
|
|
let sampler = default_sampler(device);
|
|
Self {
|
|
pages: GpuPages::new(device, queue, &layout, &sampler),
|
|
layout,
|
|
sampler,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PrimitiveRender for GlyphRender {
|
|
fn layout(&self) -> Option<&BindGroupLayout> {
|
|
Some(&self.layout)
|
|
}
|
|
|
|
fn update(&mut self, ui: &mut UiData) {
|
|
self.pages
|
|
.update(&mut ui.text.atlas, &self.layout, &self.sampler);
|
|
}
|
|
|
|
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
|
pass.set_bind_group(2, self.pages.group(), &[]);
|
|
pass.draw(0..4, 0..list.instances);
|
|
}
|
|
}
|
|
|
|
/// The glyph atlas on the GPU: one array texture whose layers are the pages
|
|
/// `GlyphAtlas` packs.
|
|
///
|
|
/// One array rather than a texture per page because a layer index is ordinary
|
|
/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need
|
|
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
|
|
pub struct GpuPages {
|
|
device: Device,
|
|
queue: Queue,
|
|
texture: Texture,
|
|
group: BindGroup,
|
|
}
|
|
|
|
impl GpuPages {
|
|
pub fn new(
|
|
device: &Device,
|
|
queue: &Queue,
|
|
layout: &BindGroupLayout,
|
|
sampler: &Sampler,
|
|
) -> Self {
|
|
let texture = create_array(device, 1);
|
|
Self {
|
|
device: device.clone(),
|
|
queue: queue.clone(),
|
|
group: atlas_group(device, layout, &texture, sampler),
|
|
texture,
|
|
}
|
|
}
|
|
|
|
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
|
|
if atlas.page_count() > self.texture.depth_or_array_layers() {
|
|
self.grow(atlas.page_count(), layout, sampler);
|
|
}
|
|
for (upload, page) in atlas.uploads() {
|
|
let dst = TexelCopyTextureInfo {
|
|
texture: &self.texture,
|
|
mip_level: 0,
|
|
origin: Origin3d {
|
|
x: upload.rect.x,
|
|
y: upload.rect.y,
|
|
z: upload.layer,
|
|
},
|
|
aspect: TextureAspect::All,
|
|
};
|
|
write_region(&self.queue, dst, page, upload.rect);
|
|
}
|
|
}
|
|
|
|
pub fn group(&self) -> &BindGroup {
|
|
&self.group
|
|
}
|
|
|
|
/// Doubles until `needed` fits and copies the old layers across GPU side.
|
|
/// The new texture stales the group, so that is rebuilt here.
|
|
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
|
|
let old = self.texture.depth_or_array_layers();
|
|
let mut layers = old;
|
|
while layers < needed {
|
|
layers *= 2;
|
|
}
|
|
let texture = create_array(&self.device, layers);
|
|
let mut encoder = self
|
|
.device
|
|
.create_command_encoder(&CommandEncoderDescriptor {
|
|
label: Some("atlas grow"),
|
|
});
|
|
encoder.copy_texture_to_texture(
|
|
self.texture.as_image_copy(),
|
|
texture.as_image_copy(),
|
|
Extent3d {
|
|
width: PAGE,
|
|
height: PAGE,
|
|
depth_or_array_layers: old,
|
|
},
|
|
);
|
|
self.queue.submit(std::iter::once(encoder.finish()));
|
|
self.group = atlas_group(&self.device, layout, &texture, sampler);
|
|
self.texture = texture;
|
|
}
|
|
}
|
|
|
|
fn atlas_group(
|
|
device: &Device,
|
|
layout: &BindGroupLayout,
|
|
texture: &Texture,
|
|
sampler: &Sampler,
|
|
) -> BindGroup {
|
|
let view = texture.create_view(&TextureViewDescriptor {
|
|
dimension: Some(TextureViewDimension::D2Array),
|
|
..Default::default()
|
|
});
|
|
sampled_group(device, layout, &view, sampler, "ui atlas")
|
|
}
|
|
|
|
fn create_array(device: &Device, layers: u32) -> Texture {
|
|
device.create_texture(&TextureDescriptor {
|
|
label: Some("glyph atlas"),
|
|
size: Extent3d {
|
|
width: PAGE,
|
|
height: PAGE,
|
|
depth_or_array_layers: layers,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: TextureFormat::Rgba8Unorm,
|
|
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
|
|
view_formats: &[],
|
|
})
|
|
}
|