diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/graphics.rs | 61 | ||||
| -rw-r--r-- | src/graphics/font.rs | 238 | ||||
| -rw-r--r-- | src/graphics/texture.rs | 10 | ||||
| -rw-r--r-- | src/main.rs | 2 |
4 files changed, 278 insertions, 33 deletions
diff --git a/src/graphics.rs b/src/graphics.rs index bbe07df..2b96a7a 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -2,9 +2,10 @@ use std::{borrow::Cow, sync::Arc}; use wgpu::*; use winit::{dpi::PhysicalSize, event_loop::OwnedDisplayHandle, window::Window}; +mod font; mod texture; -use texture::TextureWrapper; +use font::CharAtlas; pub enum AcquireSurfaceTextureResult { Ok(SurfaceTexture), @@ -21,7 +22,7 @@ pub struct Renderer { present_mode: PresentMode, pipeline: RenderPipeline, - font_tex: TextureWrapper, + char_atlas: CharAtlas, fixed_bind_group: BindGroup, } @@ -31,10 +32,10 @@ impl Renderer { window: Arc<Window>, ) -> Result<Self, String> { let size = window.inner_size(); + let scale_factor = window.scale_factor() as f32; let instance = Instance::new(InstanceDescriptor::new_with_display_handle(display)); let surface = instance.create_surface(window).map_err(|e| e.to_string())?; - let adapter = instance .request_adapter(&RequestAdapterOptions { compatible_surface: Some(&surface), @@ -53,20 +54,7 @@ impl Renderer { } let (surface_format, view_format) = decide_surface_and_view_format(&adapter, &surface_cap); - - // REVIEW: 高負荷なので素直にFIFOにして垂直同期で回す方が良いかもしれない。 - let present_mode = if surface_cap.present_modes.contains(&PresentMode::Mailbox) { - // 垂直同期を待つフレーム上書きモード - PresentMode::Mailbox - } else if surface_cap.present_modes.contains(&PresentMode::Immediate) { - // 垂直同期を待たないフレーム上書きモード - PresentMode::Immediate - } else { - // 垂直同期を待つフレーム保守モード - // NOTE: 殆どすべての環境でサポートされていることを期待して良い。 - // https://wgpu.rs/doc/wgpu/enum.PresentMode.html - PresentMode::Fifo - }; + let present_mode = decide_presentation_mode(&surface_cap); let shader = device.create_shader_module(ShaderModuleDescriptor { label: None, @@ -112,7 +100,11 @@ impl Renderer { module: &shader, entry_point: Some("fs_main"), compilation_options: Default::default(), - targets: &[Some(view_format.into())], + targets: &[Some(ColorTargetState { + format: view_format, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], }), primitive: PrimitiveState { topology: PrimitiveTopology::TriangleStrip, @@ -137,13 +129,7 @@ impl Renderer { ..Default::default() }); // NOTE: 文字テクスチャアトラスは使い回すので、ここで1個だけ作成すれば良い。 - let font_tex = TextureWrapper::new( - &device, - 128, - 128, - TextureFormat::R8Unorm, - TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, - ); + let char_atlas = CharAtlas::new(scale_factor, &device); let fixed_bind_group = device.create_bind_group(&BindGroupDescriptor { label: None, layout: &fixed_bind_group_layout, @@ -154,7 +140,7 @@ impl Renderer { }, BindGroupEntry { binding: 1, - resource: BindingResource::TextureView(&font_tex.view), + resource: BindingResource::TextureView(&char_atlas.texture.view), }, ], }); @@ -167,7 +153,7 @@ impl Renderer { view_format, present_mode, pipeline, - font_tex, + char_atlas, fixed_bind_group, }; @@ -199,7 +185,7 @@ impl Renderer { } } - pub fn render(&self, window: &Window, current_texture: SurfaceTexture) { + pub fn render(&mut self, window: &Window, current_texture: SurfaceTexture) { let surface_texture_view = current_texture.texture.create_view(&TextureViewDescriptor { format: Some(self.view_format), ..Default::default() @@ -289,6 +275,7 @@ fn decide_surface_and_view_format( // sRGBがサポートされていない・かつテクスチャとビューのフォーマットが一致しなくて良いなら // テクスチャを`formats[0]`・ビューをsRGB + // // NOTE: ビューのフォーマットに従って色変換が行われるので、 // テクスチャの実際のフォーマットが違うだけでsRGBと同様の結果になる。 if can_srgb_view { @@ -296,9 +283,27 @@ fn decide_surface_and_view_format( } // sRGBがサポートされていない・かつテクスチャとビューのフォーマットが一致する必要があるなら // テクスチャもビューも`formats[0]` + // // NOTE: 色変換が行われないので中間色が暗くなる。 // シェーダで自力補正してやれば解決するが、まあ余力があったらね。 else { (format, format) } } + +fn decide_presentation_mode(cap: &SurfaceCapabilities) -> PresentMode { + // REVIEW: 高負荷なので素直にFIFOにして垂直同期で回す方が良いかもしれない。 + if cap.present_modes.contains(&PresentMode::Mailbox) { + // 垂直同期を待つフレーム上書きモード + PresentMode::Mailbox + } else if cap.present_modes.contains(&PresentMode::Immediate) { + // 垂直同期を待たないフレーム上書きモード + PresentMode::Immediate + } else { + // 垂直同期を待つフレーム保守モード + // + // NOTE: 殆どすべての環境でサポートされていることを期待して良い。 + // https://wgpu.rs/doc/wgpu/enum.PresentMode.html + PresentMode::Fifo + } +} diff --git a/src/graphics/font.rs b/src/graphics/font.rs new file mode 100644 index 0000000..d05682d --- /dev/null +++ b/src/graphics/font.rs @@ -0,0 +1,238 @@ +use super::texture::TextureWrapper; +use ab_glyph::*; +use etagere::{AllocId, AtlasAllocator}; +use glam::Vec4; +use lru::LruCache; +use std::sync::LazyLock; +use wgpu::*; + +static FONT: LazyLock<FontRef<'static>> = LazyLock::new(|| { + FontRef::try_from_slice(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/KazukiReiwa-Regular.ttf" + ))) + .expect("fatal: failed to load font") +}); + +/// スケールされたフォントのトップ-ベースライン間を取得するメソッド +pub fn ascent(scale: u32) -> f32 { + FONT.as_scaled(scale as f32).ascent() +} + +/// スケールされたフォントの行間を取得するメソッド +pub fn line_gap(scale: u32) -> f32 { + FONT.as_scaled(scale as f32).line_gap() +} + +/// スケールされたフォントのカーニング量を取得するメソッド +pub fn kern(scale: u32, first: char, second: char) -> f32 { + let first = FONT.glyph_id(first); + let second = FONT.glyph_id(second); + FONT.as_scaled(scale as f32).kern(first, second) +} + +const WIDTH: u32 = 128; +const HEIGHT: u32 = WIDTH; + +// NOTE: macOSではswizzleをゼロコストで使えないのでRGBAにする。 +const FORMAT: TextureFormat = if cfg!(target_os = "macos") { + TextureFormat::Rgba8Unorm +} else { + TextureFormat::R8Unorm +}; + +/// 1文字テクスチャの1辺におけるマージン +/// +/// NOTE: 境界で隣の文字のピクセルを含まないように、テクスチャ上で四辺に2pxのマージンを取る。 +/// 文字情報(UV)はこのマージンを除いた領域を指定する。 +const MARGIN: usize = 2; +/// 1文字テクスチャの1方向におけるマージン +const MARGIN_AXIS: usize = MARGIN * 2; + +#[derive(Clone, Copy)] +pub enum CharInfo { + Rasterizable { + alloc_id: AllocId, + width: f32, + height: f32, + x_offset: f32, + y_offset: f32, + advance: f32, + uv: Vec4, + }, + Unrasterizable { + advance: f32, + }, +} + +pub struct CharAtlas { + /// ウィンドウの倍率 + /// + /// 物理解像度と論理解像度の差を考慮するため、この倍率に従ってテクスチャアトラス全体をスケールする。 + /// + /// NOTE: 実行中に変化しうるが、致命的ではないので初期化時の倍率に従うことにする。 + scale_factor: f32, + width: f32, + height: f32, + atlas: AtlasAllocator, + /// 文字テクスチャアトラスに登録されている文字とそのID + /// + /// NOTE: 文字テクスチャアトラスが満ちている場合に最も古く使われた文字を消すためにLRUキャッシュを用いている。 + registereds: LruCache<(char, u32), CharInfo>, + pub texture: TextureWrapper, +} + +impl CharAtlas { + pub fn new(scale_factor: f32, device: &Device) -> Self { + let width = WIDTH as f32 * scale_factor; + let height = HEIGHT as f32 * scale_factor; + + Self { + scale_factor, + width, + height, + atlas: AtlasAllocator::new(etagere::size2(width as i32, height as i32)), + registereds: LruCache::unbounded(), + texture: TextureWrapper::new( + device, + width as u32, + height as u32, + FORMAT, + TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + ), + } + } + + /// 文字情報を取得するメソッド + /// + /// * queue - queue + /// * c - 対象文字 + /// * scale - 論理解像度上の倍率 (雑に1文字の最大高と思って良い) + /// + /// 対象文字がテクスチャアトラス上に存在しない場合はラスタライズ・登録を行う。 + /// また、同時にLRUキャッシュの更新を行う。 + /// + /// WARN: アトラス上の全ての文字が同時に使用される場合、 + /// 一部文字はテクスチャが更新されるため、意図しない結果を得る。 + pub fn acquire(&mut self, queue: &Queue, c: char, scale: u32) -> &CharInfo { + if !self.registereds.contains(&(c, scale)) { + self.register_character(queue, c, scale); + } + self.registereds.get(&(c, scale)).unwrap() + } + + fn register_character(&mut self, queue: &Queue, c: char, scale: u32) { + // ラスタライズ + let rasterized = match RasterizedChar::new(&FONT, c, scale as f32 * self.scale_factor) { + Ok(n) => n, + Err(advance) => { + self.registereds + .push((c, scale), CharInfo::Unrasterizable { advance }); + return; + } + }; + + // アロケート + let size = etagere::size2(rasterized.width as i32, rasterized.height as i32); + let allocated = loop { + if let Some(alloc) = self.atlas.allocate(size) { + break alloc; + } else { + let least_used = self + .registereds + .pop_lru() + .expect("internal error: no character exists but character atlas is full"); + if let CharInfo::Rasterizable { alloc_id, .. } = least_used.1 { + self.atlas.deallocate(alloc_id); + } + } + }; + + // アップロード + self.texture.write_partial( + queue, + &rasterized.data, + // NOTE: 非負が保証されているため、`u32`にキャストして良い。 + allocated.rectangle.min.x as u32, + // NOTE: 非負が保証されているため、`u32`にキャストして良い。 + allocated.rectangle.min.y as u32, + rasterized.width, + rasterized.height, + ); + + // 登録 + let width = rasterized.width as f32 - MARGIN_AXIS as f32; + let height = rasterized.height as f32 - MARGIN_AXIS as f32; + let info = CharInfo::Rasterizable { + alloc_id: allocated.id, + width: width / self.scale_factor, + height: height / self.scale_factor, + x_offset: rasterized.x_offset / self.scale_factor, + y_offset: rasterized.y_offset / self.scale_factor, + advance: rasterized.advance / self.scale_factor, + uv: Vec4::new( + (allocated.rectangle.min.x as f32 + MARGIN as f32) / self.width, + (allocated.rectangle.min.y as f32 + MARGIN as f32) / self.height, + width / self.width, + height / self.height, + ), + }; + self.registereds.push((c, scale), info); + } +} + +struct RasterizedChar { + data: Vec<u8>, + width: u32, + height: u32, + x_offset: f32, + y_offset: f32, + advance: f32, +} + +impl RasterizedChar { + /// 文字をラスタライズするコンストラクタ + /// + /// * font - フォント + /// * c - 対象文字 + /// * scale - 論理解像度上の倍率 (雑に1文字の最大高と思って良い) + /// + /// 物理解像度と論理解像度の違いを考慮しない。 + /// ラスタライザブルでない文字に対してはadvanceと共に`Err`を返す。 + fn new(font: &FontRef<'static>, c: char, scale: f32) -> Result<Self, f32> { + let font = font.as_scaled(scale); + let glyph = font.scaled_glyph(c); + let advance = font.h_advance(glyph.id); + + let Some(outlined_glyph) = font.outline_glyph(glyph) else { + return Err(advance); + }; + + let width = outlined_glyph.px_bounds().width().ceil() as usize + MARGIN_AXIS; + let height = outlined_glyph.px_bounds().height().ceil() as usize + MARGIN_AXIS; + let channel_count = FORMAT.block_copy_size(None).unwrap() as usize; + let mut data = vec![0x00; channel_count * width * height]; + + outlined_glyph.draw(|x, y, c| { + let x = x as usize + MARGIN; + let y = y as usize + MARGIN; + let i = channel_count * width * y + channel_count * x; + + data[i] = (c * 255.0) as u8; + + #[cfg(target_os = "macos")] + for j in 1..4 { + data[i + j] = 255; + } + }); + + Ok(Self { + data, + width: width as u32, + height: height as u32, + x_offset: outlined_glyph.px_bounds().min.x, + y_offset: outlined_glyph.px_bounds().min.y, + advance, + }) + } +} diff --git a/src/graphics/texture.rs b/src/graphics/texture.rs index 6273b6e..95b9727 100644 --- a/src/graphics/texture.rs +++ b/src/graphics/texture.rs @@ -51,14 +51,15 @@ impl TextureWrapper { /// /// `data`がテクスチャサイズと一致しない場合は、クラッシュしないように不足分が白色で埋められる。 pub fn write(&self, queue: &Queue, data: &[u8]) { - self.write_partial(queue, data, Origin3d::ZERO, self.width, self.height); + self.write_partial(queue, data, 0, 0, self.width, self.height); } /// テクスチャを部分的に更新するメソッド /// /// * queue - queue /// * data - 更新データ - /// * origin - 更新領域の開始座標 + /// * x - 更新領域の開始X座標 + /// * y - 更新領域の開始Y座標 /// * width - 更新領域の幅 /// * height - 更新領域の高さ /// @@ -67,7 +68,8 @@ impl TextureWrapper { &self, queue: &Queue, data: &[u8], - origin: Origin3d, + x: u32, + y: u32, width: u32, height: u32, ) { @@ -89,7 +91,7 @@ impl TextureWrapper { TexelCopyTextureInfo { texture: &self.texture, mip_level: 0, - origin, + origin: Origin3d { x, y, z: 0 }, aspect: TextureAspect::All, }, data, diff --git a/src/main.rs b/src/main.rs index d78aee7..879e010 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,7 +96,7 @@ impl ApplicationHandler for App { fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { let start = Instant::now(); - let (Some(window), Some(renderer)) = (&self.window, &self.renderer) else { + let (Some(window), Some(renderer)) = (&self.window, &mut self.renderer) else { // NOTE: `window`が`Some`になる`resumed`メソッドで`ControlFlow::Poll`を指定しているため、 // ここで`ControlFlow::WaitUntil`を指定しても問題ない。 event_loop.set_control_flow(ControlFlow::WaitUntil(start + Duration::from_millis(16))); |
