Compare commits

...

1 Commits

Author SHA1 Message Date
983f892895 First attempt at GPU-generated text 2022-11-18 23:39:00 +01:00
9 changed files with 547 additions and 29 deletions

227
Cargo.lock generated
View File

@ -147,6 +147,7 @@ dependencies = [
"cgmath",
"chrono",
"fern",
"font-kit",
"image",
"log",
"pollster",
@ -206,6 +207,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "cmake"
version = "0.1.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c"
dependencies = [
"cc",
]
[[package]]
name = "cocoa"
version = "0.24.1"
@ -264,6 +274,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "const-cstr"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6"
[[package]]
name = "copyless"
version = "0.1.5"
@ -339,6 +355,18 @@ dependencies = [
"libc",
]
[[package]]
name = "core-text"
version = "19.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
dependencies = [
"core-foundation 0.9.3",
"core-graphics 0.22.3",
"foreign-types",
"libc",
]
[[package]]
name = "core-video-sys"
version = "0.1.4"
@ -457,6 +485,27 @@ dependencies = [
"syn",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if 1.0.0",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@ -478,6 +527,18 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "dwrote"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
dependencies = [
"lazy_static",
"libc",
"winapi",
"wio",
]
[[package]]
name = "fern"
version = "0.6.1"
@ -498,12 +559,43 @@ dependencies = [
"miniz_oxide 0.5.4",
]
[[package]]
name = "float-ord"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "font-kit"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21fe28504d371085fae9ac7a3450f0b289ab71e07c8e57baa3fb68b9e57d6ce5"
dependencies = [
"bitflags",
"byteorder",
"core-foundation 0.9.3",
"core-graphics 0.22.3",
"core-text",
"dirs-next",
"dwrote",
"float-ord",
"freetype",
"lazy_static",
"libc",
"log",
"pathfinder_geometry",
"pathfinder_simd",
"walkdir",
"winapi",
"yeslogic-fontconfig-sys",
]
[[package]]
name = "foreign-types"
version = "0.3.2"
@ -519,6 +611,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "freetype"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6"
dependencies = [
"freetype-sys",
"libc",
]
[[package]]
name = "freetype-sys"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a"
dependencies = [
"cmake",
"libc",
"pkg-config",
]
[[package]]
name = "fxhash"
version = "0.2.1"
@ -1084,12 +1197,41 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "pathfinder_geometry"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3"
dependencies = [
"log",
"pathfinder_simd",
]
[[package]]
name = "pathfinder_simd"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff"
dependencies = [
"rustc_version",
]
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8"
dependencies = [
"thiserror",
"ucd-trie",
]
[[package]]
name = "pkg-config"
version = "0.3.26"
@ -1173,6 +1315,17 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom",
"redox_syscall",
"thiserror",
]
[[package]]
name = "renderdoc-sys"
version = "0.7.1"
@ -1185,6 +1338,24 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
dependencies = [
"semver",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@ -1203,6 +1374,24 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]]
name = "serde"
version = "1.0.147"
@ -1319,6 +1508,12 @@ dependencies = [
"serde",
]
[[package]]
name = "ucd-trie"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unicode-ident"
version = "1.0.5"
@ -1343,6 +1538,17 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -1719,6 +1925,15 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "wio"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5"
dependencies = [
"winapi",
]
[[package]]
name = "x11-dl"
version = "2.20.0"
@ -1744,3 +1959,15 @@ name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "yeslogic-fontconfig-sys"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386"
dependencies = [
"const-cstr",
"dlib",
"once_cell",
"pkg-config",
]

View File

@ -13,6 +13,7 @@ fern = { version = "^0.6.1", features = ["colored"] }
bitflags = "^1.3.2"
cgmath = { version = "^0.18.0", features = ["bytemuck"] }
pollster = "^0.2.5"
font-kit = "^0.11.0"
# surface creation
winit = "^0.26.1"

View File

@ -80,8 +80,12 @@ impl Canvas {
)
}
pub fn create_text_sprite(&mut self, _size: Size, _scale: f32) -> TextSprite {
unimplemented!();
pub fn create_text_sprite(&mut self, text: &'static str, size: Size) -> TextSprite {
TextSprite::new(
text,
size,
&self.renderer,
)
}
pub fn create_shape_sprite(&mut self) -> ShapeSprite {

View File

@ -5,7 +5,7 @@ use canvas::{
Application,
Canvas,
texture::TextureHandle,
sprite::TextureSprite,
sprite::{TextureSprite, TextSprite},
utils::Position,
};
@ -40,6 +40,7 @@ struct ExampleState {
pub texture: TextureHandle,
pub tex_sprite: TextureSprite,
pub sub_sprite: TextureSprite,
pub txt_sprite: TextSprite,
pub last_instant: Instant,
pub last_offset: u32,
pub last_pos: Position,
@ -62,6 +63,8 @@ impl Application<ExampleState> for ExampleApp {
let mut sub_sprite = canvas.create_texture_sprite(Size {w: 200, h: 200});
sub_sprite.set_texture(texture.clone(), Some(Position {x: 350, y: 0}), 1.0);
let txt_sprite = canvas.create_text_sprite(".", Size {w: 10, h: 10});
canvas.clear();
canvas.update();
@ -71,6 +74,7 @@ impl Application<ExampleState> for ExampleApp {
texture,
tex_sprite,
sub_sprite,
txt_sprite,
last_instant,
last_offset: 0,
last_pos: Position::origin(),
@ -81,21 +85,21 @@ impl Application<ExampleState> for ExampleApp {
fn tick(state: &mut ExampleState, canvas: &mut Canvas) -> Result<(), &'static str> {
use canvas::sprite::Sprite;
let now = Instant::now();
let elapsed = now.duration_since(state.last_instant).as_millis();
state.last_instant = now;
debug!("frame time: {}ms", elapsed);
//let now = Instant::now();
//let elapsed = now.duration_since(state.last_instant).as_millis();
//state.last_instant = now;
//debug!("frame time: {}ms", elapsed);
//state.sub_sprite.for_each(|pix| unsafe {pix.flat = pix.flat.wrapping_add(1);});
////state.sub_sprite.for_each(|pix| unsafe {pix.flat = pix.flat.wrapping_add(1);});
state.last_offset += 1;
state.last_pos.y += 1;
state.last_rot += 1.0;
state.tex_sprite.set_texture(state.texture.clone(), None, state.last_rot/100.0);
state.sub_sprite.set_texture(state.texture.clone(),
Some(Position {x: state.last_offset, y: 0}), 1.0);
state.sub_sprite.set_position(state.last_pos);
state.sub_sprite.set_rotation(state.last_rot);
//state.last_offset += 1;
//state.last_pos.y += 1;
//state.last_rot += 1.0;
//state.tex_sprite.set_texture(state.texture.clone(), None, state.last_rot/100.0);
//state.sub_sprite.set_texture(state.texture.clone(),
// Some(Position {x: state.last_offset, y: 0}), 1.0);
//state.sub_sprite.set_position(state.last_pos);
//state.sub_sprite.set_rotation(state.last_rot);
//state.sub_sprite.set_scale(state.last_rot/1000.0);
@ -142,7 +146,7 @@ impl Application<ExampleState> for ExampleApp {
//canvas.draw(&pix_sprite);
//// floating text
//let mut txt_sprite = canvas.create_text_sprite(Size {w: 10, h: 10}, 5.0);
//let mut txt_sprite = canvas.create_text_sprite(".", Size {w: 10, h: 10});
//txt_sprite.set_text("text");
//txt_sprite.set_color(Color::BLACK);
@ -154,8 +158,10 @@ impl Application<ExampleState> for ExampleApp {
//canvas.draw(&txt_sprite);
canvas.draw(&mut state.tex_sprite);
canvas.draw(&mut state.sub_sprite);
//canvas.draw(&mut state.tex_sprite);
//canvas.draw(&mut state.sub_sprite);
canvas.clear();
canvas.draw(&mut state.txt_sprite);
canvas.update();
Ok(())
@ -167,6 +173,6 @@ fn main() -> Result<(), &'static str> {
setup_logger()
.map_err(|_| "Failed to setup logger")?;
canvas::run_canvas("vk_example", Size {w: 1280, h: 720}, ExampleApp {});
canvas::run_canvas("vk_example", Size {w: 1920, h: 1080}, ExampleApp {});
}

View File

@ -64,11 +64,18 @@ impl WgpuRenderer {
None, // Trace path
).await.unwrap();
debug!("formats: {:#?}", surface.get_supported_formats(&adapter));
let format = *surface.get_supported_formats(&adapter)
.into_iter()
.filter(|f| f == &wgpu::TextureFormat::Rgba8UnormSrgb)
.collect::<Vec<wgpu::TextureFormat>>()
.first()
.ok_or_else(|| "Current adapter incompatible with required surface format")?;
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
//first format in the list is preferred
format: *surface.get_supported_formats(&adapter).first()
.ok_or_else(|| { "Surface is incompatible with current adapter" })?,
format,
width: size.w,
height: size.h,
present_mode: wgpu::PresentMode::Fifo,

View File

@ -0,0 +1,38 @@
// Vertex shader
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) tex_coords: vec2<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@group(0)@binding(0)
var<uniform> model_matrix: mat3x3<f32>;
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.clip_position
= vec4<f32>(model_matrix * vec3<f32>(model.position, 1.0), 1.0);
return out;
}
// Fragment shader
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
var pos = in.tex_coords;
if abs(pos.x * pos.x - pos.y) < 0.8 {
return vec4<f32>(0.0, 1.0, 0.0, 1.0);
} else {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
}

View File

@ -4,25 +4,141 @@ use log::{debug, error, info, trace, warn};
//--Internal imports--------------------------------------------------------------------------------
use crate::{
renderer::WgpuRenderer,
renderer::{
utils::Mesh,
WgpuRenderer, ModelMatrix
},
sprite::Sprite,
utils::{Pixel, Position, Size},
};
//--External imports--------------------------------------------------------------------------------
use cgmath::Matrix4;
use wgpu;
use std::cell::RefCell;
thread_local!(static PIPELINE : RefCell<Option<wgpu::RenderPipeline>> = RefCell::new(None));
//--TextSprite struct-------------------------------------------------------------------------------
#[allow(dead_code)]
pub struct TextSprite {
matrix: Matrix4<f32>,
matrix: ModelMatrix,
mesh: Mesh<TextVertex, 100, 6>,
size: Size,
text: &'static str, //TODO: temporary
}
impl TextSprite {
pub fn new(text: &'static str, size: Size, renderer: &WgpuRenderer) -> Self {
use font_kit::{
family_name::FamilyName,
source::SystemSource,
properties::Properties,
outline::{OutlineBuilder, Outline},
};
// initialize pipeline if needed
PIPELINE.with(|cell| {
if cell.borrow().is_none() {
cell.replace(Some(initialize_pipeline(renderer)));
}
});
let font = SystemSource::new()
.select_best_match(&[FamilyName::SansSerif], &Properties::new())
.unwrap()
.load()
.unwrap();
let mut outline_builder = OutlineBuilder::new();
let outlines : Vec<Outline> = text.chars().map(|char| {
use font_kit::hinting::HintingOptions;
let glyph = font.glyph_for_char(char).unwrap();
let _ = font.outline(glyph, HintingOptions::None, &mut outline_builder).unwrap();
outline_builder.take_outline()
}).collect();
//TODO improve that
let mut mesh = Vec::<TextVertex>::new();
for outline in &outlines {
for contour in &outline.contours {
for i in 0..(contour.positions.len() - 1) {
use font_kit::outline::PointFlags;
match contour.flags[i] {
PointFlags::CONTROL_POINT_0 => (),
_ => {
mesh.push(TextVertex {
position: [
contour.positions[i].x(),
contour.positions[i].y(),
],
tex_coords: [0.0, 0.5],
});
match contour.flags[i+1] {
PointFlags::CONTROL_POINT_0 => {
mesh.push(TextVertex {
position: [
contour.positions[i+1].x(),
contour.positions[i+1].y(),
],
tex_coords: [0.0, 1.0],
});
mesh.push(TextVertex {
position: [
contour.positions[i+2].x(),
contour.positions[i+2].y(),
],
tex_coords: [1.0, 1.0],
});
},
_ => {
mesh.push(TextVertex {
position: [
contour.positions[i].x(),
contour.positions[i].y(),
],
tex_coords: [0.0, 1.0],
});
mesh.push(TextVertex {
position: [
contour.positions[i+1].x(),
contour.positions[i+1].y(),
],
tex_coords: [1.0, 1.0],
});
},
};
},
}
}
}
}
debug!("outlines: {:#?}", outlines);
debug!("mesh: {:#?}", mesh);
let mut sprite = Self {
matrix: ModelMatrix::default(renderer),
mesh: Mesh::new(&renderer.device),
size,
text,
};
sprite.mesh.set_vertices(renderer, &mesh[..]);
sprite
}
pub fn set_text(&mut self, _text: &'static str) {
todo!();
}
@ -50,8 +166,127 @@ impl Sprite for TextSprite {
todo!();
}
fn render(&mut self, _renderer: &mut WgpuRenderer) {
todo!();
fn render(&mut self, renderer: &mut WgpuRenderer) {
// create command encoder
let mut encoder = renderer.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
PIPELINE.with(|pipeline| {
let pipeline = pipeline.borrow();
let view = renderer.create_texture_view();
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load, store: true, },
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(pipeline.as_ref().unwrap());
render_pass.set_vertex_buffer(0, self.mesh.get_vertex_buffer_slice());
//render_pass.set_index_buffer(self.mesh.get_index_buffer_slice(),
// wgpu::IndexFormat::Uint16);
render_pass.set_bind_group(0,
self.matrix.get_uniform().get_bind_group(&renderer.queue),
&[]);
render_pass.draw(0..100, 0..1);
//render_pass.draw_indexed(0..6, 0, 0..1);
drop(render_pass);
});
renderer.queue.submit(std::iter::once(encoder.finish()));
}
}
//--Pipeline initialization-------------------------------------------------------------------------
fn initialize_pipeline(renderer: &WgpuRenderer) -> wgpu::RenderPipeline {
let shader = renderer.device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/text.wgsl").into()),
});
let render_pipeline_layout =
renderer.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("texture render pipeline layout"),
bind_group_layouts: &[
&renderer.matrix_layout,
],
push_constant_ranges: &[],
});
renderer.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("texture render pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[
TextVertex::layout(),
],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: renderer.config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,//Some(wgpu::Face::Back),
// Setting this to anything other than Fill requires
// Features::NON_FILL_POLYGON_MODE
polygon_mode: wgpu::PolygonMode::Fill,
// Requires Features::DEPTH_CLIP_CONTROL
unclipped_depth: false,
// Requires Features::CONSERVATIVE_RASTERIZATION
conservative: false,
},
depth_stencil: None,
// multisampling, we don't need it
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
})
}
//--TextVertex struct-------------------------------------------------------------------------------
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TextVertex {
pub position: [f32; 2],
pub tex_coords: [f32; 2],
}
impl TextVertex {
const ATTRIBS: [wgpu::VertexAttribute; 2] =
wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2];
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<TextVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS,
}
}
}