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>
97 lines
2.6 KiB
WebGPU Shading Language
97 lines
2.6 KiB
WebGPU Shading Language
// Prepended to every primitive's shader, which declares its own instance data
|
|
// as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
|
|
// shading one instance of it. What it samples, if anything, is bound at group
|
|
// 2: the texture at binding 0 and the sampler at binding 1.
|
|
|
|
@group(0) @binding(0)
|
|
var<uniform> window: WindowUniform;
|
|
@group(0) @binding(1)
|
|
var<storage> masks: array<Mask>;
|
|
|
|
struct WindowUniform {
|
|
dim: vec2<f32>,
|
|
};
|
|
|
|
struct Mask {
|
|
x: UiSpan,
|
|
y: UiSpan,
|
|
}
|
|
|
|
struct UiSpan {
|
|
start: UiScalar,
|
|
end: UiScalar,
|
|
}
|
|
|
|
struct UiScalar {
|
|
rel: f32,
|
|
abs: f32,
|
|
}
|
|
|
|
struct InstanceInput {
|
|
@location(0) x_start: vec2<f32>,
|
|
@location(1) x_end: vec2<f32>,
|
|
@location(2) y_start: vec2<f32>,
|
|
@location(3) y_end: vec2<f32>,
|
|
@location(4) mask_idx: u32,
|
|
}
|
|
|
|
struct VertexOutput {
|
|
@location(0) top_left: vec2<f32>,
|
|
@location(1) bot_right: vec2<f32>,
|
|
@location(2) uv: vec2<f32>,
|
|
@location(3) @interpolate(flat) mask_idx: u32,
|
|
@location(4) @interpolate(flat) idx: u32,
|
|
@builtin(position) clip_position: vec4<f32>,
|
|
};
|
|
|
|
@vertex
|
|
fn vs_main(
|
|
@builtin(vertex_index) vi: u32,
|
|
@builtin(instance_index) ii: u32,
|
|
in: InstanceInput,
|
|
) -> VertexOutput {
|
|
var out: VertexOutput;
|
|
|
|
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
|
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
|
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
|
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
|
|
|
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
|
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
|
let size = bot_right - top_left;
|
|
|
|
let uv = vec2<f32>(
|
|
f32(vi % 2u),
|
|
f32(vi / 2u)
|
|
);
|
|
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
|
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
|
out.uv = uv;
|
|
out.top_left = top_left;
|
|
out.bot_right = bot_right;
|
|
out.mask_idx = in.mask_idx;
|
|
out.idx = ii;
|
|
|
|
return out;
|
|
}
|
|
|
|
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
|
if in.mask_idx == 4294967295u {
|
|
return color;
|
|
}
|
|
let mask = masks[in.mask_idx];
|
|
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
|
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
|
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
|
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
|
|
|
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
|
let bot_right = floor(br * window.dim) + floor(br_abs);
|
|
let pos = in.clip_position.xy;
|
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
|
return color * 0.0;
|
|
}
|
|
return color;
|
|
}
|