add <image> widget

This commit is contained in:
galister
2025-12-23 22:10:46 +09:00
parent 17453ae1d9
commit b352269556
19 changed files with 686 additions and 26 deletions

View File

@@ -2,21 +2,22 @@ use std::{cell::RefCell, rc::Rc, sync::Arc};
use cosmic_text::Buffer;
use glam::{Mat4, Vec2, Vec3};
use slotmap::{SlotMap, new_key_type};
use slotmap::{new_key_type, SlotMap};
use vulkano::pipeline::graphics::viewport;
use crate::{
drawing::{self},
font_config,
gfx::{WGfx, cmd::GfxCommandBuffer},
gfx::{cmd::GfxCommandBuffer, WGfx},
renderer_vk::image::{ImagePipeline, ImageRenderer},
};
use super::{
rect::{RectPipeline, RectRenderer},
text::{
DEFAULT_METRICS, SWASH_CACHE, TextArea, TextBounds,
text_atlas::{TextAtlas, TextPipeline},
text_renderer::TextRenderer,
TextArea, TextBounds, DEFAULT_METRICS, SWASH_CACHE,
},
viewport::Viewport,
};
@@ -26,6 +27,7 @@ struct RendererPass<'a> {
text_areas: Vec<TextArea<'a>>,
text_renderer: TextRenderer,
rect_renderer: RectRenderer,
image_renderer: ImageRenderer,
scissor: Option<drawing::Boundary>,
pixel_scale: f32,
}
@@ -34,16 +36,19 @@ impl RendererPass<'_> {
fn new(
text_atlas: &mut TextAtlas,
rect_pipeline: RectPipeline,
image_pipeline: ImagePipeline,
scissor: Option<drawing::Boundary>,
pixel_scale: f32,
) -> anyhow::Result<Self> {
let text_renderer = TextRenderer::new(text_atlas)?;
let rect_renderer = RectRenderer::new(rect_pipeline)?;
let image_renderer = ImageRenderer::new(image_pipeline)?;
Ok(Self {
submitted: false,
text_renderer,
rect_renderer,
image_renderer,
text_areas: Vec::new(),
scissor,
pixel_scale,
@@ -90,6 +95,7 @@ impl RendererPass<'_> {
self.submitted = true;
self.rect_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?;
self.image_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?;
{
let mut font_system = font_system.system.lock();
@@ -119,18 +125,21 @@ pub struct SharedContext {
atlas_map: SlotMap<SharedContextKey, SharedAtlas>,
rect_pipeline: RectPipeline,
text_pipeline: TextPipeline,
image_pipeline: ImagePipeline,
}
impl SharedContext {
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
let rect_pipeline = RectPipeline::new(gfx.clone(), gfx.surface_format)?;
let text_pipeline = TextPipeline::new(gfx.clone(), gfx.surface_format)?;
let image_pipeline = ImagePipeline::new(gfx.clone(), gfx.surface_format)?;
Ok(Self {
gfx,
atlas_map: SlotMap::with_key(),
rect_pipeline,
text_pipeline,
image_pipeline,
})
}
@@ -237,6 +246,7 @@ impl Context {
passes.push(RendererPass::new(
&mut atlas.text_atlas,
shared.rect_pipeline.clone(),
shared.image_pipeline.clone(),
next_scissor,
self.pixel_scale,
)?);
@@ -293,6 +303,11 @@ impl Context {
transform: extent.transform,
});
}
drawing::RenderPrimitive::Image(extent, image) => {
pass
.image_renderer
.add_image(extent.boundary, image.clone(), &extent.transform);
}
drawing::RenderPrimitive::ScissorSet(boundary) => {
next_scissor = Some(boundary.0);
needs_new_pass = true;

View File

@@ -0,0 +1,244 @@
use std::{collections::HashMap, sync::Arc};
use cosmic_text::SubpixelBin;
use glam::Mat4;
use smallvec::smallvec;
use vulkano::{
buffer::{BufferContents, BufferUsage, Subbuffer},
command_buffer::CommandBufferUsage,
format::Format,
image::view::ImageView,
pipeline::graphics::{self, vertex_input::Vertex},
};
use crate::{
drawing::{Boundary, ImagePrimitive},
gfx::{
cmd::GfxCommandBuffer,
pass::WGfxPass,
pipeline::{WGfxPipeline, WPipelineCreateInfo},
WGfx, BLEND_ALPHA,
},
renderer_vk::{
model_buffer::ModelBuffer,
text::custom_glyph::{CustomGlyphData, RasterizeCustomGlyphRequest, RasterizedCustomGlyph},
},
};
use super::viewport::Viewport;
#[repr(C)]
#[derive(BufferContents, Vertex, Copy, Clone, Debug)]
pub struct ImageVertex {
#[format(R32_UINT)]
pub in_model_idx: u32,
#[format(R32_UINT)]
pub in_rect_dim: [u16; 2],
#[format(R32_UINT)]
pub in_border_color: u32,
#[format(R32_UINT)]
pub round_border: [u8; 4],
}
/// Cloneable pipeline & shaders to be shared between `RectRenderer` instances.
#[derive(Clone)]
pub struct ImagePipeline {
gfx: Arc<WGfx>,
pub(super) inner: Arc<WGfxPipeline<ImageVertex>>,
}
impl ImagePipeline {
pub fn new(gfx: Arc<WGfx>, format: Format) -> anyhow::Result<Self> {
let vert = vert_image::load(gfx.device.clone())?;
let frag = frag_image::load(gfx.device.clone())?;
let pipeline = gfx.create_pipeline::<ImageVertex>(
&vert,
&frag,
WPipelineCreateInfo::new(format)
.use_blend(BLEND_ALPHA)
.use_instanced()
.use_updatable_descriptors(smallvec![2]),
)?;
Ok(Self { gfx, inner: pipeline })
}
}
struct ImageVertexWithContent {
vert: ImageVertex,
content: CustomGlyphData,
content_key: usize, // identifies an image tag.
}
struct CachedPass {
content_id: usize,
vert_buffer: Subbuffer<[ImageVertex]>,
inner: WGfxPass<ImageVertex>,
res: [u32; 2],
}
pub struct ImageRenderer {
pipeline: ImagePipeline,
image_verts: Vec<ImageVertexWithContent>,
model_buffer: ModelBuffer,
cached_passes: HashMap<usize, CachedPass>,
}
impl ImageRenderer {
pub fn new(pipeline: ImagePipeline) -> anyhow::Result<Self> {
Ok(Self {
model_buffer: ModelBuffer::new(&pipeline.gfx)?,
pipeline,
image_verts: vec![],
cached_passes: HashMap::new(),
})
}
pub fn begin(&mut self) {
self.image_verts.clear();
self.model_buffer.begin();
}
pub fn add_image(&mut self, boundary: Boundary, image: ImagePrimitive, transform: &Mat4) {
let in_model_idx = self
.model_buffer
.register_pos_size(&boundary.pos, &boundary.size, transform);
self.image_verts.push(ImageVertexWithContent {
vert: ImageVertex {
in_model_idx,
in_rect_dim: [boundary.size.x as u16, boundary.size.y as u16],
in_border_color: cosmic_text::Color::from(image.border_color).0,
round_border: [
image.round_units,
(image.border) as u8,
0, // unused
0,
],
},
content: image.content,
content_key: image.content_key,
});
}
fn upload_image(
gfx: Arc<WGfx>,
res: [u32; 2],
img: &ImageVertexWithContent,
) -> anyhow::Result<Option<Arc<ImageView>>> {
let raster = match RasterizedCustomGlyph::try_from(&RasterizeCustomGlyphRequest {
data: img.content.clone(),
width: res[0] as _,
height: res[1] as _,
x_bin: SubpixelBin::Zero,
y_bin: SubpixelBin::Zero,
scale: 1.0, // unused
}) {
Some(x) => x,
None => {
log::error!("Unable to rasterize custom image");
return Ok(None);
}
};
let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
let image = cmd_buf.upload_image(
raster.width as _,
raster.height as _,
Format::R8G8B8A8_UNORM,
&raster.data,
)?;
let image_view = ImageView::new_default(image)?;
cmd_buf.build_and_execute_now()?;
Ok(Some(image_view))
}
pub fn render(
&mut self,
gfx: &Arc<WGfx>,
viewport: &mut Viewport,
vk_scissor: &graphics::viewport::Scissor,
cmd_buf: &mut GfxCommandBuffer,
) -> anyhow::Result<()> {
let res = viewport.resolution();
self.model_buffer.upload(gfx)?;
for img in self.image_verts.iter() {
let pass = match self.cached_passes.get_mut(&img.content_key) {
Some(x) => {
if x.content_id != img.content.id || x.res != res {
// image changed
let Some(image_view) = Self::upload_image(self.pipeline.gfx.clone(), res, img)? else {
continue;
};
x.inner
.update_sampler(2, image_view, self.pipeline.gfx.texture_filter)?;
}
x
}
None => {
let vert_buffer = self.pipeline.gfx.empty_buffer(
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
(std::mem::size_of::<ImageVertex>()) as _,
)?;
let Some(image_view) = Self::upload_image(self.pipeline.gfx.clone(), res, img)? else {
continue;
};
let set0 = viewport.get_image_descriptor(&self.pipeline);
let set1 = self.model_buffer.get_image_descriptor(&self.pipeline);
let set2 = self
.pipeline
.inner
.uniform_sampler(2, image_view, self.pipeline.gfx.texture_filter)?;
let pass = self.pipeline.inner.create_pass(
[res[0] as _, res[1] as _],
vert_buffer.clone(),
0..4,
0..self.image_verts.len() as _,
vec![set0, set1, set2],
vk_scissor,
)?;
self.cached_passes.insert(
img.content_key,
CachedPass {
content_id: img.content.id,
vert_buffer,
inner: pass,
res,
},
);
self.cached_passes.get_mut(&img.content_key).unwrap()
}
};
pass.vert_buffer.write()?[0..1].clone_from_slice(&[img.vert]);
cmd_buf.run_ref(&pass.inner)?;
}
Ok(())
}
}
pub mod vert_image {
vulkano_shaders::shader! {
ty: "vertex",
path: "src/renderer_vk/shaders/image.vert",
}
}
pub mod frag_image {
vulkano_shaders::shader! {
ty: "fragment",
path: "src/renderer_vk/shaders/image.frag",
}
}

View File

@@ -1,4 +1,5 @@
pub mod context;
pub mod image;
pub mod model_buffer;
pub mod rect;
pub mod text;

View File

@@ -8,7 +8,7 @@ use vulkano::{
use crate::{
gfx,
renderer_vk::{rect::RectPipeline, text::text_atlas::TextPipeline},
renderer_vk::{image::ImagePipeline, rect::RectPipeline, text::text_atlas::TextPipeline},
};
pub struct ModelBuffer {
@@ -19,6 +19,8 @@ pub struct ModelBuffer {
buffer_capacity_f32: u32,
rect_descriptor: Option<Arc<DescriptorSet>>,
text_descriptor: Option<Arc<DescriptorSet>>,
image_descriptor: Option<Arc<DescriptorSet>>,
}
impl ModelBuffer {
@@ -40,6 +42,8 @@ impl ModelBuffer {
buffer,
buffer_capacity_f32: INITIAL_CAPACITY_F32,
rect_descriptor: None,
text_descriptor: None,
image_descriptor: None,
})
}
@@ -109,8 +113,15 @@ impl ModelBuffer {
pub fn get_text_descriptor(&mut self, pipeline: &TextPipeline) -> Arc<DescriptorSet> {
self
.rect_descriptor
.text_descriptor
.get_or_insert_with(|| pipeline.inner.buffer(3, self.buffer.clone()).unwrap())
.clone()
}
pub fn get_image_descriptor(&mut self, pipeline: &ImagePipeline) -> Arc<DescriptorSet> {
self
.image_descriptor
.get_or_insert_with(|| pipeline.inner.buffer(1, self.buffer.clone()).unwrap())
.clone()
}
}

View File

@@ -10,10 +10,10 @@ use vulkano::{
use crate::{
drawing::{Boundary, Rectangle},
gfx::{
BLEND_ALPHA, WGfx,
cmd::GfxCommandBuffer,
pass::WGfxPass,
pipeline::{WGfxPipeline, WPipelineCreateInfo},
WGfx, BLEND_ALPHA,
},
renderer_vk::model_buffer::ModelBuffer,
};

View File

@@ -0,0 +1,55 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
precision highp float;
layout(location = 0) in vec2 in_uv;
layout(location = 1) in vec4 in_border_color;
layout(location = 2) in float in_border_size; // in units
layout(location = 3) in float in_radius; // in units
layout(location = 4) in vec2 in_rect_size;
layout(location = 0) out vec4 out_color;
#define UNIFORM_PARAMS_SET 0
#include "uniform.glsl"
layout(set = 2, binding = 0) uniform sampler2D image;
void main() {
float rect_aspect = in_rect_size.x / in_rect_size.y;
vec2 center = in_rect_size / 2.0;
vec2 coords = in_uv * in_rect_size;
float radius = in_radius;
vec2 sdf_rect_dim = center - vec2(radius);
float sdf = length(max(abs(coords - center), sdf_rect_dim) - sdf_rect_dim) -
in_radius;
vec4 color = texture(image, in_uv);
float pixel_size = 1.0 / uniforms.pixel_scale;
if (in_border_size < in_radius) {
// rounded border
float f = in_border_size > 0.0 ? smoothstep(in_border_size + pixel_size,
in_border_size, -sdf) *
in_border_color.a
: 0.0;
out_color = mix(color, in_border_color, f);
} else {
// square border
vec2 a = abs(coords - center);
float aa = center.x - in_border_size;
float bb = center.y - in_border_size;
out_color = (a.x > aa || a.y > bb) ? in_border_color : color;
}
if (in_radius > 0.0) {
// rounding cutout alpha
out_color.a *= 1.0 - smoothstep(-pixel_size, 0.0, sdf);
}
}

View File

@@ -0,0 +1,52 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
precision highp float;
layout(location = 0) in uint in_model_idx;
layout(location = 1) in uint in_rect_dim;
layout(location = 2) in uint in_border_color;
layout(location = 3) in uint round_border;
layout(location = 0) out vec2 out_uv;
layout(location = 1) out vec4 out_border_color;
layout(location = 2) out float out_border_size;
layout(location = 3) out float out_radius;
layout(location = 4) out vec2 out_rect_size;
#define UNIFORM_PARAMS_SET 0
#define MODEL_BUFFER_SET 1
#include "model_buffer.glsl"
#include "uniform.glsl"
void main() {
uint v = uint(gl_VertexIndex); // 0-3
uint rect_width = in_rect_dim & 0xffffu;
uint rect_height = (in_rect_dim & 0xffff0000u) >> 16u;
vec2 rect_size = vec2(float(rect_width), float(rect_height));
float rect_aspect = rect_size.x / rect_size.y;
// 0.0 - 1.0 normalized
uvec2 corner_pos_u = uvec2(v & 1u, (v >> 1u) & 1u);
vec2 corner_pos = vec2(corner_pos_u);
out_uv = corner_pos;
mat4 model_matrix = model_buffer.models[in_model_idx];
out_rect_size = rect_size;
gl_Position = uniforms.projection * model_matrix * vec4(corner_pos, 0.0, 1.0);
out_border_color =
vec4(float((in_border_color & 0x00ff0000u) >> 16u) / 255.0,
float((in_border_color & 0x0000ff00u) >> 8u) / 255.0,
float(in_border_color & 0x000000ffu) / 255.0,
float((in_border_color & 0xff000000u) >> 24u) / 255.0);
float radius = float(round_border & 0xffu);
out_radius = radius;
float border_size = float((round_border & 0xff00u) >> 8);
out_border_size = border_size;
}

View File

@@ -53,8 +53,8 @@ impl CustomGlyphContent {
/// Clone and reuse this to avoid atlasing the same content twice.
#[derive(Debug, Clone)]
pub struct CustomGlyphData {
pub(super) id: usize,
pub(super) content: Arc<CustomGlyphContent>,
pub(crate) id: usize,
pub(crate) content: Arc<CustomGlyphContent>,
}
impl CustomGlyphData {
@@ -157,7 +157,7 @@ pub struct RasterizedCustomGlyph {
}
impl RasterizedCustomGlyph {
pub(super) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<Self> {
pub(crate) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<Self> {
match input.data.content.as_ref() {
CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input),
CustomGlyphContent::Image(data) => Some(rasterize_image(data)),

View File

@@ -5,7 +5,10 @@ use vulkano::{
descriptor_set::DescriptorSet,
};
use crate::{gfx::WGfx, renderer_vk::util::WMat4};
use crate::{
gfx::WGfx,
renderer_vk::{image::ImagePipeline, util::WMat4},
};
use super::{rect::RectPipeline, text::text_atlas::TextPipeline};
@@ -16,6 +19,7 @@ pub struct Viewport {
params_buffer: Subbuffer<[Params]>,
text_descriptor: Option<Arc<DescriptorSet>>,
rect_descriptor: Option<Arc<DescriptorSet>>,
image_descriptor: Option<Arc<DescriptorSet>>,
}
impl Viewport {
@@ -36,6 +40,7 @@ impl Viewport {
params_buffer,
text_descriptor: None,
rect_descriptor: None,
image_descriptor: None,
})
}
@@ -57,6 +62,15 @@ impl Viewport {
.clone()
}
pub fn get_image_descriptor(&mut self, pipeline: &ImagePipeline) -> Arc<DescriptorSet> {
self
.image_descriptor
.get_or_insert_with(|| {
pipeline.inner.buffer(0, self.params_buffer.clone()).unwrap() // safe unwrap
})
.clone()
}
/// Updates the `Viewport` with the given `resolution` and `projection`.
pub fn update(&mut self, resolution: [u32; 2], projection: &glam::Mat4, pixel_scale: f32) -> anyhow::Result<()> {
if self.params.screen_resolution == resolution