//! wgpu text pipeline: one instanced draw call per frame. use std::collections::HashMap; use bytemuck::{Pod, Zeroable}; use swash::GlyphId; use wgpu::util::DeviceExt; use crate::{ atlas::{AtlasSlot, GlyphAtlas}, shaper::{FontId, PRIMARY, Shaper}, }; /// One character with its cell-anchored position and color. #[derive(Debug, Clone, Copy)] pub struct CellDraw { pub c: char, /// Top-left of the cell, in pixels. pub x: f32, pub y: f32, pub color: [f32; 4], } /// Solid-color rectangle. Used for cell backgrounds and the cursor. #[derive(Debug, Clone, Copy)] pub struct BgFill { pub x: f32, pub y: f32, pub w: f32, pub h: f32, pub color: [f32; 4], } const ATLAS_SIZE: u32 = 1024; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct Instance { pos: [f32; 2], size: [f32; 2], uv_min: [f32; 2], uv_max: [f32; 2], color: [f32; 4], } #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct Uniforms { screen: [f32; 2], _pad: [f32; 2], } const QUAD: [[f32; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; const INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3]; const SHADER: &str = r" struct Uniforms { screen: vec2, _pad: vec2, }; @group(0) @binding(0) var u: Uniforms; @group(0) @binding(1) var atlas: texture_2d; @group(0) @binding(2) var samp: sampler; struct VOut { @builtin(position) pos: vec4, @location(0) uv: vec2, @location(1) color: vec4, }; @vertex fn vs( @location(0) corner: vec2, @location(1) inst_pos: vec2, @location(2) inst_size: vec2, @location(3) uv_min: vec2, @location(4) uv_max: vec2, @location(5) color: vec4, ) -> VOut { let px = inst_pos + corner * inst_size; let ndc = vec2( (px.x / u.screen.x) * 2.0 - 1.0, 1.0 - (px.y / u.screen.y) * 2.0, ); var out: VOut; out.pos = vec4(ndc, 0.0, 1.0); out.uv = mix(uv_min, uv_max, corner); out.color = color; return out; } @fragment fn fs(in: VOut) -> @location(0) vec4 { let a = textureSample(atlas, samp, in.uv).r; return vec4(in.color.rgb, in.color.a * a); } "; pub struct TextRenderer { pipeline: wgpu::RenderPipeline, bind_group: wgpu::BindGroup, uniforms_buf: wgpu::Buffer, quad_buf: wgpu::Buffer, index_buf: wgpu::Buffer, instance_buf: wgpu::Buffer, instance_cap: u64, atlas: GlyphAtlas, shaper: Shaper, /// Keyed by (font, glyph). A glyph id is only unique inside the font that /// issued it, so the moment a fallback face joins the set, keying on the id /// alone would serve one font's glyph from another's atlas slot. cache: HashMap<(FontId, GlyphId), Option>, /// Per-row cache of "cheap-to-recompute" instance data. Positions get /// computed at draw time from row_idx + cell_advance, so scroll can /// just rotate this vec. row_cache: Vec>, } #[derive(Clone, Copy)] struct CachedGlyph { slot: AtlasSlot, left: i32, top: i32, } /// Per-glyph metadata cached for a live cell. Position is NOT stored — it's /// derived from (row_idx, col, scale, pad) at draw time so the same cache /// entry stays valid across scroll rotations and doesn't need re-computing /// on scale changes. #[derive(Clone, Copy)] struct CachedCell { col: u16, slot: AtlasSlot, left: i32, top: i32, /// Offset within the cell, in pixels. Zero for the one glyph an ordinary /// cell draws; non-zero only for the marks of a cluster, which the shaper /// positions against their base rather than against the cell. dx: f32, dy: f32, color: [f32; 4], } /// What one cell draws. /// /// Almost always a single character looked up in the font's charmap, which is /// the path worth keeping cheap. A cell carrying combining marks is shaped /// instead, because where a mark sits over its base is the font's business and /// GPOS is how it says so. pub enum CellText { Char(char), Cluster(String), } impl TextRenderer { pub fn new( device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat, font_data: Vec, font_px: f32, // The grid's step, in physical pixels. Furniture is snapped to it, so // it has to be the cell the caller actually lays out with rather than // one this constructor measures for itself. cell: crate::CellMetrics, // Where on the face's weight axis to draw. The bundled face is variable // and its own default is the light end of the axis, so this is named // rather than taken from the file. weight: f32, ) -> anyhow::Result { let atlas = GlyphAtlas::new(device, queue, ATLAS_SIZE); let shaper = Shaper::new(font_data, font_px, cell, weight)?; let uniforms_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("shop.text.uniforms"), contents: bytemuck::bytes_of(&Uniforms { screen: [1.0, 1.0], _pad: [0.0, 0.0], }), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); let quad_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("shop.text.quad"), contents: bytemuck::cast_slice(&QUAD), usage: wgpu::BufferUsages::VERTEX, }); let index_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("shop.text.index"), contents: bytemuck::cast_slice(&INDICES), usage: wgpu::BufferUsages::INDEX, }); let instance_cap: u64 = 256; let instance_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("shop.text.instances"), size: instance_cap * std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("shop.text.sampler"), mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, ..Default::default() }); let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("shop.text.bgl"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("shop.text.bg"), layout: &bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: uniforms_buf.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&atlas.view), }, wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&sampler), }, ], }); let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("shop.text.shader"), source: wgpu::ShaderSource::Wgsl(SHADER.into()), }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("shop.text.pl"), bind_group_layouts: &[Some(&bind_group_layout)], immediate_size: 0, }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("shop.text.pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &module, entry_point: Some("vs"), compilation_options: wgpu::PipelineCompilationOptions::default(), buffers: &[ Some(wgpu::VertexBufferLayout { array_stride: 8, step_mode: wgpu::VertexStepMode::Vertex, attributes: &wgpu::vertex_attr_array![0 => Float32x2], }), Some(wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &wgpu::vertex_attr_array![ 1 => Float32x2, 2 => Float32x2, 3 => Float32x2, 4 => Float32x2, 5 => Float32x4, ], }), ], }, fragment: Some(wgpu::FragmentState { module: &module, entry_point: Some("fs"), compilation_options: wgpu::PipelineCompilationOptions::default(), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }); Ok(Self { pipeline, bind_group, uniforms_buf, quad_buf, index_buf, instance_buf, instance_cap, atlas, shaper, cache: HashMap::new(), row_cache: Vec::new(), }) } /// Ensure the per-row cache has `rows` slots. pub fn ensure_rows(&mut self, rows: u16) { self.row_cache.resize(rows as usize, Vec::new()); } /// Wipe the per-row cache — alt-screen swap or full grid resize. pub fn clear_rows(&mut self) { for row in &mut self.row_cache { row.clear(); } } /// Rotate the per-row cache to reflect a fullscreen scroll. Positive = /// scrolled up (contents move up N rows, blank appears at bottom). pub fn scroll(&mut self, delta: i16) { if delta == 0 || self.row_cache.is_empty() { return; } let n = delta.unsigned_abs() as usize; let n = n.min(self.row_cache.len()); if delta > 0 { self.row_cache.rotate_left(n); } else { self.row_cache.rotate_right(n); } } /// Replace one row's cached glyph batch. Blanks pre-filtered by caller. pub fn update_row( &mut self, queue: &wgpu::Queue, row: u16, cells: impl IntoIterator, ) { let Some(slot) = self.row_cache.get_mut(row as usize) else { return; }; slot.clear(); let atlas = &mut self.atlas; let cache = &mut self.cache; let shaper = &mut self.shaper; // One glyph id to an atlas slot, memoized. The id is what the cache is // keyed on either way, so a mark rasterized for one base is reused for // every other base it ever sits on. let mut push = |slot: &mut Vec, shaper: &mut Shaper, font: FontId, id, col, dx: f32, dy: f32, color| { let cached = match cache.get(&(font, id)).copied() { Some(v) => v, None => { let entry = shaper.rasterize(font, id).and_then(|r| { atlas .upload(queue, r.width, r.height, &r.bitmap) .map(|s| CachedGlyph { slot: s, left: r.placement_left, top: r.placement_top, }) }); cache.insert((font, id), entry); entry } }; if let Some(g) = cached && g.slot.px[0] > 0 && g.slot.px[1] > 0 { slot.push(CachedCell { col, slot: g.slot, left: g.left, top: g.top, dx, dy, color, }); } }; for (col, text, color) in cells { match text { CellText::Char(c) => { let (font, id) = shaper.glyph_id_for(c); push(slot, shaper, font, id, col, 0.0, 0.0, color); } CellText::Cluster(s) => { // The pen DOES advance across the cluster, and the marks // still land on the base. Measured on the face bundled at // the time, IosevkaTerm, and it is a property of how fonts // are built rather than of that face: // a combining glyph carries a full cell of advance and its // ink hangs off the LEFT of its origin (acute -5px for a // 3px mark, diaeresis -7px for a 6px one). The font is // built for a pen that has already stepped past the base, // and the negative bearing is what pulls the mark back // over it, centred. // // Anchoring every glyph at the cell origin instead — which // looks right, and which this did briefly — draws each mark // a whole cell to the LEFT, over the previous character. // Do not "fix" it back without looking at a window first. // // The two other shapes this can take are handled by the // same line: where the shaper composes the pair into one // precomposed glyph (Quasi Mono maps U+00E9 to a glyph of // its own) there is // nothing after the base to place, and where a font // positions marks by GPOS the mark's advance is zero and // its offsets do the work. // // Shaped against the font that has the BASE. A cluster is // one character plus marks on it, so it belongs to one // font; shaping it against the bundled font when the base // came from a fallback would look every glyph up in the // wrong charmap and return notdef for the lot. let base = s.chars().next().unwrap_or(' '); let (font, _) = shaper.glyph_id_for(base); let mut pen_x = 0.0; for g in shaper.shape(font, &s) { push( slot, shaper, font, g.id, col, pen_x + g.x_offset, g.y_offset, color, ); pen_x += g.advance; } } } } } /// Draw one frame from cached per-row glyphs + caller-supplied fills /// (bg, underline, cursor). Fills are cheap to fully rebuild each frame. // wgpu handles plus the frame geometry. Grouping them into a struct would // add a type that exists only to satisfy the lint. #[allow(clippy::too_many_arguments)] pub fn draw_cached( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, view: &wgpu::TextureView, encoder: &mut wgpu::CommandEncoder, fills: &[BgFill], pad_x: f32, pad_y: f32, cell_w: f32, cell_h: f32, ) { let baseline = self.shaper.baseline(); let glyph_count: usize = self.row_cache.iter().map(Vec::len).sum(); let mut instances: Vec = Vec::with_capacity(fills.len() + glyph_count); let solid_uv = self.atlas.solid_uv; for fill in fills { instances.push(Instance { pos: [fill.x, fill.y], size: [fill.w, fill.h], uv_min: solid_uv, uv_max: solid_uv, color: fill.color, }); } for (row_idx, row) in self.row_cache.iter().enumerate() { let row_y = pad_y + row_idx as f32 * cell_h; for cell in row { let cell_x = pad_x + cell.col as f32 * cell_w; instances.push(Instance { pos: [ cell_x + cell.left as f32 + cell.dx, row_y + baseline - cell.top as f32 - cell.dy, ], size: [cell.slot.px[0] as f32, cell.slot.px[1] as f32], uv_min: cell.slot.uv_min, uv_max: cell.slot.uv_max, color: cell.color, }); } } if instances.is_empty() { return; } let needed = instances.len() as u64; if needed > self.instance_cap { let new_cap = needed.next_power_of_two(); self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("shop.text.instances"), size: new_cap * std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); self.instance_cap = new_cap; } queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances)); let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("shop.text.cached"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, })], ..Default::default() }); pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); pass.set_vertex_buffer(0, self.quad_buf.slice(..)); pass.set_vertex_buffer(1, self.instance_buf.slice(..)); pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16); pass.draw_indexed(0..6, 0, 0..instances.len() as u32); } /// Draw a frame: solid-color fills first (bg + cursor), glyph cells second. /// All emitted as a single instanced draw call using the atlas's reserved /// solid-white block for the fill quads. pub fn draw_frame( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, view: &wgpu::TextureView, encoder: &mut wgpu::CommandEncoder, fills: &[BgFill], cells: &[CellDraw], ) { let baseline = self.shaper.baseline(); let solid_uv = self.atlas.solid_uv; let mut instances: Vec = Vec::with_capacity(fills.len() + cells.len()); // Fills go first so alpha blending stacks glyphs on top. for fill in fills { instances.push(Instance { pos: [fill.x, fill.y], size: [fill.w, fill.h], uv_min: solid_uv, uv_max: solid_uv, color: fill.color, }); } for cell in cells { let (font, glyph_id) = self.shaper.glyph_id_for(cell.c); let cached = match self.cache.get(&(font, glyph_id)).copied() { Some(v) => v, None => { let entry = self.shaper.rasterize(font, glyph_id).and_then(|r| { self.atlas .upload(queue, r.width, r.height, &r.bitmap) .map(|slot| CachedGlyph { slot, left: r.placement_left, top: r.placement_top, }) }); self.cache.insert((font, glyph_id), entry); entry } }; if let Some(c) = cached && c.slot.px[0] > 0 && c.slot.px[1] > 0 { instances.push(Instance { pos: [cell.x + c.left as f32, cell.y + baseline - c.top as f32], size: [c.slot.px[0] as f32, c.slot.px[1] as f32], uv_min: c.slot.uv_min, uv_max: c.slot.uv_max, color: cell.color, }); } } if instances.is_empty() { return; } let needed = instances.len() as u64; if needed > self.instance_cap { let new_cap = needed.next_power_of_two(); self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("shop.text.instances"), size: new_cap * std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); self.instance_cap = new_cap; } queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances)); let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("shop.text.frame"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, })], ..Default::default() }); pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); pass.set_vertex_buffer(0, self.quad_buf.slice(..)); pass.set_vertex_buffer(1, self.instance_buf.slice(..)); pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16); pass.draw_indexed(0..6, 0, 0..instances.len() as u32); } /// Draw a batch of cell-anchored characters in one instanced draw call. /// Uses the font's charmap for glyph lookup (no shaping), so ligatures do /// not activate and per-glyph advance is ignored — each character sits at /// its passed-in cell coordinate. Missing glyphs are silently skipped. pub fn draw_cells( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, view: &wgpu::TextureView, encoder: &mut wgpu::CommandEncoder, cells: &[CellDraw], ) { let baseline = self.shaper.baseline(); let mut instances: Vec = Vec::with_capacity(cells.len()); for cell in cells { let (font, glyph_id) = self.shaper.glyph_id_for(cell.c); let cached = match self.cache.get(&(font, glyph_id)).copied() { Some(v) => v, None => { let entry = self.shaper.rasterize(font, glyph_id).and_then(|r| { self.atlas .upload(queue, r.width, r.height, &r.bitmap) .map(|slot| CachedGlyph { slot, left: r.placement_left, top: r.placement_top, }) }); self.cache.insert((font, glyph_id), entry); entry } }; if let Some(c) = cached && c.slot.px[0] > 0 && c.slot.px[1] > 0 { let gx = cell.x + c.left as f32; let gy = cell.y + baseline - c.top as f32; instances.push(Instance { pos: [gx, gy], size: [c.slot.px[0] as f32, c.slot.px[1] as f32], uv_min: c.slot.uv_min, uv_max: c.slot.uv_max, color: cell.color, }); } } if instances.is_empty() { return; } let needed = instances.len() as u64; if needed > self.instance_cap { let new_cap = needed.next_power_of_two(); self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("shop.text.instances"), size: new_cap * std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); self.instance_cap = new_cap; } queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances)); let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("shop.text.cells"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, })], ..Default::default() }); pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); pass.set_vertex_buffer(0, self.quad_buf.slice(..)); pass.set_vertex_buffer(1, self.instance_buf.slice(..)); pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16); pass.draw_indexed(0..6, 0, 0..instances.len() as u32); } pub fn resize(&self, queue: &wgpu::Queue, width: u32, height: u32) { let u = Uniforms { screen: [width as f32, height as f32], _pad: [0.0, 0.0], }; queue.write_buffer(&self.uniforms_buf, 0, bytemuck::bytes_of(&u)); } /// Shape `text`, rasterize any missing glyphs, and draw the resulting /// quads. `x`/`y` is the pen origin in pixels (top-left convention, with /// the baseline placed at `y + baseline`). // wgpu handles plus pen origin and colour. See `draw_cached`. #[allow(clippy::too_many_arguments)] pub fn draw( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, view: &wgpu::TextureView, encoder: &mut wgpu::CommandEncoder, text: &str, x: f32, y: f32, color: [f32; 4], ) { let baseline = self.shaper.baseline(); let baseline_y = y + baseline; // Chrome text, not grid text: drawn from the bundled font, which is // the one whose metrics the surrounding layout is built on. let glyphs = self.shaper.shape(PRIMARY, text); let mut pen_x = x; let mut instances: Vec = Vec::with_capacity(glyphs.len()); for g in glyphs { let cached = match self.cache.get(&(PRIMARY, g.id)).copied() { Some(v) => v, None => { let entry = self.shaper.rasterize(PRIMARY, g.id).and_then(|r| { self.atlas .upload(queue, r.width, r.height, &r.bitmap) .map(|slot| CachedGlyph { slot, left: r.placement_left, top: r.placement_top, }) }); self.cache.insert((PRIMARY, g.id), entry); entry } }; if let Some(c) = cached && c.slot.px[0] > 0 && c.slot.px[1] > 0 { let gx = pen_x + g.x_offset + c.left as f32; let gy = baseline_y - g.y_offset - c.top as f32; instances.push(Instance { pos: [gx, gy], size: [c.slot.px[0] as f32, c.slot.px[1] as f32], uv_min: c.slot.uv_min, uv_max: c.slot.uv_max, color, }); } pen_x += g.advance; } if instances.is_empty() { return; } let needed = instances.len() as u64; if needed > self.instance_cap { let new_cap = needed.next_power_of_two(); self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("shop.text.instances"), size: new_cap * std::mem::size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); self.instance_cap = new_cap; } queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances)); let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("shop.text.pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, })], ..Default::default() }); pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); pass.set_vertex_buffer(0, self.quad_buf.slice(..)); pass.set_vertex_buffer(1, self.instance_buf.slice(..)); pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16); pass.draw_indexed(0..6, 0, 0..instances.len() as u32); } }