new workspace
This commit is contained in:
259
wgui/src/renderer_vk/text/custom_glyph.rs
Normal file
259
wgui/src/renderer_vk/text/custom_glyph.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use std::{
|
||||
f32,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use cosmic_text::SubpixelBin;
|
||||
use image::RgbaImage;
|
||||
use resvg::usvg::{Options, Tree};
|
||||
|
||||
use crate::assets::AssetProvider;
|
||||
|
||||
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CustomGlyphContent {
|
||||
Svg(Box<Tree>),
|
||||
Image(RgbaImage),
|
||||
}
|
||||
|
||||
impl CustomGlyphContent {
|
||||
pub fn from_bin_svg(data: &[u8]) -> anyhow::Result<Self> {
|
||||
let tree = Tree::from_data(data, &Options::default())?;
|
||||
Ok(CustomGlyphContent::Svg(Box::new(tree)))
|
||||
}
|
||||
|
||||
pub fn from_bin_raster(data: &[u8]) -> anyhow::Result<Self> {
|
||||
let image = image::load_from_memory(data)?.into_rgba8();
|
||||
Ok(CustomGlyphContent::Image(image))
|
||||
}
|
||||
|
||||
pub fn from_assets(provider: &mut Box<dyn AssetProvider>, path: &str) -> anyhow::Result<Self> {
|
||||
let data = provider.load_from_path(path)?;
|
||||
if path.ends_with(".svg") || path.ends_with(".svgz") {
|
||||
Ok(CustomGlyphContent::from_bin_svg(&data)?)
|
||||
} else {
|
||||
Ok(CustomGlyphContent::from_bin_raster(&data)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(path: &str) -> anyhow::Result<Self> {
|
||||
let data = std::fs::read(path)?;
|
||||
if path.ends_with(".svg") || path.ends_with(".svgz") {
|
||||
Ok(CustomGlyphContent::from_bin_svg(&data)?)
|
||||
} else {
|
||||
Ok(CustomGlyphContent::from_bin_raster(&data)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CustomGlyphData {
|
||||
pub(super) id: usize,
|
||||
pub(super) content: Arc<CustomGlyphContent>,
|
||||
}
|
||||
|
||||
impl CustomGlyphData {
|
||||
pub fn new(content: CustomGlyphContent) -> Self {
|
||||
Self {
|
||||
id: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed),
|
||||
content: Arc::new(content),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dim_for_cache_key(&self, width: u16, height: u16) -> (u16, u16) {
|
||||
const MAX_RASTER_DIM: u16 = 256;
|
||||
match self.content.as_ref() {
|
||||
CustomGlyphContent::Svg(..) => (
|
||||
width.next_power_of_two().min(MAX_RASTER_DIM),
|
||||
height.next_power_of_two().min(MAX_RASTER_DIM),
|
||||
),
|
||||
CustomGlyphContent::Image(image) => (image.width() as _, image.height() as _),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CustomGlyphData {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id.eq(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom glyph to render
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CustomGlyph {
|
||||
/// The unique identifier for this glyph
|
||||
pub data: CustomGlyphData,
|
||||
/// The position of the left edge of the glyph
|
||||
pub left: f32,
|
||||
/// The position of the top edge of the glyph
|
||||
pub top: f32,
|
||||
/// The width of the glyph
|
||||
pub width: f32,
|
||||
/// The height of the glyph
|
||||
pub height: f32,
|
||||
/// The color of this glyph (only relevant if the glyph is rendered with the
|
||||
/// type [`ContentType::Mask`])
|
||||
///
|
||||
/// Set to `None` to use [`crate::TextArea::default_color`].
|
||||
pub color: Option<cosmic_text::Color>,
|
||||
/// If `true`, then this glyph will be snapped to the nearest whole physical
|
||||
/// pixel and the resulting `SubpixelBin`'s in `RasterizationRequest` will always
|
||||
/// be `Zero` (useful for images and other large glyphs).
|
||||
pub snap_to_physical_pixel: bool,
|
||||
}
|
||||
|
||||
impl CustomGlyph {
|
||||
pub fn new(data: CustomGlyphData) -> Self {
|
||||
Self {
|
||||
data,
|
||||
left: 0.0,
|
||||
top: 0.0,
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
color: None,
|
||||
snap_to_physical_pixel: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A request to rasterize a custom glyph
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RasterizeCustomGlyphRequest {
|
||||
/// The unique identifier of the glyph
|
||||
pub data: CustomGlyphData,
|
||||
/// The width of the glyph in physical pixels
|
||||
pub width: u16,
|
||||
/// The height of the glyph in physical pixels
|
||||
pub height: u16,
|
||||
/// Binning of fractional X offset
|
||||
///
|
||||
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
|
||||
/// will always be `Zero`.
|
||||
pub x_bin: SubpixelBin,
|
||||
/// Binning of fractional Y offset
|
||||
///
|
||||
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
|
||||
/// will always be `Zero`.
|
||||
pub y_bin: SubpixelBin,
|
||||
/// The scaling factor applied to the text area (Note that `width` and
|
||||
/// `height` are already scaled by this factor.)
|
||||
pub scale: f32,
|
||||
}
|
||||
|
||||
/// A rasterized custom glyph
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RasterizedCustomGlyph {
|
||||
/// The raw image data
|
||||
pub data: Vec<u8>,
|
||||
/// The type of image data contained in `data`
|
||||
pub content_type: ContentType,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl RasterizedCustomGlyph {
|
||||
pub(super) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<RasterizedCustomGlyph> {
|
||||
match input.data.content.as_ref() {
|
||||
CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input),
|
||||
CustomGlyphContent::Image(data) => rasterize_image(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn validate(
|
||||
&self,
|
||||
input: &RasterizeCustomGlyphRequest,
|
||||
expected_type: Option<ContentType>,
|
||||
) {
|
||||
if let Some(expected_type) = expected_type {
|
||||
assert_eq!(
|
||||
self.content_type, expected_type,
|
||||
"Custom glyph rasterizer must always produce the same content type for a given input. Expected {:?}, got {:?}. Input: {:?}",
|
||||
expected_type, self.content_type, input
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
self.data.len(),
|
||||
self.width as usize * self.height as usize * self.content_type.bytes_per_pixel(),
|
||||
"Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}",
|
||||
self.width as usize * self.height as usize * self.content_type.bytes_per_pixel(),
|
||||
self.data.len(),
|
||||
input,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct CustomGlyphCacheKey {
|
||||
/// Font ID
|
||||
pub glyph_id: usize,
|
||||
/// Glyph width
|
||||
pub width: u16,
|
||||
/// Glyph height
|
||||
pub height: u16,
|
||||
/// Binning of fractional X offset
|
||||
pub x_bin: SubpixelBin,
|
||||
/// Binning of fractional Y offset
|
||||
pub y_bin: SubpixelBin,
|
||||
}
|
||||
|
||||
/// The type of image data contained in a rasterized glyph
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ContentType {
|
||||
/// Each pixel contains 32 bits of rgba data
|
||||
Color,
|
||||
/// Each pixel contains a single 8 bit channel
|
||||
Mask,
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
/// The number of bytes per pixel for this content type
|
||||
pub fn bytes_per_pixel(&self) -> usize {
|
||||
match self {
|
||||
Self::Color => 4,
|
||||
Self::Mask => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rasterize_svg(
|
||||
tree: &Tree,
|
||||
input: &RasterizeCustomGlyphRequest,
|
||||
) -> Option<RasterizedCustomGlyph> {
|
||||
// Calculate the scale based on the "glyph size".
|
||||
let svg_size = tree.size();
|
||||
let scale_x = input.width as f32 / svg_size.width();
|
||||
let scale_y = input.height as f32 / svg_size.height();
|
||||
|
||||
let mut pixmap = resvg::tiny_skia::Pixmap::new(input.width as u32, input.height as u32)?;
|
||||
let mut transform = resvg::usvg::Transform::from_scale(scale_x, scale_y);
|
||||
|
||||
// Offset the glyph by the subpixel amount.
|
||||
let offset_x = input.x_bin.as_float();
|
||||
let offset_y = input.y_bin.as_float();
|
||||
if offset_x != 0.0 || offset_y != 0.0 {
|
||||
transform = transform.post_translate(offset_x, offset_y);
|
||||
}
|
||||
|
||||
resvg::render(tree, transform, &mut pixmap.as_mut());
|
||||
|
||||
Some(RasterizedCustomGlyph {
|
||||
data: pixmap.data().to_vec(),
|
||||
content_type: ContentType::Color,
|
||||
width: input.width,
|
||||
height: input.height,
|
||||
})
|
||||
}
|
||||
|
||||
fn rasterize_image(image: &RgbaImage) -> Option<RasterizedCustomGlyph> {
|
||||
Some(RasterizedCustomGlyph {
|
||||
data: image.to_vec(),
|
||||
content_type: ContentType::Color,
|
||||
width: image.width() as _,
|
||||
height: image.height() as _,
|
||||
})
|
||||
}
|
||||
225
wgui/src/renderer_vk/text/mod.rs
Normal file
225
wgui/src/renderer_vk/text/mod.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
pub mod custom_glyph;
|
||||
mod shaders;
|
||||
pub mod text_atlas;
|
||||
pub mod text_renderer;
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
use cosmic_text::{
|
||||
Align, Attrs, Buffer, Color, FontSystem, Metrics, Style, SwashCache, Weight, Wrap,
|
||||
};
|
||||
use custom_glyph::{ContentType, CustomGlyph};
|
||||
use etagere::AllocId;
|
||||
use glam::Mat4;
|
||||
|
||||
use crate::drawing::{self};
|
||||
|
||||
pub static FONT_SYSTEM: LazyLock<Mutex<FontSystem>> =
|
||||
LazyLock::new(|| Mutex::new(FontSystem::new()));
|
||||
pub static SWASH_CACHE: LazyLock<Mutex<SwashCache>> =
|
||||
LazyLock::new(|| Mutex::new(SwashCache::new()));
|
||||
|
||||
/// Used in case no font_size is defined
|
||||
const DEFAULT_FONT_SIZE: f32 = 14.;
|
||||
|
||||
/// In case no line_height is defined, use font_size * DEFAULT_LINE_HEIGHT_RATIO
|
||||
const DEFAULT_LINE_HEIGHT_RATIO: f32 = 1.43;
|
||||
|
||||
pub(crate) const DEFAULT_METRICS: Metrics = Metrics::new(
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_SIZE * DEFAULT_LINE_HEIGHT_RATIO,
|
||||
);
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextStyle {
|
||||
pub size: Option<f32>,
|
||||
pub line_height: Option<f32>,
|
||||
pub color: Option<drawing::Color>, // TODO: should this be hex?
|
||||
pub style: Option<FontStyle>,
|
||||
pub weight: Option<FontWeight>,
|
||||
pub align: Option<HorizontalAlign>,
|
||||
pub wrap: bool,
|
||||
}
|
||||
|
||||
impl From<&TextStyle> for Attrs<'_> {
|
||||
fn from(style: &TextStyle) -> Self {
|
||||
Attrs::new()
|
||||
.color(style.color.unwrap_or_default().into())
|
||||
.style(style.style.unwrap_or_default().into())
|
||||
.weight(style.weight.unwrap_or_default().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TextStyle> for Metrics {
|
||||
fn from(style: &TextStyle) -> Self {
|
||||
let font_size = style.size.unwrap_or(DEFAULT_FONT_SIZE);
|
||||
|
||||
Metrics {
|
||||
font_size,
|
||||
line_height: style
|
||||
.size
|
||||
.unwrap_or_else(|| (font_size * DEFAULT_LINE_HEIGHT_RATIO).round()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TextStyle> for Wrap {
|
||||
fn from(value: &TextStyle) -> Self {
|
||||
if value.wrap {
|
||||
Wrap::WordOrGlyph
|
||||
} else {
|
||||
Wrap::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper structs for serde
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum FontStyle {
|
||||
#[default]
|
||||
Normal,
|
||||
Italic,
|
||||
}
|
||||
|
||||
impl From<FontStyle> for Style {
|
||||
fn from(value: FontStyle) -> Style {
|
||||
match value {
|
||||
FontStyle::Normal => Style::Normal,
|
||||
FontStyle::Italic => Style::Italic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum FontWeight {
|
||||
#[default]
|
||||
Normal,
|
||||
Bold,
|
||||
}
|
||||
|
||||
impl From<FontWeight> for Weight {
|
||||
fn from(value: FontWeight) -> Weight {
|
||||
match value {
|
||||
FontWeight::Normal => Weight::NORMAL,
|
||||
FontWeight::Bold => Weight::BOLD,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum HorizontalAlign {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
Justified,
|
||||
End,
|
||||
}
|
||||
|
||||
impl From<HorizontalAlign> for Align {
|
||||
fn from(value: HorizontalAlign) -> Align {
|
||||
match value {
|
||||
HorizontalAlign::Left => Align::Left,
|
||||
HorizontalAlign::Right => Align::Right,
|
||||
HorizontalAlign::Center => Align::Center,
|
||||
HorizontalAlign::Justified => Align::Justified,
|
||||
HorizontalAlign::End => Align::End,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<drawing::Color> for cosmic_text::Color {
|
||||
fn from(value: drawing::Color) -> cosmic_text::Color {
|
||||
cosmic_text::Color::rgba(
|
||||
(value.r * 255.999) as _,
|
||||
(value.g * 255.999) as _,
|
||||
(value.b * 255.999) as _,
|
||||
(value.a * 255.999) as _,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<cosmic_text::Color> for drawing::Color {
|
||||
fn from(value: cosmic_text::Color) -> drawing::Color {
|
||||
drawing::Color::new(
|
||||
value.r() as f32 / 255.999,
|
||||
value.g() as f32 / 255.999,
|
||||
value.b() as f32 / 255.999,
|
||||
value.a() as f32 / 255.999,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// glyphon types below
|
||||
|
||||
pub(super) enum GpuCacheStatus {
|
||||
InAtlas {
|
||||
x: u16,
|
||||
y: u16,
|
||||
content_type: ContentType,
|
||||
},
|
||||
SkipRasterization,
|
||||
}
|
||||
|
||||
pub(super) struct GlyphDetails {
|
||||
width: u16,
|
||||
height: u16,
|
||||
gpu_cache: GpuCacheStatus,
|
||||
atlas_id: Option<AllocId>,
|
||||
top: i16,
|
||||
left: i16,
|
||||
}
|
||||
|
||||
/// Controls the visible area of the text. Any text outside of the visible area will be clipped.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TextBounds {
|
||||
/// The position of the left edge of the visible area.
|
||||
pub left: i32,
|
||||
/// The position of the top edge of the visible area.
|
||||
pub top: i32,
|
||||
/// The position of the right edge of the visible area.
|
||||
pub right: i32,
|
||||
/// The position of the bottom edge of the visible area.
|
||||
pub bottom: i32,
|
||||
}
|
||||
|
||||
/// The default visible area doesn't clip any text.
|
||||
impl Default for TextBounds {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
left: i32::MIN,
|
||||
top: i32::MIN,
|
||||
right: i32::MAX,
|
||||
bottom: i32::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A text area containing text to be rendered along with its overflow behavior.
|
||||
#[derive(Clone)]
|
||||
pub struct TextArea<'a> {
|
||||
/// The buffer containing the text to be rendered.
|
||||
pub buffer: Rc<RefCell<Buffer>>,
|
||||
/// The left edge of the buffer.
|
||||
pub left: f32,
|
||||
/// The top edge of the buffer.
|
||||
pub top: f32,
|
||||
/// The scaling to apply to the buffer.
|
||||
pub scale: f32,
|
||||
/// The visible bounds of the text area. This is used to clip the text and doesn't have to
|
||||
/// match the `left` and `top` values.
|
||||
pub bounds: TextBounds,
|
||||
/// The default color of the text area.
|
||||
pub default_color: Color,
|
||||
/// Additional custom glyphs to render.
|
||||
pub custom_glyphs: &'a [CustomGlyph],
|
||||
/// Distance from camera, 0.0..=1.0
|
||||
pub depth: f32,
|
||||
/// Text transformation
|
||||
pub transform: Mat4,
|
||||
}
|
||||
13
wgui/src/renderer_vk/text/shaders.rs
Normal file
13
wgui/src/renderer_vk/text/shaders.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod vert_atlas {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "vertex",
|
||||
path: "src/renderer_vk/shaders/text.vert",
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_atlas {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
path: "src/renderer_vk/shaders/text.frag",
|
||||
}
|
||||
}
|
||||
335
wgui/src/renderer_vk/text/text_atlas.rs
Normal file
335
wgui/src/renderer_vk/text/text_atlas.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use cosmic_text::{FontSystem, SwashCache};
|
||||
use etagere::{Allocation, BucketedAtlasAllocator, size2};
|
||||
use lru::LruCache;
|
||||
use rustc_hash::FxHasher;
|
||||
use std::{collections::HashSet, hash::BuildHasherDefault, sync::Arc};
|
||||
use vulkano::{
|
||||
buffer::BufferContents,
|
||||
command_buffer::CommandBufferUsage,
|
||||
descriptor_set::DescriptorSet,
|
||||
format::Format,
|
||||
image::{Image, ImageCreateInfo, ImageType, ImageUsage, view::ImageView},
|
||||
memory::allocator::AllocationCreateInfo,
|
||||
pipeline::graphics::{input_assembly::PrimitiveTopology, vertex_input::Vertex},
|
||||
};
|
||||
|
||||
use super::{
|
||||
GlyphDetails, GpuCacheStatus,
|
||||
custom_glyph::ContentType,
|
||||
shaders::{frag_atlas, vert_atlas},
|
||||
text_renderer::GlyphonCacheKey,
|
||||
};
|
||||
use crate::gfx::{BLEND_ALPHA, WGfx, pipeline::WGfxPipeline};
|
||||
|
||||
/// Pipeline & shaders to be reused between TextRenderer instances
|
||||
#[derive(Clone)]
|
||||
pub struct TextPipeline {
|
||||
pub(super) gfx: Arc<WGfx>,
|
||||
pub(in super::super) inner: Arc<WGfxPipeline<GlyphVertex>>,
|
||||
}
|
||||
|
||||
impl TextPipeline {
|
||||
pub fn new(gfx: Arc<WGfx>, format: Format) -> anyhow::Result<Self> {
|
||||
let vert = vert_atlas::load(gfx.device.clone())?;
|
||||
let frag = frag_atlas::load(gfx.device.clone())?;
|
||||
|
||||
let pipeline = gfx.create_pipeline::<GlyphVertex>(
|
||||
vert,
|
||||
frag,
|
||||
format,
|
||||
Some(BLEND_ALPHA),
|
||||
PrimitiveTopology::TriangleStrip,
|
||||
true,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
gfx,
|
||||
inner: pipeline,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(BufferContents, Vertex, Copy, Clone, Debug, Default)]
|
||||
pub struct GlyphVertex {
|
||||
#[format(R32_UINT)]
|
||||
pub in_model_idx: u32,
|
||||
#[format(R32_UINT)]
|
||||
pub in_rect_dim: [u16; 2],
|
||||
#[format(R32_UINT)]
|
||||
pub in_uv: [u16; 2],
|
||||
#[format(R32_UINT)]
|
||||
pub in_color: u32,
|
||||
#[format(R32_UINT)]
|
||||
pub in_content_type: [u16; 2], // 2 bytes unused! TODO
|
||||
#[format(R32_SFLOAT)]
|
||||
pub depth: f32,
|
||||
#[format(R32_SFLOAT)]
|
||||
pub scale: f32,
|
||||
}
|
||||
|
||||
type Hasher = BuildHasherDefault<FxHasher>;
|
||||
|
||||
pub(super) struct InnerAtlas {
|
||||
pub kind: Kind,
|
||||
pub image_view: Arc<ImageView>,
|
||||
pub image_descriptor: Arc<DescriptorSet>,
|
||||
pub packer: BucketedAtlasAllocator,
|
||||
pub size: u32,
|
||||
pub glyph_cache: LruCache<GlyphonCacheKey, GlyphDetails, Hasher>,
|
||||
pub glyphs_in_use: HashSet<GlyphonCacheKey, Hasher>,
|
||||
pub max_texture_dimension_2d: u32,
|
||||
common: TextPipeline,
|
||||
}
|
||||
|
||||
impl InnerAtlas {
|
||||
const INITIAL_SIZE: u32 = 256;
|
||||
|
||||
fn new(common: TextPipeline, kind: Kind) -> anyhow::Result<Self> {
|
||||
let max_texture_dimension_2d = common
|
||||
.gfx
|
||||
.device
|
||||
.physical_device()
|
||||
.properties()
|
||||
.max_image_dimension2_d;
|
||||
let size = Self::INITIAL_SIZE.min(max_texture_dimension_2d);
|
||||
|
||||
let packer = BucketedAtlasAllocator::new(size2(size as i32, size as i32));
|
||||
|
||||
// Create a texture to use for our atlas
|
||||
let image = Image::new(
|
||||
common.gfx.memory_allocator.clone(),
|
||||
ImageCreateInfo {
|
||||
image_type: ImageType::Dim2d,
|
||||
format: kind.texture_format(),
|
||||
extent: [size, size, 1],
|
||||
usage: ImageUsage::SAMPLED | ImageUsage::TRANSFER_SRC | ImageUsage::TRANSFER_DST,
|
||||
..Default::default()
|
||||
},
|
||||
AllocationCreateInfo::default(),
|
||||
)?;
|
||||
|
||||
let image_view = ImageView::new_default(image).unwrap();
|
||||
|
||||
let image_descriptor = common.inner.uniform_sampler(
|
||||
Self::descriptor_set(kind),
|
||||
image_view.clone(),
|
||||
common.gfx.texture_filter,
|
||||
)?;
|
||||
|
||||
let glyph_cache = LruCache::unbounded_with_hasher(Hasher::default());
|
||||
let glyphs_in_use = HashSet::with_hasher(Hasher::default());
|
||||
|
||||
Ok(Self {
|
||||
kind,
|
||||
image_view,
|
||||
image_descriptor,
|
||||
packer,
|
||||
size,
|
||||
glyph_cache,
|
||||
glyphs_in_use,
|
||||
max_texture_dimension_2d,
|
||||
common,
|
||||
})
|
||||
}
|
||||
|
||||
fn descriptor_set(kind: Kind) -> usize {
|
||||
match kind {
|
||||
Kind::Color => 0,
|
||||
Kind::Mask => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn try_allocate(&mut self, width: usize, height: usize) -> Option<Allocation> {
|
||||
let size = size2(width as i32, height as i32);
|
||||
|
||||
loop {
|
||||
let allocation = self.packer.allocate(size);
|
||||
|
||||
if allocation.is_some() {
|
||||
return allocation;
|
||||
}
|
||||
|
||||
// Try to free least recently used allocation
|
||||
let (mut key, mut value) = self.glyph_cache.peek_lru()?;
|
||||
|
||||
// Find a glyph with an actual size
|
||||
while value.atlas_id.is_none() {
|
||||
// All sized glyphs are in use, cache is full
|
||||
if self.glyphs_in_use.contains(key) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let _ = self.glyph_cache.pop_lru();
|
||||
|
||||
(key, value) = self.glyph_cache.peek_lru()?;
|
||||
}
|
||||
|
||||
// All sized glyphs are in use, cache is full
|
||||
if self.glyphs_in_use.contains(key) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (_, value) = self.glyph_cache.pop_lru().unwrap();
|
||||
self.packer.deallocate(value.atlas_id.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn num_channels(&self) -> usize {
|
||||
self.kind.num_channels()
|
||||
}
|
||||
|
||||
pub(super) fn grow(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
cache: &mut SwashCache,
|
||||
) -> anyhow::Result<bool> {
|
||||
if self.size >= self.max_texture_dimension_2d {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Grow each dimension by a factor of 2. The growth factor was chosen to match the growth
|
||||
// factor of `Vec`.`
|
||||
const GROWTH_FACTOR: u32 = 2;
|
||||
let new_size = (self.size * GROWTH_FACTOR).min(self.max_texture_dimension_2d);
|
||||
log::info!("Grow {:?} atlas {} → {new_size}", self.kind, self.size);
|
||||
|
||||
self.packer.grow(size2(new_size as i32, new_size as i32));
|
||||
|
||||
let old_image = self.image_view.image().clone();
|
||||
|
||||
let image = self.common.gfx.new_image(
|
||||
new_size,
|
||||
new_size,
|
||||
old_image.format(),
|
||||
ImageUsage::SAMPLED | ImageUsage::TRANSFER_SRC | ImageUsage::TRANSFER_DST,
|
||||
)?;
|
||||
|
||||
self.image_view = ImageView::new_default(image.clone()).unwrap();
|
||||
|
||||
let mut cmd_buf = self
|
||||
.common
|
||||
.gfx
|
||||
.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
|
||||
// Re-upload glyphs
|
||||
for (&cache_key, glyph) in &self.glyph_cache {
|
||||
let (x, y) = match glyph.gpu_cache {
|
||||
GpuCacheStatus::InAtlas { x, y, .. } => (x, y),
|
||||
GpuCacheStatus::SkipRasterization => continue,
|
||||
};
|
||||
|
||||
let (width, height) = match cache_key {
|
||||
GlyphonCacheKey::Text(cache_key) => {
|
||||
let image = cache.get_image_uncached(font_system, cache_key).unwrap();
|
||||
let width = image.placement.width as usize;
|
||||
let height = image.placement.height as usize;
|
||||
(width, height)
|
||||
}
|
||||
GlyphonCacheKey::Custom(cache_key) => (cache_key.width as usize, cache_key.height as usize),
|
||||
};
|
||||
|
||||
let offset = [x as _, y as _, 0];
|
||||
cmd_buf.copy_image(
|
||||
old_image.clone(),
|
||||
offset,
|
||||
image.clone(),
|
||||
offset,
|
||||
Some([width as _, height as _, 1]),
|
||||
)?;
|
||||
}
|
||||
cmd_buf.build_and_execute_now()?;
|
||||
|
||||
self.size = new_size;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn trim(&mut self) {
|
||||
self.glyphs_in_use.clear();
|
||||
}
|
||||
|
||||
fn rebind_descriptor(&mut self) -> anyhow::Result<()> {
|
||||
self.image_descriptor = self.common.inner.uniform_sampler(
|
||||
Self::descriptor_set(self.kind),
|
||||
self.image_view.clone(),
|
||||
self.common.gfx.texture_filter,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Kind {
|
||||
Mask,
|
||||
Color,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
fn num_channels(self) -> usize {
|
||||
match self {
|
||||
Kind::Mask => 1,
|
||||
Kind::Color => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_format(self) -> Format {
|
||||
match self {
|
||||
Kind::Mask => Format::R8_UNORM,
|
||||
Kind::Color => Format::R8G8B8A8_UNORM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An atlas containing a cache of rasterized glyphs that can be rendered.
|
||||
pub struct TextAtlas {
|
||||
pub(super) common: TextPipeline,
|
||||
pub(super) color_atlas: InnerAtlas,
|
||||
pub(super) mask_atlas: InnerAtlas,
|
||||
}
|
||||
|
||||
impl TextAtlas {
|
||||
/// Creates a new [`TextAtlas`].
|
||||
pub fn new(common: TextPipeline) -> anyhow::Result<Self> {
|
||||
let color_atlas = InnerAtlas::new(common.clone(), Kind::Color)?;
|
||||
let mask_atlas = InnerAtlas::new(common.clone(), Kind::Mask)?;
|
||||
|
||||
Ok(Self {
|
||||
common,
|
||||
color_atlas,
|
||||
mask_atlas,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn trim(&mut self) {
|
||||
self.mask_atlas.trim();
|
||||
self.color_atlas.trim();
|
||||
}
|
||||
|
||||
pub(super) fn grow(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
cache: &mut SwashCache,
|
||||
content_type: ContentType,
|
||||
) -> anyhow::Result<bool> {
|
||||
let did_grow = match content_type {
|
||||
ContentType::Mask => self.mask_atlas.grow(font_system, cache)?,
|
||||
ContentType::Color => self.color_atlas.grow(font_system, cache)?,
|
||||
};
|
||||
|
||||
if did_grow {
|
||||
self.color_atlas.rebind_descriptor()?;
|
||||
self.mask_atlas.rebind_descriptor()?;
|
||||
}
|
||||
|
||||
Ok(did_grow)
|
||||
}
|
||||
|
||||
pub(super) fn inner_for_content_mut(&mut self, content_type: ContentType) -> &mut InnerAtlas {
|
||||
match content_type {
|
||||
ContentType::Color => &mut self.color_atlas,
|
||||
ContentType::Mask => &mut self.mask_atlas,
|
||||
}
|
||||
}
|
||||
}
|
||||
484
wgui/src/renderer_vk/text/text_renderer.rs
Normal file
484
wgui/src/renderer_vk/text/text_renderer.rs
Normal file
@@ -0,0 +1,484 @@
|
||||
use crate::{
|
||||
gfx::cmd::GfxCommandBuffer,
|
||||
renderer_vk::{model_buffer::ModelBuffer, viewport::Viewport},
|
||||
};
|
||||
|
||||
use super::{
|
||||
ContentType, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, TextArea,
|
||||
custom_glyph::{CustomGlyphCacheKey, RasterizeCustomGlyphRequest, RasterizedCustomGlyph},
|
||||
text_atlas::{GlyphVertex, TextAtlas, TextPipeline},
|
||||
};
|
||||
use cosmic_text::{Color, SubpixelBin, SwashContent};
|
||||
use glam::{Mat4, Vec2, Vec3};
|
||||
use vulkano::{
|
||||
buffer::{BufferUsage, Subbuffer},
|
||||
command_buffer::CommandBufferUsage,
|
||||
};
|
||||
|
||||
/// A text renderer that uses cached glyphs to render text into an existing render pass.
|
||||
pub struct TextRenderer {
|
||||
pipeline: TextPipeline,
|
||||
vertex_buffer: Subbuffer<[GlyphVertex]>,
|
||||
vertex_buffer_capacity: usize,
|
||||
glyph_vertices: Vec<GlyphVertex>,
|
||||
model_buffer: ModelBuffer,
|
||||
}
|
||||
|
||||
impl TextRenderer {
|
||||
/// Creates a new `TextRenderer`.
|
||||
pub fn new(atlas: &mut TextAtlas) -> anyhow::Result<Self> {
|
||||
// A buffer element is a single quad with a glyph on it
|
||||
const INITIAL_CAPACITY: usize = 256;
|
||||
|
||||
let vertex_buffer = atlas.common.gfx.empty_buffer(
|
||||
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
|
||||
INITIAL_CAPACITY as _,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
model_buffer: ModelBuffer::new(&atlas.common.gfx)?,
|
||||
pipeline: atlas.common.clone(),
|
||||
vertex_buffer,
|
||||
vertex_buffer_capacity: INITIAL_CAPACITY,
|
||||
glyph_vertices: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Prepares all of the provided text areas for rendering.
|
||||
pub fn prepare<'a>(
|
||||
&mut self,
|
||||
font_system: &mut FontSystem,
|
||||
atlas: &mut TextAtlas,
|
||||
viewport: &Viewport,
|
||||
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||
cache: &mut SwashCache,
|
||||
) -> anyhow::Result<()> {
|
||||
self.glyph_vertices.clear();
|
||||
|
||||
let resolution = viewport.resolution();
|
||||
|
||||
for text_area in text_areas {
|
||||
let bounds_min_x = text_area.bounds.left.max(0);
|
||||
let bounds_min_y = text_area.bounds.top.max(0);
|
||||
let bounds_max_x = text_area.bounds.right.min(resolution[0] as i32);
|
||||
let bounds_max_y = text_area.bounds.bottom.min(resolution[1] as i32);
|
||||
|
||||
for glyph in text_area.custom_glyphs.iter() {
|
||||
let x = text_area.left + (glyph.left * text_area.scale);
|
||||
let y = text_area.top + (glyph.top * text_area.scale);
|
||||
let width = (glyph.width * text_area.scale).round() as u16;
|
||||
let height = (glyph.height * text_area.scale).round() as u16;
|
||||
|
||||
let (x, y, x_bin, y_bin) = if glyph.snap_to_physical_pixel {
|
||||
(
|
||||
x.round() as i32,
|
||||
y.round() as i32,
|
||||
SubpixelBin::Zero,
|
||||
SubpixelBin::Zero,
|
||||
)
|
||||
} else {
|
||||
let (x, x_bin) = SubpixelBin::new(x);
|
||||
let (y, y_bin) = SubpixelBin::new(y);
|
||||
(x, y, x_bin, y_bin)
|
||||
};
|
||||
|
||||
let (cached_width, cached_height) = glyph.data.dim_for_cache_key(width, height);
|
||||
|
||||
let cache_key = GlyphonCacheKey::Custom(CustomGlyphCacheKey {
|
||||
glyph_id: glyph.data.id,
|
||||
width: cached_width,
|
||||
height: cached_height,
|
||||
x_bin,
|
||||
y_bin,
|
||||
});
|
||||
|
||||
let color = glyph.color.unwrap_or(text_area.default_color);
|
||||
|
||||
if let Some(glyph_to_render) = prepare_glyph(
|
||||
PrepareGlyphParams {
|
||||
label_pos: Vec2::new(text_area.left, text_area.top),
|
||||
x,
|
||||
y,
|
||||
line_y: 0.0,
|
||||
color,
|
||||
cache_key,
|
||||
atlas,
|
||||
cache,
|
||||
font_system,
|
||||
model_buffer: &mut self.model_buffer,
|
||||
scale_factor: text_area.scale,
|
||||
glyph_scale: width as f32 / cached_width as f32,
|
||||
bounds_min_x,
|
||||
bounds_min_y,
|
||||
bounds_max_x,
|
||||
bounds_max_y,
|
||||
depth: text_area.depth,
|
||||
transform: &text_area.transform,
|
||||
},
|
||||
|_cache, _font_system| -> Option<GetGlyphImageResult> {
|
||||
if cached_width == 0 || cached_height == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let input = RasterizeCustomGlyphRequest {
|
||||
data: glyph.data.clone(),
|
||||
width: cached_width,
|
||||
height: cached_height,
|
||||
x_bin,
|
||||
y_bin,
|
||||
scale: text_area.scale,
|
||||
};
|
||||
|
||||
let output = RasterizedCustomGlyph::try_from(&input)?;
|
||||
|
||||
output.validate(&input, None);
|
||||
|
||||
Some(GetGlyphImageResult {
|
||||
content_type: output.content_type,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: output.width,
|
||||
height: output.height,
|
||||
data: output.data,
|
||||
})
|
||||
},
|
||||
)? {
|
||||
self.glyph_vertices.push(glyph_to_render);
|
||||
}
|
||||
}
|
||||
|
||||
let is_run_visible = |run: &cosmic_text::LayoutRun| {
|
||||
let start_y_physical = (text_area.top + (run.line_top * text_area.scale)) as i32;
|
||||
let end_y_physical = start_y_physical + (run.line_height * text_area.scale) as i32;
|
||||
|
||||
start_y_physical <= text_area.bounds.bottom && text_area.bounds.top <= end_y_physical
|
||||
};
|
||||
|
||||
let buffer = text_area.buffer.borrow();
|
||||
|
||||
let layout_runs = buffer
|
||||
.layout_runs()
|
||||
.skip_while(|run| !is_run_visible(run))
|
||||
.take_while(is_run_visible);
|
||||
|
||||
for run in layout_runs {
|
||||
for glyph in run.glyphs.iter() {
|
||||
let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale);
|
||||
|
||||
let color = match glyph.color_opt {
|
||||
Some(some) => some,
|
||||
None => text_area.default_color,
|
||||
};
|
||||
|
||||
if let Some(glyph_to_render) = prepare_glyph(
|
||||
PrepareGlyphParams {
|
||||
label_pos: Vec2::new(text_area.left, text_area.top),
|
||||
x: physical_glyph.x,
|
||||
y: physical_glyph.y,
|
||||
line_y: run.line_y,
|
||||
color,
|
||||
cache_key: GlyphonCacheKey::Text(physical_glyph.cache_key),
|
||||
atlas,
|
||||
cache,
|
||||
font_system,
|
||||
model_buffer: &mut self.model_buffer,
|
||||
glyph_scale: 1.0,
|
||||
scale_factor: text_area.scale,
|
||||
bounds_min_x,
|
||||
bounds_min_y,
|
||||
bounds_max_x,
|
||||
bounds_max_y,
|
||||
depth: text_area.depth,
|
||||
transform: &text_area.transform,
|
||||
},
|
||||
|cache, font_system| -> Option<GetGlyphImageResult> {
|
||||
let image = cache.get_image_uncached(font_system, physical_glyph.cache_key)?;
|
||||
|
||||
let content_type = match image.content {
|
||||
SwashContent::Color => ContentType::Color,
|
||||
SwashContent::Mask => ContentType::Mask,
|
||||
SwashContent::SubpixelMask => {
|
||||
// Not implemented yet, but don't panic if this happens.
|
||||
ContentType::Mask
|
||||
}
|
||||
};
|
||||
|
||||
Some(GetGlyphImageResult {
|
||||
content_type,
|
||||
top: image.placement.top as i16,
|
||||
left: image.placement.left as i16,
|
||||
width: image.placement.width as u16,
|
||||
height: image.placement.height as u16,
|
||||
data: image.data,
|
||||
})
|
||||
},
|
||||
)? {
|
||||
self.glyph_vertices.push(glyph_to_render);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let will_render = !self.glyph_vertices.is_empty();
|
||||
if !will_render {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let vertices = self.glyph_vertices.as_slice();
|
||||
|
||||
while self.vertex_buffer_capacity < vertices.len() {
|
||||
let new_capacity = self.vertex_buffer_capacity * 2;
|
||||
self.vertex_buffer = self.pipeline.gfx.empty_buffer(
|
||||
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
|
||||
new_capacity as _,
|
||||
)?;
|
||||
self.vertex_buffer_capacity = new_capacity;
|
||||
}
|
||||
self.vertex_buffer.write()?[..vertices.len()].clone_from_slice(vertices);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders all layouts that were previously provided to `prepare`.
|
||||
pub fn render(
|
||||
&mut self,
|
||||
atlas: &TextAtlas,
|
||||
viewport: &mut Viewport,
|
||||
cmd_buf: &mut GfxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
if self.glyph_vertices.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.model_buffer.upload(&atlas.common.gfx)?;
|
||||
|
||||
let descriptor_sets = vec![
|
||||
atlas.color_atlas.image_descriptor.clone(),
|
||||
atlas.mask_atlas.image_descriptor.clone(),
|
||||
viewport.get_text_descriptor(&self.pipeline),
|
||||
self.model_buffer.get_text_descriptor(&self.pipeline),
|
||||
];
|
||||
|
||||
let res = viewport.resolution();
|
||||
|
||||
let pass = self.pipeline.inner.create_pass(
|
||||
[res[0] as _, res[1] as _],
|
||||
self.vertex_buffer.clone(),
|
||||
0..4,
|
||||
0..self.glyph_vertices.len() as u32,
|
||||
descriptor_sets,
|
||||
)?;
|
||||
|
||||
cmd_buf.run_ref(&pass)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(super) enum GlyphonCacheKey {
|
||||
Text(cosmic_text::CacheKey),
|
||||
Custom(CustomGlyphCacheKey),
|
||||
}
|
||||
|
||||
struct GetGlyphImageResult {
|
||||
content_type: ContentType,
|
||||
top: i16,
|
||||
left: i16,
|
||||
width: u16,
|
||||
height: u16,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
struct PrepareGlyphParams<'a> {
|
||||
label_pos: Vec2,
|
||||
x: i32,
|
||||
y: i32,
|
||||
line_y: f32,
|
||||
color: Color,
|
||||
cache_key: GlyphonCacheKey,
|
||||
atlas: &'a mut TextAtlas,
|
||||
cache: &'a mut SwashCache,
|
||||
font_system: &'a mut FontSystem,
|
||||
model_buffer: &'a mut ModelBuffer,
|
||||
transform: &'a Mat4,
|
||||
scale_factor: f32,
|
||||
glyph_scale: f32,
|
||||
bounds_min_x: i32,
|
||||
bounds_min_y: i32,
|
||||
bounds_max_x: i32,
|
||||
bounds_max_y: i32,
|
||||
depth: f32,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepare_glyph(
|
||||
par: PrepareGlyphParams,
|
||||
get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option<GetGlyphImageResult>,
|
||||
) -> anyhow::Result<Option<GlyphVertex>> {
|
||||
let gfx = par.atlas.common.gfx.clone();
|
||||
let details = if let Some(details) = par.atlas.mask_atlas.glyph_cache.get(&par.cache_key) {
|
||||
par.atlas.mask_atlas.glyphs_in_use.insert(par.cache_key);
|
||||
details
|
||||
} else if let Some(details) = par.atlas.color_atlas.glyph_cache.get(&par.cache_key) {
|
||||
par.atlas.color_atlas.glyphs_in_use.insert(par.cache_key);
|
||||
details
|
||||
} else {
|
||||
let Some(image) = (get_glyph_image)(par.cache, par.font_system) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let should_rasterize = image.width > 0 && image.height > 0;
|
||||
|
||||
let (gpu_cache, atlas_id, inner) = if should_rasterize {
|
||||
let mut inner = par.atlas.inner_for_content_mut(image.content_type);
|
||||
|
||||
// Find a position in the packer
|
||||
let allocation = loop {
|
||||
match inner.try_allocate(image.width as usize, image.height as usize) {
|
||||
Some(a) => break a,
|
||||
None => {
|
||||
if !par
|
||||
.atlas
|
||||
.grow(par.font_system, par.cache, image.content_type)?
|
||||
{
|
||||
anyhow::bail!(
|
||||
"Atlas full. atlas: {:?} cache_key: {:?}",
|
||||
image.content_type,
|
||||
par.cache_key
|
||||
);
|
||||
}
|
||||
|
||||
inner = par.atlas.inner_for_content_mut(image.content_type);
|
||||
}
|
||||
}
|
||||
};
|
||||
let atlas_min = allocation.rectangle.min;
|
||||
|
||||
let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
|
||||
cmd_buf.update_image(
|
||||
inner.image_view.image().clone(),
|
||||
&image.data,
|
||||
[atlas_min.x as _, atlas_min.y as _, 0],
|
||||
Some([image.width as _, image.height as _, 1]),
|
||||
)?;
|
||||
|
||||
cmd_buf.build_and_execute_now()?; //TODO: do not wait for fence here
|
||||
|
||||
(
|
||||
GpuCacheStatus::InAtlas {
|
||||
x: atlas_min.x as u16,
|
||||
y: atlas_min.y as u16,
|
||||
content_type: image.content_type,
|
||||
},
|
||||
Some(allocation.id),
|
||||
inner,
|
||||
)
|
||||
} else {
|
||||
let inner = &mut par.atlas.color_atlas;
|
||||
(GpuCacheStatus::SkipRasterization, None, inner)
|
||||
};
|
||||
|
||||
inner.glyphs_in_use.insert(par.cache_key);
|
||||
// Insert the glyph into the cache and return the details reference
|
||||
inner
|
||||
.glyph_cache
|
||||
.get_or_insert(par.cache_key, || GlyphDetails {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
gpu_cache,
|
||||
atlas_id,
|
||||
top: image.top,
|
||||
left: image.left,
|
||||
})
|
||||
};
|
||||
|
||||
let mut x = par.x + details.left as i32;
|
||||
let mut y = (par.line_y * par.scale_factor).round() as i32 + par.y - details.top as i32;
|
||||
|
||||
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
|
||||
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
|
||||
GpuCacheStatus::SkipRasterization => return Ok(None),
|
||||
};
|
||||
|
||||
let mut glyph_width = details.width as i32;
|
||||
let mut glyph_height = details.height as i32;
|
||||
|
||||
// Starts beyond right edge or ends beyond left edge
|
||||
let max_x = x + glyph_width;
|
||||
if x > par.bounds_max_x || max_x < par.bounds_min_x {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Starts beyond bottom edge or ends beyond top edge
|
||||
let max_y = y + glyph_height;
|
||||
if y > par.bounds_max_y || max_y < par.bounds_min_y {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Clip left ege
|
||||
if x < par.bounds_min_x {
|
||||
let right_shift = par.bounds_min_x - x;
|
||||
|
||||
x = par.bounds_min_x;
|
||||
glyph_width = max_x - par.bounds_min_x;
|
||||
atlas_x += right_shift as u16;
|
||||
}
|
||||
|
||||
// Clip right edge
|
||||
if x + glyph_width > par.bounds_max_x {
|
||||
glyph_width = par.bounds_max_x - x;
|
||||
}
|
||||
|
||||
// Clip top edge
|
||||
if y < par.bounds_min_y {
|
||||
let bottom_shift = par.bounds_min_y - y;
|
||||
|
||||
y = par.bounds_min_y;
|
||||
glyph_height = max_y - par.bounds_min_y;
|
||||
atlas_y += bottom_shift as u16;
|
||||
}
|
||||
|
||||
// Clip bottom edge
|
||||
if y + glyph_height > par.bounds_max_y {
|
||||
glyph_height = par.bounds_max_y - y;
|
||||
}
|
||||
|
||||
let mut model = Mat4::IDENTITY;
|
||||
|
||||
// top-left text transform
|
||||
model *= Mat4::from_translation(Vec3::new(
|
||||
par.label_pos.x / par.scale_factor,
|
||||
par.label_pos.y / par.scale_factor,
|
||||
0.0,
|
||||
));
|
||||
|
||||
model *= *par.transform;
|
||||
|
||||
// per-character transform
|
||||
model *= Mat4::from_translation(Vec3::new(
|
||||
((x as f32) - par.label_pos.x) / par.scale_factor,
|
||||
((y as f32) - par.label_pos.y) / par.scale_factor,
|
||||
0.0,
|
||||
));
|
||||
|
||||
model *= glam::Mat4::from_scale(Vec3::new(
|
||||
glyph_width as f32 / par.scale_factor,
|
||||
glyph_height as f32 / par.scale_factor,
|
||||
0.0,
|
||||
));
|
||||
|
||||
let in_model_idx = par.model_buffer.register(&model);
|
||||
|
||||
Ok(Some(GlyphVertex {
|
||||
in_model_idx,
|
||||
in_rect_dim: [glyph_width as u16, glyph_height as u16],
|
||||
in_uv: [atlas_x, atlas_y],
|
||||
in_color: par.color.0,
|
||||
in_content_type: [
|
||||
content_type as u16,
|
||||
0, // unused (TODO!)
|
||||
],
|
||||
depth: par.depth,
|
||||
scale: par.glyph_scale,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user