Skip to main content

atmos/kernel/
draw.rs

1use crate::kernel::font::FONT_DATA;
2use core::cell::Cell;
3
4static ICON_CURSOR_64: &[u8] = include_bytes!("../../icons/ic_fluent_cursor_64_regular.svg");
5
6#[derive(Copy, Clone)]
7pub struct Color(pub u32);
8
9impl Color {
10    pub fn to_hardware_format(self) -> u32 {
11        let val = self.0;
12        let a = val & 0xFF000000;
13        let r = (val >> 16) & 0xFF;
14        let g = (val >> 8) & 0xFF;
15        let b = val & 0xFF;
16        a | (b << 16) | (g << 8) | r
17    }
18
19    pub fn from_hardware_format(val: u32) -> Color {
20        let a = val & 0xFF000000;
21        let r = (val >> 16) & 0xFF;
22        let g = (val >> 8) & 0xFF;
23        let b = val & 0xFF;
24        Color(a | (b << 16) | (g << 8) | r)
25    }
26}
27
28impl Color {
29    pub const BLACK: Color = Color(0xFF000000);
30    pub const WHITE: Color = Color(0xFFFFFFFF);
31    pub const LIGHT_GRAY: Color = Color(0xFFC6C6C6);
32    pub const DARK_GRAY: Color = Color(0xFF848484);
33    pub const BLUE: Color = Color(0xFF000084);
34    pub const DARK_RED: Color = Color(0xFF840000);
35    pub const INACTIVE_TITLE: Color = Color(0xFF5A5A5A);
36}
37
38const DMA_THRESHOLD_PIXELS: u32 = 2048; // 32bpp で 8KB 相当
39
40/// 【2026-07-27診断】コンポジタが画面へ出したフレームの通し番号。
41/// ヒーロー見出しの描画は成功しているのに画面に出ない件で、
42/// 「文字を描いたフレーム」と「実際に表示されているフレーム」が
43/// 同一かを突き合わせるために使う。
44pub static FRAME_NO: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
45/// ヒーロー見出し(size>=40 のテキスト)を最後に描画したフレーム番号。
46pub static HERO_TEXT_FRAME: core::sync::atomic::AtomicU32 =
47    core::sync::atomic::AtomicU32::new(u32::MAX);
48
49
50pub struct Screen {
51    pub vram: *mut u32,
52    pub back_buffer: *mut u32,
53    pub use_back_buffer: core::cell::Cell<bool>,
54    pub width: u32,
55    pub height: u32,
56    pub pitch: u32,                               // bytes per line
57    pub clip: Cell<Option<(u32, u32, u32, u32)>>, // (x0, y0, x1, y1)
58    clip_stack: Cell<[Option<(u32, u32, u32, u32)>; 4]>,
59    clip_depth: Cell<usize>,
60
61    // 動的描画ターゲットバッファの追加
62    render_target: Cell<*mut u32>,
63    target_width: Cell<u32>,
64    target_height: Cell<u32>,
65
66    // VSync ページフリップ用
67    vsync: Cell<bool>, // 2ページ確保済み(virtual_height=2H)で flip が使えるか
68    page: Cell<u32>,   // 次に描画する VRAM ページ (0/1)
69    fb_virtual_height: Cell<u32>, // GPU が確保した仮想高さ(2*height で 2 ページ)
70    fb_size: Cell<u32>, // GPU が確保したバッファ総バイト数
71    // VSync 部分フラッシュ用: 前回フレームのコンテンツのダーティ領域(今回と合わせ 2 フレーム分を裏ページへ)
72    prev_content_region: Cell<Option<(u32, u32, u32, u32)>>,
73    // 各ページ(0/1)に焼き込まれているカーソル位置。裏ページのカーソル消去に使う
74    // (ページは交互なので裏ページのカーソルは 2 フレーム前の位置)。
75    page_cursor: Cell<[(u32, u32); 2]>,
76    // VSync 有効化直後は両ページを全面初期化する必要があるため、最初の数フレームは全面転送する
77    vsync_init_count: Cell<u8>,
78}
79
80/// 【2026-07-31計測】テキスト描画に費やした tick の累積(フレーム毎にリセット)。
81pub static TEXT_DRAW_TICKS: core::sync::atomic::AtomicUsize =
82    core::sync::atomic::AtomicUsize::new(0);
83
84/// 【2026-08-01計測】矩形塗り(boxfill)の累積 tick。
85pub static BOXFILL_TICKS: core::sync::atomic::AtomicUsize =
86    core::sync::atomic::AtomicUsize::new(0);
87/// 【2026-08-01計測】box-shadow 描画の累積 tick。
88pub static SHADOW_TICKS: core::sync::atomic::AtomicUsize =
89    core::sync::atomic::AtomicUsize::new(0);
90
91/// 【2026-08-01計測】角丸/クリップ付き背景の毎ピクセル塗りの累積 tick。
92pub static RRECT_TICKS: core::sync::atomic::AtomicUsize =
93    core::sync::atomic::AtomicUsize::new(0);
94
95/// 【2026-08-01計測】clip-path / opacity / mix-blend-mode による
96/// 背景の毎ピクセル塗りの累積 tick。
97pub static PIXBG_TICKS: core::sync::atomic::AtomicUsize =
98    core::sync::atomic::AtomicUsize::new(0);
99
100/// 描画側から使う計測ガード(`PIXBG_TICKS` 用)。
101pub struct PixBgTimer(usize);
102impl PixBgTimer {
103    #[allow(clippy::new_without_default)]
104    pub fn new() -> Self {
105        Self(crate::kernel::timer::get_ticks())
106    }
107}
108impl Drop for PixBgTimer {
109    fn drop(&mut self) {
110        PIXBG_TICKS.fetch_add(
111            crate::kernel::timer::get_ticks().wrapping_sub(self.0),
112            core::sync::atomic::Ordering::Relaxed,
113        );
114    }
115}
116
117/// 描画側から使う計測ガード。
118pub struct RRectTimer(usize);
119impl RRectTimer {
120    #[allow(clippy::new_without_default)]
121    pub fn new() -> Self {
122        Self(crate::kernel::timer::get_ticks())
123    }
124}
125impl Drop for RRectTimer {
126    fn drop(&mut self) {
127        RRECT_TICKS.fetch_add(
128            crate::kernel::timer::get_ticks().wrapping_sub(self.0),
129            core::sync::atomic::Ordering::Relaxed,
130        );
131    }
132}
133
134struct AccTimer(usize, &'static core::sync::atomic::AtomicUsize);
135impl Drop for AccTimer {
136    fn drop(&mut self) {
137        self.1.fetch_add(
138            crate::kernel::timer::get_ticks().wrapping_sub(self.0),
139            core::sync::atomic::Ordering::Relaxed,
140        );
141    }
142}
143
144/// 早期 return があるため Drop で確実に積む。
145struct TextDrawTimer(usize);
146impl Drop for TextDrawTimer {
147    fn drop(&mut self) {
148        TEXT_DRAW_TICKS.fetch_add(
149            crate::kernel::timer::get_ticks().wrapping_sub(self.0),
150            core::sync::atomic::Ordering::Relaxed,
151        );
152    }
153}
154
155impl Screen {
156    pub fn new(vram: *mut u32, width: u32, height: u32, pitch: u32) -> Self {
157        Self {
158            vram,
159            back_buffer: core::ptr::null_mut(),
160            use_back_buffer: core::cell::Cell::new(false),
161            width,
162            height,
163            pitch,
164            clip: Cell::new(None),
165            clip_stack: Cell::new([None; 4]),
166            clip_depth: Cell::new(0),
167            render_target: Cell::new(core::ptr::null_mut()),
168            target_width: Cell::new(width),
169            target_height: Cell::new(height),
170            vsync: Cell::new(false),
171            page: Cell::new(0),
172            fb_virtual_height: Cell::new(0),
173            fb_size: Cell::new(0),
174            prev_content_region: Cell::new(None),
175            page_cursor: Cell::new([(0, 0), (0, 0)]),
176            vsync_init_count: Cell::new(0),
177        }
178    }
179
180    pub fn vsync_enabled(&self) -> bool {
181        self.vsync.get()
182    }
183
184    pub fn has_pending_vsync_sync(&self) -> bool {
185        self.prev_content_region.get().is_some()
186    }
187
188    /// フレームバッファの実ジオメトリをシリアルへ出力する(実機デバッグ用)。
189    /// ブート最序盤のログがスクロールアウトしても確認できるよう、任意の地点で呼べる。
190    pub fn debug_geometry(&self) {
191        let addr = self.vram as usize;
192        let region = if addr >= crate::kernel::mmu::DEVICE_REGION_START {
193            "uncached/Device OK"
194        } else {
195            "CACHED RAM (<0x38000000) !!"
196        };
197        crate::println!(
198            "[FB] vram=0x{:08X} ({}) | {}x{} | pitch={} (width*4={}) | virt_h={} | size={}",
199            addr,
200            region,
201            self.width,
202            self.height,
203            self.pitch,
204            self.width * 4,
205            self.fb_virtual_height.get(),
206            self.fb_size.get()
207        );
208    }
209
210    /// GPU が実際に確保したフレームバッファのジオメトリ(仮想高さ・総サイズ)を記録する。
211    /// enable_vsync() がこれを見て 2 ページ確保済みか判定する。
212    pub fn set_fb_geometry(&self, virtual_height: u32, size: u32) {
213        self.fb_virtual_height.set(virtual_height);
214        self.fb_size.set(size);
215    }
216
217    /// VSync ページフリップを有効化する。ただし 2 ページ(virtual_height >= 2*height かつ
218    /// バッファサイズ十分)が実際に確保できている場合のみ。確保できていなければ単一バッファの
219    /// ままにする(page1 がフレームバッファ外を指して横線ノイズになるのを防ぐ)。
220    /// 有効化できたら true を返す。
221    pub fn enable_vsync(&self) -> bool {
222        let virt_h = self.fb_virtual_height.get();
223        let size = self.fb_size.get();
224        let two_pages = virt_h >= self.height * 2
225            && (size == 0 || size as u64 >= (self.pitch as u64) * (self.height as u64) * 2);
226        if !two_pages {
227            self.vsync.set(false);
228            return false;
229        }
230        // 初期表示をページ0に固定
231        let _ = crate::kernel::mailbox::set_virtual_offset(0, 0);
232        self.page.set(0);
233        self.vsync.set(true);
234        // 最初の 2 フラッシュは両ページを全面初期化する(部分フラッシュ前提のため)
235        self.vsync_init_count.set(2);
236        self.prev_content_region.set(None);
237        self.page_cursor.set([(0, 0), (0, 0)]);
238        true
239    }
240
241    pub fn set_render_target(&self, buf: *mut u32, w: u32, h: u32) {
242        self.render_target.set(buf);
243        self.target_width.set(w);
244        self.target_height.set(h);
245    }
246
247    pub fn reset_render_target(&self) {
248        self.render_target.set(core::ptr::null_mut());
249        self.target_width.set(self.width);
250        self.target_height.set(self.height);
251    }
252
253    pub fn resolve_target(&self) -> (*mut u32, u32, u32) {
254        let custom_target = self.render_target.get();
255        if !custom_target.is_null() {
256            (
257                custom_target,
258                self.target_width.get(),
259                self.target_height.get(),
260            )
261        } else {
262            let base_ptr = if self.use_back_buffer.get() && !self.back_buffer.is_null() {
263                self.back_buffer
264            } else {
265                self.vram
266            };
267            (base_ptr, self.width, self.height)
268        }
269    }
270
271    /// ターゲットバッファの1行あたりの実際のピクセル数(Stride)を返します。
272    pub fn resolve_stride(&self) -> u32 {
273        let custom_target = self.render_target.get();
274        if !custom_target.is_null() {
275            self.target_width.get()
276        } else if self.use_back_buffer.get() && !self.back_buffer.is_null() {
277            self.width
278        } else {
279            self.pitch / 4
280        }
281    }
282
283    pub fn enable_double_buffering(&mut self) {
284        if self.back_buffer.is_null() {
285            // Allocate a back buffer matching the screen size
286            let Ok(layout) = core::alloc::Layout::array::<u32>((self.width * self.height) as usize)
287            else {
288                return; // サイズ計算がオーバーフローした場合は有効化しない
289            };
290            unsafe {
291                self.back_buffer = alloc::alloc::alloc_zeroed(layout) as *mut u32;
292            }
293            if self.back_buffer.is_null() {
294                return; // 確保失敗時はバックバッファ無効のまま継続
295            }
296        }
297        self.use_back_buffer.set(true);
298    }
299
300    pub fn flush(&self) {
301        if self.use_back_buffer.get() && !self.back_buffer.is_null() {
302            let stride = self.pitch / 4;
303            let w = self.width;
304            let h = self.height;
305
306            if self.vsync.get() {
307                // ページフリップ: 裏ページ(現在 page)へ全画面コピーしてから VBlank で flip。
308                // 表示中ページは書き換えないためティアリングが起きない。
309                let page = self.page.get();
310                let page_off_px = page * h * stride; // ページ先頭のピクセルオフセット
311                let dst = self.vram as u32 + page_off_px * 4;
312                if stride == w {
313                    crate::kernel::dma::dma_memcpy(5, self.back_buffer as u32, dst, w * h * 4);
314                } else {
315                    crate::kernel::dma::dma_memcpy_2d(
316                        5,
317                        self.back_buffer as u32,
318                        dst,
319                        w * 4,
320                        h,
321                        w * 4,
322                        stride * 4,
323                    );
324                }
325                // VBlank 同期フリップ(表示元をこのページへ)
326                let _ = crate::kernel::mailbox::set_virtual_offset(0, page * h);
327                // VSync(VBlank)完了を待機
328                let _ = crate::kernel::mailbox::wait_vsync();
329                // 次回は反対のページへ描く
330                self.page.set(1 - page);
331            } else if stride == w {
332                crate::kernel::dma::dma_memcpy(
333                    5,
334                    self.back_buffer as u32,
335                    self.vram as u32,
336                    w * h * 4,
337                );
338            } else {
339                crate::kernel::dma::dma_memcpy_2d(
340                    5,
341                    self.back_buffer as u32,
342                    self.vram as u32,
343                    w * 4,
344                    h,
345                    w * 4,
346                    stride * 4,
347                );
348            }
349        }
350    }
351
352    pub fn flush_rect(&self, x0: u32, y0: u32, x1: u32, y1: u32) {
353        // VSync ページフリップ時は部分転送だと裏ページに古い領域が残るため、
354        // 全画面コピー+flip に切り替える(ティアリング無し優先)。
355        if self.vsync.get() {
356            self.flush();
357            return;
358        }
359        if self.use_back_buffer.get() && !self.back_buffer.is_null() {
360            let stride = self.pitch / 4;
361            let w = self.width;
362            let h = self.height;
363
364            let cx0 = x0.min(w - 1);
365            let cx1 = x1.min(w - 1).max(cx0);
366            let cy0 = y0.min(h - 1);
367            let cy1 = y1.min(h - 1).max(cy0);
368
369            let rect_w = cx1 - cx0 + 1;
370            let rect_h = cy1 - cy0 + 1;
371
372            if rect_w * rect_h >= DMA_THRESHOLD_PIXELS {
373                let src_offset = (cy0 * w + cx0) * 4;
374                let dst_offset = (cy0 * stride + cx0) * 4;
375                crate::kernel::dma::dma_memcpy_2d(
376                    5,
377                    self.back_buffer as u32 + src_offset,
378                    self.vram as u32 + dst_offset,
379                    rect_w * 4,
380                    rect_h,
381                    w * 4,
382                    stride * 4,
383                );
384            } else {
385                for y in cy0..=cy1 {
386                    unsafe {
387                        let src = self.back_buffer.add((y * w + cx0) as usize);
388                        let dst = self.vram.add((y * stride + cx0) as usize);
389                        core::ptr::copy_nonoverlapping(src, dst, rect_w as usize);
390                    }
391                }
392            }
393        }
394    }
395
396    /// 一時的に裏画面を無効化し、直接VRAM(表画面)にマウスを描画します
397    pub fn draw_mouse_direct(&self, mouse: &Mouse) {
398        let prev = self.use_back_buffer.get();
399        self.use_back_buffer.set(false);
400        mouse.draw(self);
401        self.use_back_buffer.set(prev);
402    }
403
404    /// バックバッファにカーソルを一時合成してからflushする。
405    /// flush中にカーソルが消えてちらつく現象を防ぐ。
406    pub fn flush_with_cursor(&self, mouse: &Mouse) {
407        if !self.use_back_buffer.get() || self.back_buffer.is_null() {
408            self.flush();
409            return;
410        }
411        let cursor_size = Mouse::SIZE as usize;
412        let w = self.width as usize;
413        let h = self.height as usize;
414        let cx = mouse.x as usize;
415        let cy = mouse.y as usize;
416
417        // カーソル領域のバックバッファを一時保存
418        let rows = cursor_size.min(h.saturating_sub(cy));
419        let cols = cursor_size.min(w.saturating_sub(cx));
420        let mut saved = alloc::vec![0u32; cursor_size * cursor_size];
421        unsafe {
422            for row in 0..rows {
423                let src = self.back_buffer.add((cy + row) * w + cx);
424                core::ptr::copy_nonoverlapping(
425                    src,
426                    saved.as_mut_ptr().add(row * cursor_size),
427                    cols,
428                );
429            }
430        }
431
432        // バックバッファにカーソルを描画してから flush
433        mouse.draw(self);
434        self.flush();
435
436        // バックバッファを復元(カーソルを消す)
437        unsafe {
438            for row in 0..rows {
439                let dst = self.back_buffer.add((cy + row) * w + cx);
440                core::ptr::copy_nonoverlapping(saved.as_ptr().add(row * cursor_size), dst, cols);
441            }
442        }
443    }
444
445    /// VSync ページフリップ時に、ダーティ領域だけを裏ページへ転送してフリップする部分フラッシュ。
446    /// `content` は今回のコンテンツのダーティ矩形 (x0,y0,x1,y1)。カーソルは内部で合成する。
447    /// 裏ページは 2 フレーム前の内容なので「今回 ∪ 前回」のダーティ領域を転送して整合を保つ。
448    /// 全画面コピーを避けるため、小さな更新ではコストが大幅に下がる。
449    pub fn flush_dirty_pageflip(&self, content: Option<(u32, u32, u32, u32)>, mouse: &Mouse) {
450        if !self.vsync.get() || !self.use_back_buffer.get() || self.back_buffer.is_null() {
451            // VSync 無効時は呼ばれない想定だが、安全のため全面フラッシュにフォールバック
452            self.flush_with_cursor(mouse);
453            return;
454        }
455        let w = self.width;
456        let h = self.height;
457        let stride = self.pitch / 4;
458        let cs = Mouse::SIZE;
459
460        #[inline]
461        fn union(a: (u32, u32, u32, u32), b: (u32, u32, u32, u32)) -> (u32, u32, u32, u32) {
462            (a.0.min(b.0), a.1.min(b.1), a.2.max(b.2), a.3.max(b.3))
463        }
464
465        #[inline]
466        fn cursor_rect(x: u32, y: u32, cs: u32, w: u32, h: u32) -> (u32, u32, u32, u32) {
467            (x.min(w), y.min(h), (x + cs).min(w), (y + cs).min(h))
468        }
469
470        let page = self.page.get(); // これから描く裏ページ
471                                    // 今回のカーソル矩形(新位置)
472        let c_new = cursor_rect(mouse.x, mouse.y, cs, w, h);
473        // 裏ページに焼き込まれている古いカーソル(2 フレーム前の位置)を消すための矩形
474        let old_c = self.page_cursor.get()[page as usize];
475        let c_old = cursor_rect(old_c.0, old_c.1, cs, w, h);
476
477        // 今回のコンテンツのダーティ(クランプ)
478        let content_now = content.map(|c| (c.0.min(w), c.1.min(h), c.2.min(w), c.3.min(h)));
479
480        // 転送領域: 最初の数フレームは全面、それ以降は
481        //   コンテンツ(今回 ∪ 前回) ∪ カーソル(新) ∪ 裏ページの旧カーソル
482        // 【2026-07-26 検証済み・仮説は否定】ヒーロー見出しがバックバッファへは
483        // 書き込まれている([GLYPH][PX] で 1 文字 200〜500px を確認)のに画面へ
484        // 出ない件について、「ダーティ矩形が描画範囲を覆えていない」疑いを
485        // 検証するため一時的に常時全面転送にして実機確認したが、**それでも
486        // 文字は表示されなかった**。したがって転送領域は原因ではない
487        // (=文字は「表示に使われるバッファとは別の場所」へ書かれている)。
488        // 常時全面転送は性能を損なうだけなので元の差分転送へ戻す。
489        let region = if self.vsync_init_count.get() > 0 {
490            self.vsync_init_count.set(self.vsync_init_count.get() - 1);
491            (0, 0, w, h)
492        } else {
493            // コンテンツの 2 フレーム分
494            let content_region = match (content_now, self.prev_content_region.get()) {
495                (Some(a), Some(b)) => Some(union(a, b)),
496                (Some(a), None) => Some(a),
497                (None, Some(b)) => Some(b),
498                (None, None) => None,
499            };
500            let mut r = union(c_new, c_old);
501            if let Some(cr) = content_region {
502                r = union(r, cr);
503            }
504            r
505        };
506
507        let x0 = region.0.min(w);
508        let y0 = region.1.min(h);
509        let x1 = region.2.min(w).max(x0);
510        let y1 = region.3.min(h).max(y0);
511
512        // カーソルを back_buffer に一時合成(SIZE×SIZE を退避)
513        let cx = mouse.x as usize;
514        let cy = mouse.y as usize;
515        let wsz = w as usize;
516        let hsz = h as usize;
517        let rows = (cs as usize).min(hsz.saturating_sub(cy));
518        let cols = (cs as usize).min(wsz.saturating_sub(cx));
519        let mut saved = alloc::vec![0u32; (cs * cs) as usize];
520        unsafe {
521            for row in 0..rows {
522                let src = self.back_buffer.add((cy + row) * wsz + cx);
523                core::ptr::copy_nonoverlapping(
524                    src,
525                    saved.as_mut_ptr().add(row * cs as usize),
526                    cols,
527                );
528            }
529        }
530        mouse.draw(self); // back_buffer にカーソルを描画(use_back_buffer=true 前提)
531
532        // 裏ページへ region を部分転送 → フリップ
533        if x1 > x0 && y1 > y0 {
534            let rect_w = x1 - x0;
535            let rect_h = y1 - y0;
536            let src_off = y0 * w + x0;
537            let dst_off = page * h * stride + y0 * stride + x0;
538            crate::kernel::dma::dma_memcpy_2d(
539                5,
540                self.back_buffer as u32 + src_off * 4,
541                self.vram as u32 + dst_off * 4,
542                rect_w * 4,
543                rect_h,
544                w * 4,
545                stride * 4,
546            );
547            let _ = crate::kernel::mailbox::set_virtual_offset(0, page * h);
548            let _ = crate::kernel::mailbox::wait_vsync();
549            self.page.set(1 - page);
550        }
551
552        // back_buffer のカーソルを復元
553        unsafe {
554            for row in 0..rows {
555                let dst = self.back_buffer.add((cy + row) * wsz + cx);
556                core::ptr::copy_nonoverlapping(saved.as_ptr().add(row * cs as usize), dst, cols);
557            }
558        }
559
560        // この裏ページにはカーソルを新位置で焼き込んだので記録(次にこのページを描く時に消す)
561        let mut pc = self.page_cursor.get();
562        pc[page as usize] = (mouse.x, mouse.y);
563        self.page_cursor.set(pc);
564        self.prev_content_region.set(content_now);
565    }
566
567    pub fn set_clip(&self, x0: u32, y0: u32, x1: u32, y1: u32) {
568        self.clip.set(Some((x0, y0, x1, y1)));
569    }
570
571    /// 現在のクリップ矩形(診断用)。`None` はクリップ無し。
572    pub fn clip_rect(&self) -> Option<(u32, u32, u32, u32)> {
573        self.clip.get()
574    }
575
576    pub fn clear_clip(&self) {
577        self.clip.set(None);
578    }
579
580    /// 現在のクリップをスタックに積んで、新しいクリップを現在と交差させて設定する。
581    /// pop_clip() で元のクリップに戻る(最大 4 段ネスト可)。
582    pub fn push_clip(&self, x0: u32, y0: u32, x1: u32, y1: u32) {
583        let depth = self.clip_depth.get();
584        if depth < 4 {
585            let mut stack = self.clip_stack.get();
586            stack[depth] = self.clip.get();
587            self.clip_stack.set(stack);
588            self.clip_depth.set(depth + 1);
589        }
590        let new_clip = if let Some((cx0, cy0, cx1, cy1)) = self.clip.get() {
591            (cx0.max(x0), cy0.max(y0), cx1.min(x1), cy1.min(y1))
592        } else {
593            (x0, y0, x1, y1)
594        };
595        self.clip.set(Some(new_clip));
596    }
597
598    /// push_clip() で積んだクリップを復元する。
599    pub fn pop_clip(&self) {
600        let depth = self.clip_depth.get();
601        if depth > 0 {
602            let stack = self.clip_stack.get();
603            self.clip.set(stack[depth - 1]);
604            self.clip_depth.set(depth - 1);
605        } else {
606            self.clip.set(None);
607        }
608    }
609
610    pub fn clear(&self, color: Color) {
611        // クリップ領域が設定されている場合、その範囲のみクリアする(部分再描画対応)。
612        // boxfill はクリップ対応済みなので、クリップ領域だけが塗りつぶされる。
613        if let Some((cx0, cy0, cx1, cy1)) = self.clip.get() {
614            self.boxfill(cx0, cy0, cx1, cy1, color);
615            return;
616        }
617
618        let (target_ptr, _w, h) = self.resolve_target();
619
620        // V3D タイルクリア (64x64 タイル) は実機で 64px グリッド状のアーティファクト
621        // (色付きの点線) を生じることがあり、また pitch パディングを考慮しないため使用しない。
622        // stride を正しく扱う DMA fill に一本化する。
623        let stride = self.resolve_stride();
624        let color_val = color.to_hardware_format();
625        crate::kernel::dma::dma_fill_rect(
626            5, // DMA Channel 5
627            &color_val as *const _ as u32,
628            target_ptr as u32,
629            stride,
630            h,
631            stride * 4,
632        );
633    }
634
635    /// 計算量: **O(1)** — クリップ判定 1 回と 32bit 書き込み 1 回。
636    ///
637    /// 描画の最下層プリミティブ。上層はこれの呼び出し回数で見積もれる
638    /// (規約は `spec/complexity_annotation.md`)。
639    pub fn draw_pixel(&self, x: u32, y: u32, color: Color) {
640        if let Some((cx0, cy0, cx1, cy1)) = self.clip.get() {
641            if x < cx0 || x >= cx1 || y < cy0 || y >= cy1 {
642                return;
643            }
644        }
645
646        // 半透明色は背景と source-over 合成する(`spec/alpha_compositing.md`)。
647        // 実サイトの `.hero-overlay { background: rgba(0,0,0,.2) }`(0x33000000)が
648        // 不透明な黒帯として描かれていた回帰への対処。
649        //
650        // 【互換上の妥協】アルファ 0x00 は「完全透明」ではなく**不透明**として扱う。
651        // 既存コードの大半はアルファを設定せず 0x00 のまま色を渡しており、
652        // 仕様どおり完全透明にすると画面全体が消える。合成するのは 0x01〜0xFE のみ。
653        // この妥協は `spec/alpha_compositing.md` にも明記してある。
654        let a = (color.0 >> 24) & 0xFF;
655        let color = if a == 0 || a == 0xFF {
656            color
657        } else {
658            Color(crate::os_lib::web_engine::alpha_blend::blend_argb_over(
659                color.0,
660                self.read_pixel(x, y).0 | 0xFF00_0000,
661            ))
662        };
663
664        let (target_ptr, w, h) = self.resolve_target();
665        // 画面外の座標は捨てる。要素がビューポートをはみ出すのは正常なので
666        // これは異常ではなく、ログも出さない(`spec/logging_policy.md` L-3)。
667        if x < w && y < h {
668            let stride = self.resolve_stride();
669            unsafe {
670                let ptr = target_ptr.add((y * stride + x) as usize);
671                core::ptr::write_volatile(ptr, color.to_hardware_format());
672            }
673        }
674    }
675
676    pub fn read_pixel(&self, x: u32, y: u32) -> Color {
677        let (target_ptr, w, h) = self.resolve_target();
678        if x < w && y < h {
679            let stride = self.resolve_stride();
680            unsafe {
681                let ptr = target_ptr.add((y * stride + x) as usize);
682                Color::from_hardware_format(core::ptr::read_volatile(ptr))
683            }
684        } else {
685            Color::BLACK
686        }
687    }
688
689    /// 計算量: **O(1)**。ただし定数倍は `draw_pixel` より重い。
690    ///
691    /// - `alpha == 0`: 何もしない(最速)
692    /// - `alpha == 255`: `draw_pixel` へ委譲(O(1)、軽い)
693    /// - それ以外: **既存ピクセルの読み出し**+3 チャンネルの合成が要る。
694    ///   読み出しがフレームバッファへのアクセスになるため、
695    ///   不透明の場合の数倍のコストになる
696    pub fn draw_pixel_alpha(&self, x: u32, y: u32, color: Color, alpha: u8) {
697        if alpha == 0 {
698            return;
699        }
700        if alpha == 255 {
701            self.draw_pixel(x, y, color);
702            return;
703        }
704
705        let bg = self.read_pixel(x, y);
706        let a = alpha as u32;
707        let inv_a = 255 - a;
708
709        let fg_r = (color.0 >> 16) & 0xFF;
710        let fg_g = (color.0 >> 8) & 0xFF;
711        let fg_b = color.0 & 0xFF;
712
713        let bg_r = (bg.0 >> 16) & 0xFF;
714        let bg_g = (bg.0 >> 8) & 0xFF;
715        let bg_b = bg.0 & 0xFF;
716
717        let r = ((fg_r * a + bg_r * inv_a) / 255) & 0xFF;
718        let g = ((fg_g * a + bg_g * inv_a) / 255) & 0xFF;
719        let b = ((fg_b * a + bg_b * inv_a) / 255) & 0xFF;
720
721        let blended = 0xFF000000 | (r << 16) | (g << 8) | b;
722        self.draw_pixel(x, y, Color(blended));
723    }
724
725    /// ベクトルフォントを用いて、指定したサイズと色で文字列を描画します (アンチエイリアス対応)
726    pub fn draw_string_vector(&self, x: u32, y: u32, s: &str, color: Color, size_px: u32) {
727        let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
728        if let Some(ref mut font) = *font_lock {
729            let (_, w, h) = self.resolve_target();
730            let mut cur_x = x as i32;
731            for c in s.chars() {
732                if let Some(glyph) = font.get_glyph(c, size_px) {
733                    let gx = cur_x + glyph.x_offset;
734                    // ベクトルフォントのベースラインから上端(Top)基準にアセンダ補正(0.85 * size_px)を追加して、位置ズレを解消
735                    let gy = y as i32 + glyph.y_offset + (size_px as i32 * 85 / 100);
736
737                    for row in 0..glyph.height {
738                        let py = gy + row as i32;
739                        if py < 0 || py >= h as i32 {
740                            continue;
741                        }
742                        for col in 0..glyph.width {
743                            let px = gx + col as i32;
744                            if px < 0 || px >= w as i32 {
745                                continue;
746                            }
747
748                            let alpha = glyph.data[(row * glyph.width + col) as usize];
749                            if alpha > 0 {
750                                self.draw_pixel_alpha(px as u32, py as u32, color, alpha);
751                            }
752                        }
753                    }
754
755                    cur_x += glyph.advance as i32;
756                }
757            }
758        }
759    }
760
761    /// ベクトルフォントを用いて、指定した背景色で文字列を描画します(カラーキャッシュによる高速化版)
762    pub fn draw_string_vector_bg(
763        &self,
764        x: u32,
765        y: u32,
766        s: &str,
767        color: Color,
768        size_px: u32,
769        bg: Color,
770    ) {
771        let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
772        if let Some(ref mut font) = *font_lock {
773            let (_, w, h) = self.resolve_target();
774            let mut cur_x = x as i32;
775            for c in s.chars() {
776                if let Some(glyph) = font.get_color_glyph(c, size_px, color.0, bg.0) {
777                    let gx = cur_x + glyph.x_offset;
778                    let gy = y as i32 + glyph.y_offset + (size_px as i32 * 85 / 100);
779
780                    for row in 0..glyph.height {
781                        let py = gy + row as i32;
782                        if py < 0 || py >= h as i32 {
783                            continue;
784                        }
785                        for col in 0..glyph.width {
786                            let px = gx + col as i32;
787                            if px < 0 || px >= w as i32 {
788                                continue;
789                            }
790                            let pixel_color = glyph.data[(row * glyph.width + col) as usize];
791                            if (pixel_color >> 24) > 0 {
792                                // 完全透明ピクセルはスキップ
793                                self.draw_pixel(px as u32, py as u32, Color(pixel_color));
794                            }
795                        }
796                    }
797                    cur_x += glyph.advance as i32;
798                } else {
799                    cur_x += (size_px / 2) as i32;
800                }
801            }
802        }
803    }
804
805    /// 縦スクロールバーの共通描画ヘルパー(幅 6px のサム)。
806    ///
807    /// - `track_x`: バー帯の左端 X
808    /// - `track_y` / `track_h`: トラック(可動域)の上端 Y と高さ
809    /// - `content_h`: コンテンツ全体の高さ
810    /// - `viewport_h`: 表示領域の高さ
811    /// - `scroll_pos`: 現在のスクロール量 (0..=content_h-viewport_h)
812    ///
813    /// コンテンツが表示領域に収まっている場合は何も描かない。
814    /// Web ページ・textarea・将来的にターミナル等、スクロールする UI は
815    /// すべてこのヘルパーを使い、見た目と計算を統一する。
816    /// 計算量: **O(6×Th)** — Th はサムの高さ(ピクセル)。`boxfill` 1 回ぶん。
817    /// 幅は 6px 固定なので実質 O(Th)。
818    pub fn draw_vscrollbar(
819        &self,
820        track_x: u32,
821        track_y: i32,
822        track_h: i32,
823        content_h: i32,
824        viewport_h: i32,
825        scroll_pos: i32,
826    ) {
827        if content_h <= viewport_h || track_h <= 0 || viewport_h <= 0 {
828            return;
829        }
830        let max_scroll = (content_h - viewport_h).max(1);
831        let thumb_h = ((track_h * viewport_h) / content_h).clamp(24.min(track_h), track_h);
832        let ratio = scroll_pos.clamp(0, max_scroll) as f32 / max_scroll as f32;
833        let thumb_y = track_y + (ratio * (track_h - thumb_h) as f32) as i32;
834        self.boxfill(
835            track_x,
836            thumb_y.max(0) as u32,
837            track_x + 6,
838            (thumb_y + thumb_h).max(0) as u32,
839            Color(crate::kernel::config::get_config().theme.scrollbar_thumb),
840        );
841    }
842
843    /// 計算量: **O(W×H)** — W = `x1-x0`, H = `y1-y0`(ピクセル)。
844    ///
845    /// 定数倍がアルファで大きく変わる:
846    /// - 不透明(`alpha == 0xFF`)/ 完全透明: 行単位の一括書き込み。軽い
847    /// - **半透明(1〜254)**: 1 ピクセルずつ `draw_pixel_alpha` へ委譲するため
848    ///   **数倍重い**(`spec/alpha_compositing.md`)
849    ///
850    /// 呼び出し頻度: 要素ごとに毎フレーム。画面を覆う大きな要素が支配的になる。
851    pub fn boxfill(&self, x0: u32, y0: u32, x1: u32, y1: u32, color: Color) {
852        // 半透明色(0x01〜0xFE)は 1 ピクセルずつ背景と合成する必要があるため、
853        // 一括書き込み経路ではなく draw_pixel へ委譲する。
854        // これを怠ると `.hero-overlay { background: rgba(0,0,0,.2) }` が
855        // 不透明な黒帯として塗られる(`spec/alpha_compositing.md`)。
856        let a = (color.0 >> 24) & 0xFF;
857        // 【2026-08-05 発見・修正】アルファ 0(完全透明)は**何も描かない**。
858        //
859        // 以前は `a == 0` が下の一括書き込み経路へ落ち、
860        // `0x00000000` をそのまま書き込んでいた。フレームバッファには
861        // アルファが無いので、これは**不透明な黒**になる。
862        //
863        // 実測: `border-color: transparent`(= `bd=Some(0)`)を持つ
864        // 20×20 の `<i>` 要素が黒い枠(☐)として描かれ、
865        // Font Awesome のアイコン位置に box が並んでいた。
866        //
867        // `extract_color` で直したのと同じバグ種別
868        // (`spec/alpha_compositing.md`: アルファ 0 は「透明」であって
869        //  「不透明な黒」ではない)。
870        if a == 0 {
871            return;
872        }
873        if a != 0xFF {
874            // 【2026-09-08 修正】ここは以前クリップも画面境界も見ずに
875            // `y0..y1` を回していた。呼び出し側が負値を u32 へキャストして
876            // 渡すと y1 が約 43 億になり、`draw_pixel` が捨てるだけの
877            // ループを延々と回して事実上ハングする。塗る範囲を先に確定する。
878            let (target_w, target_h) = {
879                let (_, w, h) = self.resolve_target();
880                (w, h)
881            };
882            if let Some((cx0, cy0, cx1, cy1)) = crate::kernel::draw_clip::clip_fill_rect(
883                x0,
884                y0,
885                x1,
886                y1,
887                target_w,
888                target_h,
889                self.clip.get(),
890            ) {
891                for y in cy0..=cy1 {
892                    for x in cx0..=cx1 {
893                        self.draw_pixel(x, y, color);
894                    }
895                }
896            }
897            return;
898        }
899
900        let (target_ptr, w, h) = self.resolve_target();
901        // 【2026-07-26診断】ヒーロー背景(#ff6b6b)の塗りつぶし時の描画先バッファを
902        // 記録し、文字描画時([GLYPH][DIAG])と同一バッファかを突き合わせる。
903        // 異なっていれば「背景と文字が別バッファへ描かれている」ことになり、
904        // 「文字は書き込まれているのに画面に出ない」症状と完全に一致する。
905        if (color.0 & 0x00FF_FFFF) == 0x00FF_6B6B {
906            static BG_DIAG: core::sync::atomic::AtomicU32 =
907                core::sync::atomic::AtomicU32::new(0);
908            if BG_DIAG.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 3 {
909                crate::warn!(
910                    "[TARGET][BG] hero fill target=0x{:x} w={} h={} rect=({},{})-({},{})",
911                    target_ptr as usize, w, h, x0, y0, x1, y1
912                );
913            }
914        }
915        // 画面サイズと追加クリップ矩形で切り詰める。
916        // 【2026-09-08 カーネルパニック修正】この計算はかつてここに直書きされており、
917        // 幅ゼロのクリップ矩形 (`kx1 == 0`) で `kx1 - 1` が u32 ラップし、
918        // 43 億ピクセルの塗りつぶしが RAM 全域を破壊していた。
919        // 減算を一切しない純粋関数へ切り出し、ホスト側で試験している
920        // (`kernel::draw_clip` / `tests/src/test_draw_clip.rs`)。
921        let (cx0, cy0, cx1, cy1) =
922            match crate::kernel::draw_clip::clip_fill_rect(x0, y0, x1, y1, w, h, self.clip.get()) {
923                Some(r) => r,
924                None => return,
925            };
926
927        let rect_w = cx1 - cx0 + 1;
928        let rect_h = cy1 - cy0 + 1;
929        let stride = self.resolve_stride();
930
931        if rect_w * rect_h >= DMA_THRESHOLD_PIXELS {
932            let dst_offset = (cy0 * stride + cx0) * 4;
933            let color_val = color.to_hardware_format();
934            crate::kernel::dma::dma_fill_rect(
935                5,
936                &color_val as *const _ as u32,
937                target_ptr as u32 + dst_offset,
938                rect_w,
939                rect_h,
940                stride * 4,
941            );
942        } else {
943            let c = color.to_hardware_format();
944            for y in cy0..=cy1 {
945                unsafe {
946                    let row_ptr = target_ptr.add((y * stride + cx0) as usize);
947                    let count = (cx1 - cx0 + 1) as usize;
948                    let slice = core::slice::from_raw_parts_mut(row_ptr, count);
949                    slice.fill(c);
950                }
951            }
952        }
953    }
954
955    pub fn draw_char(&self, x: u32, y: u32, c: char, color: Color) {
956        let code = c as usize;
957        if code >= 256 {
958            return;
959        }
960        let font = &FONT_DATA[code];
961        let (target_ptr, w, h) = self.resolve_target();
962        let stride = self.resolve_stride();
963        let clip = self.clip.get();
964        let hw_color = color.to_hardware_format();
965
966        for i in 0..16u32 {
967            let py = y + i;
968            if py >= h {
969                continue;
970            }
971            if let Some((_, cy0, _, cy1)) = clip {
972                if py < cy0 || py >= cy1 {
973                    continue;
974                }
975            }
976            let d = font[i as usize];
977            if d == 0 {
978                continue;
979            }
980            unsafe {
981                let row_ptr = target_ptr.add((py * stride) as usize);
982                for bit in 0..8u32 {
983                    if (d & (0x80 >> bit)) != 0 {
984                        let px = x + bit;
985                        if px < w {
986                            if let Some((cx0, _, cx1, _)) = clip {
987                                if px < cx0 || px >= cx1 {
988                                    continue;
989                                }
990                            }
991                            core::ptr::write_volatile(row_ptr.add(px as usize), hw_color);
992                        }
993                    }
994                }
995            }
996        }
997    }
998
999    pub fn draw_string(&self, x: u32, y: u32, s: &str, color: Color) {
1000        let mut cur_x = x;
1001        for c in s.chars() {
1002            self.draw_char(cur_x, y, c, color);
1003            cur_x += 8;
1004        }
1005    }
1006
1007    pub fn measure_string_monospace(&self, s: &str) -> u32 {
1008        let mut font_lock = crate::kernel::vector_font::GLOBAL_TERMINAL_FONT.lock();
1009        if let Some(ref mut font) = *font_lock {
1010            font.get_string_width(s, 16)
1011        } else {
1012            let mut width = 0;
1013            for c in s.chars() {
1014                width += if (c as u32) < 128 { 8 } else { 16 };
1015            }
1016            width
1017        }
1018    }
1019
1020    pub fn draw_string_monospace(&self, x: u32, y: u32, s: &str, color: Color) {
1021        let mut font_lock = crate::kernel::vector_font::GLOBAL_TERMINAL_FONT.lock();
1022        if let Some(font) = font_lock.as_mut() {
1023            let mut cur_x = x as i32;
1024            let mut cur_y = y as i32;
1025            let (target_ptr, w, h) = self.resolve_target();
1026            let stride = self.resolve_stride();
1027            let clip = self.clip.get();
1028
1029            for c in s.chars() {
1030                if c == '\n' {
1031                    cur_x = x as i32;
1032                    cur_y += 16;
1033                    continue;
1034                }
1035
1036                if let Some(glyph) = font.get_glyph(c, 16) {
1037                    let gx = cur_x + glyph.x_offset;
1038                    let gy = cur_y + glyph.y_offset + (16 * 14 / 16);
1039
1040                    for row in 0..glyph.height {
1041                        let py = gy + row as i32;
1042                        if py < 0 || py >= h as i32 {
1043                            continue;
1044                        }
1045                        if let Some((_, cy0, _, cy1)) = clip {
1046                            if (py as u32) < cy0 || (py as u32) >= cy1 {
1047                                continue;
1048                            }
1049                        }
1050                        let row_offset = (py as usize) * (stride as usize);
1051
1052                        for col in 0..glyph.width {
1053                            let px = gx + col as i32;
1054                            if px < 0 || px >= w as i32 {
1055                                continue;
1056                            }
1057                            if let Some((cx0, _, cx1, _)) = clip {
1058                                if (px as u32) < cx0 || (px as u32) >= cx1 {
1059                                    continue;
1060                                }
1061                            }
1062                            let alpha = glyph.data[(row * glyph.width + col) as usize];
1063                            if alpha > 0 {
1064                                unsafe {
1065                                    let ptr = target_ptr.add(row_offset + px as usize);
1066                                    let bg_val = Color::from_hardware_format(core::ptr::read_volatile(ptr)).0 | 0xFF00_0000;
1067                                    let blended = crate::os_lib::web_engine::alpha_blend::blend_argb_over(
1068                                        ((alpha as u32) << 24) | (color.0 & 0x00FF_FFFF),
1069                                        bg_val,
1070                                    );
1071                                    core::ptr::write_volatile(ptr, Color(blended).to_hardware_format());
1072                                }
1073                            }
1074                        }
1075                    }
1076                    cur_x += glyph.advance as i32;
1077                } else {
1078                    let char_width = if (c as u32) > 127 { 16 } else { 8 };
1079                    cur_x += char_width;
1080                }
1081
1082                if cur_x > w as i32 - 16 {
1083                    break;
1084                }
1085            }
1086        } else {
1087            let mut cur_x = x;
1088            let (_, w, _) = self.resolve_target();
1089            for c in s.chars() {
1090                if c == '\n' {
1091                    continue;
1092                }
1093                let code = c as u32;
1094                if code < 128 {
1095                    self.draw_char(cur_x, y, c, color);
1096                    cur_x += 8;
1097                } else {
1098                    if let Some(bitmap) = crate::kernel::font_ja::get_font_bitmap(code) {
1099                        self.draw_char_ja_16x16(cur_x, y, bitmap, color);
1100                    } else {
1101                        self.draw_char_ja_tofu(cur_x, y, color);
1102                    }
1103                    cur_x += 16;
1104                }
1105                if cur_x >= w.saturating_sub(8) {
1106                    break;
1107                }
1108            }
1109        }
1110    }
1111
1112    /// 等幅ベクターフォントを用いて、指定した背景色で文字列を描画します(カラーキャッシュによる高速化版)
1113    pub fn draw_string_monospace_bg(&self, x: u32, y: u32, s: &str, color: Color, bg: Color) {
1114        let mut font_lock = crate::kernel::vector_font::GLOBAL_TERMINAL_FONT.lock();
1115        if let Some(ref mut font) = *font_lock {
1116            let mut cur_x = x as i32;
1117            let mut cur_y = y as i32;
1118            let (_, w, h) = self.resolve_target();
1119
1120            for c in s.chars() {
1121                if c == '\n' {
1122                    cur_x = x as i32;
1123                    cur_y += 16;
1124                    continue;
1125                }
1126
1127                if let Some(glyph) = font.get_color_glyph(c, 16, color.0, bg.0) {
1128                    let gx = cur_x + glyph.x_offset;
1129                    let gy = cur_y + glyph.y_offset + (16 * 14 / 16);
1130
1131                    for row in 0..glyph.height {
1132                        let py = gy + row as i32;
1133                        if py < 0 || py >= h as i32 {
1134                            continue;
1135                        }
1136                        for col in 0..glyph.width {
1137                            let px = gx + col as i32;
1138                            if px < 0 || px >= w as i32 {
1139                                continue;
1140                            }
1141                            let pixel_color = glyph.data[(row * glyph.width + col) as usize];
1142                            if (pixel_color >> 24) > 0 {
1143                                // 完全透明ピクセルはスキップ
1144                                self.draw_pixel(px as u32, py as u32, Color(pixel_color));
1145                            }
1146                        }
1147                    }
1148                    cur_x += glyph.advance as i32;
1149                } else {
1150                    let char_width = if (c as u32) > 127 { 16 } else { 8 };
1151                    cur_x += char_width;
1152                }
1153
1154                if cur_x > w as i32 - 16 {
1155                    break;
1156                }
1157            }
1158        } else {
1159            // モノスペースフォント未初期化時の代替描画 (背景色塗りつぶし付き)
1160            let mut cur_x = x;
1161            let (_, w, _) = self.resolve_target();
1162            for c in s.chars() {
1163                if c == '\n' {
1164                    continue;
1165                }
1166                let code = c as u32;
1167                let char_w = if code < 128 { 8u32 } else { 16u32 };
1168                // 背景で塗りつぶしてから描画
1169                self.boxfill(cur_x, y, cur_x + char_w, y + 16, bg);
1170                if code < 128 {
1171                    self.draw_char(cur_x, y, c, color);
1172                    cur_x += 8;
1173                } else {
1174                    if let Some(bitmap) = crate::kernel::font_ja::get_font_bitmap(code) {
1175                        self.draw_char_ja_16x16(cur_x, y, bitmap, color);
1176                    } else {
1177                        self.draw_char_ja_tofu(cur_x, y, color);
1178                    }
1179                    cur_x += 16;
1180                }
1181                if cur_x >= w.saturating_sub(8) {
1182                    break;
1183                }
1184            }
1185        }
1186    }
1187
1188    pub fn measure_string_ja(&self, s: &str, size: u32) -> u32 {
1189        let mut width = 0;
1190        let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
1191        if let Some(ref mut font) = *font_lock {
1192            for c in s.chars() {
1193                if let Some(glyph) = font.get_glyph(c, size) {
1194                    width += glyph.advance;
1195                } else {
1196                    width += if (c as u32) < 128 { size / 2 } else { size };
1197                }
1198            }
1199        } else {
1200            for c in s.chars() {
1201                width += if (c as u32) < 128 { 8 } else { 16 };
1202            }
1203        }
1204        width
1205    }
1206
1207    /// 日本語(UTF-8)の半角・全角が混在した文字列を描画します
1208    pub fn draw_string_ja(&self, x: u32, y: u32, s: &str, color: Color) {
1209        self.draw_string_ja_size(x, y, s, 16, color);
1210    }
1211
1212    pub fn draw_string_ja_size(&self, x: u32, y: u32, s: &str, size: u32, color: Color) {
1213        self.draw_string_ja_size_ext(x, y, s, size, color, false);
1214    }
1215
1216    pub fn draw_string_ja_size_ext(
1217        &self,
1218        x: u32,
1219        y: u32,
1220        s: &str,
1221        size: u32,
1222        color: Color,
1223        italic: bool,
1224    ) {
1225        let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
1226        if let Some(font) = font_lock.as_mut() {
1227            let mut cur_x = x as i32;
1228            let mut cur_y = y as i32;
1229            let (_, w, h) = self.resolve_target();
1230
1231            for c in s.chars() {
1232                if c == '\n' {
1233                    cur_x = x as i32;
1234                    cur_y += size as i32;
1235                    continue;
1236                }
1237
1238                if let Some(glyph) = font.get_glyph(c, size) {
1239                    let gx = cur_x + glyph.x_offset;
1240                    let gy = cur_y + glyph.y_offset + (size as i32 * 14 / 16);
1241
1242                    for row in 0..glyph.height {
1243                        let py = gy + row as i32;
1244                        if py < 0 || py >= h as i32 {
1245                            continue;
1246                        }
1247                        for col in 0..glyph.width {
1248                            let mut px = gx + col as i32;
1249                            if italic {
1250                                // Yの高さ(下端に近いほどシフト量を小さく、上端に近いほどシフト量を大きくする)
1251                                // ベースラインに近い下端(row = glyph.height - 1)ではシフト量ほぼ0
1252                                // 上端(row = 0)では、(glyph.height * 2 / 7) 程度のシフトを加える
1253                                let shift = ((glyph.height as i32 - 1 - row as i32) * 2) / 7;
1254                                px += shift;
1255                            }
1256                            if px < 0 || px >= w as i32 {
1257                                continue;
1258                            }
1259                            let alpha = glyph.data[(row * glyph.width + col) as usize];
1260                            self.draw_pixel_alpha(px as u32, py as u32, color, alpha);
1261                        }
1262                    }
1263                    cur_x += glyph.advance as i32;
1264                } else {
1265                    let code = c as u32;
1266                    let char_width = if code > 127 {
1267                        size as i32
1268                    } else {
1269                        size as i32 / 2
1270                    };
1271                    if code < 128 {
1272                        let mut cx = cur_x;
1273                        if italic {
1274                            cx += 2;
1275                        }
1276                        self.draw_char(cx as u32, cur_y as u32, c, color);
1277                    } else {
1278                        // ビットマップフォントでフォールバック描画
1279                        if let Some(bitmap) = crate::kernel::font_ja::get_font_bitmap(code) {
1280                            // 16x16 に拡大して描画(または現在のサイズに応じたスケール)
1281                            // 簡易的に直接 draw_char_ja_16x16 を呼ぶ
1282                            let mut cx = cur_x;
1283                            if italic {
1284                                cx += 3;
1285                            }
1286                            self.draw_char_ja_16x16(cx as u32, cur_y as u32, bitmap, color);
1287                        } else {
1288                            self.draw_char_ja_tofu(cur_x as u32, cur_y as u32, color);
1289                        }
1290                    }
1291                    cur_x += char_width;
1292                }
1293
1294                if cur_x > w as i32 - size as i32 {
1295                    cur_x = x as i32;
1296                    cur_y += size as i32;
1297                    if cur_y >= h as i32 {
1298                        break;
1299                    }
1300                }
1301            }
1302        } else {
1303            let mut cur_x = x;
1304            for c in s.chars() {
1305                let code = c as u32;
1306                let mut cx = cur_x;
1307                if italic {
1308                    cx += 2;
1309                }
1310                if code < 128 {
1311                    self.draw_char(cx, y, c, color);
1312                    cur_x += size / 2;
1313                } else {
1314                    if let Some(bitmap) = crate::kernel::font_ja::get_font_bitmap(code) {
1315                        self.draw_char_ja_16x16(cx, y, bitmap, color);
1316                    } else {
1317                        self.draw_char_ja_tofu(cx, y, color);
1318                    }
1319                    cur_x += size;
1320                }
1321            }
1322        }
1323    }
1324
1325    pub fn draw_string_ja_size_bg(
1326        &self,
1327        x: u32,
1328        y: u32,
1329        s: &str,
1330        size: u32,
1331        color: Color,
1332        bg: Color,
1333    ) {
1334        self.draw_string_ja_size_ext_bg(x, y, s, size, color, false, bg);
1335    }
1336
1337    pub fn draw_string_ja_size_ext_bg(
1338        &self,
1339        x: u32,
1340        y: u32,
1341        s: &str,
1342        size: u32,
1343        color: Color,
1344        italic: bool,
1345        bg: Color,
1346    ) {
1347        self.draw_string_ja_size_ext_bg_ls(x, y, s, size, color, italic, bg, 0);
1348    }
1349
1350    /// letter-spacing 対応版。ls(px, 負も可) を各文字 advance に加える。
1351    #[allow(clippy::too_many_arguments)]
1352    pub fn draw_string_ja_size_ext_bg_ls(
1353        &self,
1354        x: u32,
1355        y: u32,
1356        s: &str,
1357        size: u32,
1358        color: Color,
1359        italic: bool,
1360        bg: Color,
1361        ls: i32,
1362    ) {
1363        self.draw_string_ja_size_ext_bg_ls_ws(x, y, s, size, color, italic, bg, ls, 0);
1364    }
1365
1366    /// letter-spacing + word-spacing 対応版。半角スペース1文字ごとに ws(px, 負も可) も加算する。
1367    #[allow(clippy::too_many_arguments)]
1368    /// 計算量: **O(N × G)** — N は文字数、G は 1 グリフの面積(ピクセル)。
1369    ///
1370    /// グリフのラスタライズ結果は `vector_font::get_glyph` が
1371    /// `(文字, サイズ)` をキャッシュしているため、**輪郭からの再ラスタライズは
1372    /// 初回のみ**(キャッシュは 8000 件で全クリア)。
1373    /// 2 回目以降はキャッシュ済みビットマップの転送だけになる。
1374    ///
1375    /// 実測: sugi-lab.net の 1 フレームで 0〜2 tick。**描画コストの支配項ではない**。
1376    /// `font-family` を指定して描く。
1377    ///
1378    /// 【2026-08-03】`@font-face` で登録した Web フォントは
1379    /// `CUSTOM_FONTS` に入るが、**描画側が `font-family` を受け取らず
1380    /// `GLOBAL_VECTOR_FONT` しか見ていなかった**ため、
1381    /// 取得・展開・登録まで成功しても字形に一切反映されなかった。
1382    ///
1383    /// `family` はカンマ区切りのリストをそのまま渡してよい。
1384    /// 先頭から順に見て、登録済みの family が見つかればそれで描く。
1385    /// 見つからなければ従来どおり既定フォントで描く(フォールバック)。
1386    ///
1387    /// 計算量: **O(F + N×G)** — F は family リストの要素数、
1388    /// N は文字数、G は 1 グリフの面積。
1389    #[allow(clippy::too_many_arguments)]
1390    pub fn draw_string_family(
1391        &self,
1392        x: u32,
1393        y: u32,
1394        s: &str,
1395        size: u32,
1396        color: Color,
1397        italic: bool,
1398        bg: Color,
1399        ls: i32,
1400        ws: i32,
1401        family_list: &str,
1402        bold: bool,
1403    ) {
1404        if !family_list.is_empty() {
1405            // 書体選択は計測側(`vector_font::with_family_font`)と共通(2026-09-26 一本化)。
1406            // 希望ウェイト → 文字列を描ける最も近いウェイト → family 名だけのキー、
1407            // さらに総称名(sans-serif 等)はページで読み込まれた該当系統の Web フォントへ。
1408            let drawn = crate::kernel::vector_font::with_family_font(s, family_list, bold, |font| {
1409                self.draw_glyphs_with_font(font, x, y, s, size, color, italic, bg, ls, ws);
1410            });
1411            if drawn.is_some() {
1412                return;
1413            }
1414            // 一致するフォントが無くて既定へ落ちたことを記録する。
1415            // 黙ってフォールバックすると「登録したのに使われない」理由が
1416            // 分からない(`spec/logging_policy.md` L-1)。
1417            if !family_list.is_empty() {
1418                static MISS: spin::Mutex<alloc::collections::BTreeSet<alloc::string::String>> =
1419                    spin::Mutex::new(alloc::collections::BTreeSet::new());
1420                let mut m = MISS.lock();
1421                if m.insert(alloc::string::String::from(family_list)) {
1422                    crate::warn!("[FONT] 該当フォント無しで既定へ family={:?}", family_list);
1423                }
1424            }
1425        }
1426        self.draw_string_ja_size_ext_bg_ls_ws(x, y, s, size, color, italic, bg, ls, ws);
1427    }
1428
1429    #[allow(clippy::too_many_arguments)]
1430    pub fn draw_string_ja_size_ext_bg_ls_ws(
1431        &self,
1432        x: u32,
1433        y: u32,
1434        s: &str,
1435        size: u32,
1436        color: Color,
1437        italic: bool,
1438        bg: Color,
1439        ls: i32,
1440        ws: i32,
1441    ) {
1442        let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
1443        if let Some(font) = font_lock.as_mut() {
1444            self.draw_glyphs_with_font(font, x, y, s, size, color, italic, bg, ls, ws);
1445        }
1446    }
1447
1448    /// 実際にグリフを並べて描く本体。フォントは呼び出し側が選んで渡す。
1449    #[allow(clippy::too_many_arguments)]
1450    fn draw_glyphs_with_font(
1451        &self,
1452        font: &mut crate::kernel::vector_font::VectorFont,
1453        x: u32,
1454        y: u32,
1455        s: &str,
1456        size: u32,
1457        color: Color,
1458        italic: bool,
1459        bg: Color,
1460        ls: i32,
1461        ws: i32,
1462    ) {
1463        // 引数で受け取ったフォントを使う。**ここで `GLOBAL_VECTOR_FONT` を
1464        // ロックしてはいけない**(呼び出し元が既に保持しており、
1465        // 再入不可の spin ロックで停止する。2026-08-03 に実際に踏んだ)。
1466        //
1467        // 下の `else` はベクタフォントが無い場合のビットマップ代替経路。
1468        // ここへは常にフォントが渡ってくるので実行されないが、
1469        // 元の構造をそのまま残す(削るとフォールバックの実装ごと失われる)。
1470        let font_opt: Option<&mut crate::kernel::vector_font::VectorFont> = Some(font);
1471        if let Some(font) = font_opt {
1472            let mut cur_x = x as i32;
1473            let mut cur_y = y as i32;
1474            let (target_ptr, w, h) = self.resolve_target();
1475            let stride = self.resolve_stride();
1476            let clip = self.clip.get();
1477
1478            for c in s.chars() {
1479                if c == '\n' {
1480                    cur_x = x as i32;
1481                    cur_y += size as i32;
1482                    continue;
1483                }
1484
1485                // 【2026-07-26診断】ヒーロー見出し(fs>=40)が画面に出ない件の切り分け。
1486                if size >= 40 {
1487                    static GLYPH_DIAG: core::sync::atomic::AtomicU32 =
1488                        core::sync::atomic::AtomicU32::new(0);
1489                    let n = GLYPH_DIAG.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1490                    if n < 6 {
1491                        let g = font.get_color_glyph(c, size, color.0, bg.0);
1492                        match g {
1493                            Some(gl) => crate::warn!(
1494                                "[GLYPH][DIAG] c={:?} size={} target_w={} target_h={} cur=({},{}) gw={} gh={} xoff={} yoff={} adv={} px0=0x{:08x}",
1495                                c, size, w, h, cur_x, cur_y,
1496                                gl.width, gl.height, gl.x_offset, gl.y_offset, gl.advance,
1497                                gl.data.first().copied().unwrap_or(0)
1498                            ),
1499                            None => crate::warn!(
1500                                "[GLYPH][DIAG] c={:?} size={} target_w={} target_h={} -> NO GLYPH (fallback path)",
1501                                c, size, w, h
1502                            ),
1503                        }
1504                    }
1505                }
1506
1507                if let Some(glyph) = font.get_color_glyph(c, size, color.0, bg.0) {
1508                    let gx = cur_x + glyph.x_offset;
1509                    let gy = cur_y + glyph.y_offset + (size as i32 * 14 / 16);
1510
1511                    let mut written = 0u32;
1512
1513                    for row in 0..glyph.height {
1514                        let py = gy + row as i32;
1515                        if py < 0 || py >= h as i32 {
1516                            continue;
1517                        }
1518                        if let Some((_, cy0, _, cy1)) = clip {
1519                            if (py as u32) < cy0 || (py as u32) >= cy1 {
1520                                continue;
1521                            }
1522                        }
1523                        let row_offset = (py as usize) * (stride as usize);
1524
1525                        for col in 0..glyph.width {
1526                            let mut px = gx + col as i32;
1527                            if italic {
1528                                let shift = ((glyph.height as i32 - 1 - row as i32) * 2) / 7;
1529                                px += shift;
1530                            }
1531                            if px < 0 || px >= w as i32 {
1532                                continue;
1533                            }
1534                            if let Some((cx0, _, cx1, _)) = clip {
1535                                if (px as u32) < cx0 || (px as u32) >= cx1 {
1536                                    continue;
1537                                }
1538                            }
1539                            let pixel_color = glyph.data[(row * glyph.width + col) as usize];
1540                            let a = (pixel_color >> 24) & 0xFF;
1541                            if a > 0 {
1542                                unsafe {
1543                                    let ptr = target_ptr.add(row_offset + px as usize);
1544                                    if a == 0xFF {
1545                                        core::ptr::write_volatile(ptr, Color(pixel_color).to_hardware_format());
1546                                    } else {
1547                                        let bg_val = Color::from_hardware_format(core::ptr::read_volatile(ptr)).0 | 0xFF00_0000;
1548                                        let blended = crate::os_lib::web_engine::alpha_blend::blend_argb_over(pixel_color, bg_val);
1549                                        core::ptr::write_volatile(ptr, Color(blended).to_hardware_format());
1550                                    }
1551                                }
1552                                written += 1;
1553                            }
1554                        }
1555                    }
1556                    if size >= 40 {
1557                        static PX_DIAG: core::sync::atomic::AtomicU32 =
1558                            core::sync::atomic::AtomicU32::new(0);
1559                        // どのフレームでヒーロー文字を描いたかを記録する
1560                        // (表示中フレームとの突き合わせ用。上限なしで常に更新)。
1561                        HERO_TEXT_FRAME.store(
1562                            FRAME_NO.load(core::sync::atomic::Ordering::Relaxed),
1563                            core::sync::atomic::Ordering::Relaxed,
1564                        );
1565                        if PX_DIAG.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 4 {
1566                            let (tp, tw, th) = self.resolve_target();
1567                            // 【2026-07-26】`written` は draw_pixel の**呼び出し回数**であって
1568                            // 実書き込み数ではない。draw_pixel はクリップ矩形の外なら
1569                            // 黙って return するため、クリップ状態を併せて記録する。
1570                            crate::warn!(
1571                                "[TARGET][TEXT] c={:?} target=0x{:x} w={} h={} calls={} gx={} gy={} clip={:?} rt=0x{:x} bb=0x{:x} vram=0x{:x} use_bb={}",
1572                                c, tp as usize, tw, th, written, gx, gy, self.clip.get(),
1573                                self.render_target.get() as usize,
1574                                self.back_buffer as usize,
1575                                self.vram as usize,
1576                                self.use_back_buffer.get()
1577                            );
1578                        }
1579                    }
1580                    cur_x += glyph.advance as i32 + ls + if c == ' ' { ws } else { 0 };
1581                } else {
1582                    let code = c as u32;
1583                    let char_width = if code > 127 {
1584                        size as i32
1585                    } else {
1586                        size as i32 / 2
1587                    };
1588                    if code < 128 {
1589                        let mut cx = cur_x;
1590                        if italic {
1591                            cx += 2;
1592                        }
1593                        // 背景色でクリアしてからフォールバック文字を描画
1594                        self.boxfill(
1595                            cx as u32,
1596                            cur_y as u32,
1597                            cx as u32 + (size / 2),
1598                            cur_y as u32 + size,
1599                            bg,
1600                        );
1601                        self.draw_char(cx as u32, cur_y as u32, c, color);
1602                    } else {
1603                        // ビットマップフォントでフォールバック描画
1604                        let mut cx = cur_x;
1605                        if italic {
1606                            cx += 3;
1607                        }
1608                        self.boxfill(
1609                            cx as u32,
1610                            cur_y as u32,
1611                            cx as u32 + size,
1612                            cur_y as u32 + size,
1613                            bg,
1614                        );
1615                        if let Some(bitmap) = crate::kernel::font_ja::get_font_bitmap(code) {
1616                            self.draw_char_ja_16x16(cx as u32, cur_y as u32, bitmap, color);
1617                        } else {
1618                            self.draw_char_ja_tofu(cx as u32, cur_y as u32, color);
1619                        }
1620                    }
1621                    cur_x += char_width + ls + if c == ' ' { ws } else { 0 };
1622                }
1623
1624                if cur_x > w as i32 - size as i32 {
1625                    cur_x = x as i32;
1626                    cur_y += size as i32;
1627                    if cur_y >= h as i32 {
1628                        break;
1629                    }
1630                }
1631            }
1632        } else {
1633            // ベクターフォント未初期化時の代替日本語・英語描画 (背景色クリア付き)
1634            let mut cur_x = x;
1635            let (_, _w, _) = self.resolve_target();
1636            for c in s.chars() {
1637                let code = c as u32;
1638                let mut cx = cur_x;
1639                if italic {
1640                    cx += 2;
1641                }
1642                let char_w = if code < 128 { size / 2 } else { size };
1643                self.boxfill(cx, y, cx + char_w, y + size, bg);
1644                if code < 128 {
1645                    self.draw_char(cx, y, c, color);
1646                    cur_x = (cur_x as i32 + size as i32 / 2 + ls).max(0) as u32;
1647                } else {
1648                    if let Some(bitmap) = crate::kernel::font_ja::get_font_bitmap(code) {
1649                        self.draw_char_ja_16x16(cx, y, bitmap, color);
1650                    } else {
1651                        self.draw_char_ja_tofu(cx, y, color);
1652                    }
1653                    cur_x = (cur_x as i32 + size as i32 + ls).max(0) as u32;
1654                }
1655            }
1656        }
1657    }
1658
1659    /// `text-emphasis-style`(傍点・圏点): `draw_string_ja_size_ext_bg_ls_ws` と同じ
1660    /// 文字送り幅の計算を再利用し、各文字(空白を除く)の中央上部に小さな印を描く。
1661    /// 横書き前提(`text-emphasis-position` は非対応で常に上に描く)。
1662    /// `mark` は "dot"(●)/"circle"(○)/"double-circle"(◎近似)/"triangle"(▲)/
1663    /// "sesame"(,に近い小さな2点)、またはそれ以外はカスタム1文字をそのまま
1664    /// 小さめのフォントで描く。
1665    pub fn draw_text_emphasis(
1666        &self,
1667        x: u32,
1668        y: u32,
1669        s: &str,
1670        size: u32,
1671        mark: &str,
1672        color: Color,
1673        ls: i32,
1674        ws: i32,
1675    ) {
1676        if mark.is_empty() {
1677            return;
1678        }
1679        let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
1680        let Some(font) = font_lock.as_mut() else {
1681            return;
1682        };
1683        let mut cur_x = x as i32;
1684        let mut cur_y = y as i32;
1685        let (_, w, h) = self.resolve_target();
1686        // マーク自体の半径(文字サイズの1/6程度。小さめに固定)。
1687        let r = ((size as i32) / 6).max(1);
1688        for c in s.chars() {
1689            if c == '\n' {
1690                cur_x = x as i32;
1691                cur_y += size as i32;
1692                continue;
1693            }
1694            // 文字送り幅は実描画(draw_string_ja_size_ext_bg_ls_ws)と同じ規則で測る。
1695            let advance = if let Some(glyph) = font.get_color_glyph(c, size, color.0, color.0) {
1696                glyph.advance as i32
1697            } else {
1698                let code = c as u32;
1699                if code > 127 {
1700                    size as i32
1701                } else {
1702                    size as i32 / 2
1703                }
1704            };
1705            if !c.is_whitespace() {
1706                let cx = cur_x + advance / 2;
1707                let cy = cur_y - r * 3;
1708                if cx >= 0 && cy >= 0 && (cx as u32) < w && (cy as u32) < h {
1709                    match mark {
1710                        "circle" => self.draw_circle_outline(cx as u32, cy as u32, r as u32, color),
1711                        "triangle" => self.draw_triangle_fill(cx as u32, cy as u32, r as u32, color),
1712                        "sesame" => {
1713                            self.draw_circle_fill((cx - r) as u32, cy as u32, (r / 2).max(1) as u32, color);
1714                            self.draw_circle_fill((cx + r) as u32, cy as u32, (r / 2).max(1) as u32, color);
1715                        }
1716                        // "dot"/"double-circle"/カスタム文字は全て塗りつぶし円で近似する
1717                        // (二重丸・カスタム字形の専用描画は非対応の簡略実装)。
1718                        _ => self.draw_circle_fill(cx as u32, cy as u32, r as u32, color),
1719                    }
1720                }
1721            }
1722            cur_x += advance + ls + if c == ' ' { ws } else { 0 };
1723        }
1724    }
1725
1726    /// 塗りつぶし円(emphasis mark 用の小さな円)。
1727    fn draw_circle_fill(&self, cx: u32, cy: u32, r: u32, color: Color) {
1728        let r_i = r as i32;
1729        for dy in -r_i..=r_i {
1730            for dx in -r_i..=r_i {
1731                if dx * dx + dy * dy <= r_i * r_i {
1732                    let px = cx as i32 + dx;
1733                    let py = cy as i32 + dy;
1734                    if px >= 0 && py >= 0 {
1735                        self.draw_pixel(px as u32, py as u32, color);
1736                    }
1737                }
1738            }
1739        }
1740    }
1741
1742    /// 輪郭のみの円(`circle` 用)。
1743    fn draw_circle_outline(&self, cx: u32, cy: u32, r: u32, color: Color) {
1744        let r_i = r as i32;
1745        let r_inner = (r_i - 1).max(0);
1746        for dy in -r_i..=r_i {
1747            for dx in -r_i..=r_i {
1748                let d2 = dx * dx + dy * dy;
1749                if d2 <= r_i * r_i && d2 >= r_inner * r_inner {
1750                    let px = cx as i32 + dx;
1751                    let py = cy as i32 + dy;
1752                    if px >= 0 && py >= 0 {
1753                        self.draw_pixel(px as u32, py as u32, color);
1754                    }
1755                }
1756            }
1757        }
1758    }
1759
1760    /// 塗りつぶし三角形(`triangle` 用。上向き)。
1761    fn draw_triangle_fill(&self, cx: u32, cy: u32, r: u32, color: Color) {
1762        let r_i = r as i32;
1763        for dy in 0..=(r_i * 2) {
1764            let py = cy as i32 - r_i + dy;
1765            if py < 0 {
1766                continue;
1767            }
1768            let half_w = (dy * r_i) / (r_i * 2).max(1);
1769            for dx in -half_w..=half_w {
1770                let px = cx as i32 + dx;
1771                if px >= 0 {
1772                    self.draw_pixel(px as u32, py as u32, color);
1773                }
1774            }
1775        }
1776    }
1777
1778    /// 8x8の日本語ビットマップデータを縦横2倍にして 16x16 で描画します (最近傍補間)
1779    fn draw_char_ja_16x16(&self, x: u32, y: u32, bitmap: &[u8; 8], color: Color) {
1780        for row in 0..8 {
1781            let byte = bitmap[row];
1782            for col in 0..8 {
1783                if (byte & (0x80 >> col)) != 0 {
1784                    let px = x + col as u32 * 2;
1785                    let py = y + row as u32 * 2;
1786                    self.draw_pixel(px, py, color);
1787                    self.draw_pixel(px + 1, py, color);
1788                    self.draw_pixel(px, py + 1, color);
1789                    self.draw_pixel(px + 1, py + 1, color);
1790                }
1791            }
1792        }
1793    }
1794
1795    /// フォントデータが不足している場合の 16x16 豆腐 (枠線) 描画
1796    fn draw_char_ja_tofu(&self, x: u32, y: u32, color: Color) {
1797        for dy in 0..16 {
1798            for dx in 0..16 {
1799                if dx == 0 || dx == 15 || dy == 0 || dy == 15 {
1800                    self.draw_pixel(x + dx, y + dy, color);
1801                }
1802            }
1803        }
1804    }
1805
1806    // チャットターミナル用の描画関数は terminal.rs 側で実装するため、
1807    // ここでは基本的な draw_pixel, boxfill, draw_string のみを提供する
1808
1809    /// SVGアイコンをラスタライズしてアルファブレンドで描画します。
1810    /// tint: 描画色(SVGの塗り色を上書きする)。alphaは元のアルファマスクを使用。
1811    pub fn draw_svg_icon(&self, x: u32, y: u32, svg_data: &[u8], size: u32, tint: Color) {
1812        if let Some(alpha_mask) = crate::os_lib::svg::get_or_rasterize(svg_data, size) {
1813            for row in 0..size {
1814                for col in 0..size {
1815                    let alpha = alpha_mask[(row * size + col) as usize];
1816                    if alpha > 0 {
1817                        self.draw_pixel_alpha(x + col, y + row, tint, alpha);
1818                    }
1819                }
1820            }
1821        }
1822    }
1823
1824    /// 簡易 BMP デコーダ。24ビットまたは32ビット無圧縮の BMP データを指定座標に、指定サイズへ Nearest Neighbor 拡大縮小して描画します。
1825    pub fn draw_bmp(
1826        &self,
1827        x: u32,
1828        y: u32,
1829        target_width: u32,
1830        target_height: u32,
1831        bmp_data: &[u8],
1832    ) -> Result<(), &'static str> {
1833        if bmp_data.len() < 54 {
1834            return Err("Data too short");
1835        }
1836        if &bmp_data[0..2] != b"BM" {
1837            return Err("Not a BMP file");
1838        }
1839
1840        let off_bits =
1841            u32::from_le_bytes(bmp_data[10..14].try_into().map_err(|_| "Invalid offset")?) as usize;
1842        let width = i32::from_le_bytes(bmp_data[18..22].try_into().map_err(|_| "Invalid width")?);
1843        let height = i32::from_le_bytes(bmp_data[22..26].try_into().map_err(|_| "Invalid height")?);
1844        let bit_count = u16::from_le_bytes(
1845            bmp_data[28..30]
1846                .try_into()
1847                .map_err(|_| "Invalid bitcount")?,
1848        );
1849        let compression = u32::from_le_bytes(
1850            bmp_data[30..34]
1851                .try_into()
1852                .map_err(|_| "Invalid compression")?,
1853        );
1854
1855        if compression != 0 {
1856            return Err("Only uncompressed BMP is supported");
1857        }
1858        if bit_count != 24 && bit_count != 32 {
1859            return Err("Only 24-bit or 32-bit BMP is supported");
1860        }
1861
1862        let abs_height = height.unsigned_abs();
1863        let is_bottom_up = height > 0;
1864        let bytes_per_pixel = (bit_count / 8) as usize;
1865        let row_stride = (width as usize * bytes_per_pixel).div_ceil(4) * 4;
1866
1867        if target_width == 0 || target_height == 0 {
1868            return Ok(());
1869        }
1870
1871        for row in 0..target_height {
1872            let src_row_idx = (row * abs_height) / target_height;
1873            let src_row = if is_bottom_up {
1874                abs_height - 1 - src_row_idx
1875            } else {
1876                src_row_idx
1877            };
1878            let row_offset = off_bits + (src_row as usize * row_stride);
1879            if row_offset + (width as usize * bytes_per_pixel) > bmp_data.len() {
1880                return Err("BMP pixel data out of bounds");
1881            }
1882            for col in 0..target_width {
1883                let src_col = (col * width as u32) / target_width;
1884                let pixel_offset = row_offset + (src_col as usize * bytes_per_pixel);
1885                let b = bmp_data[pixel_offset];
1886                let g = bmp_data[pixel_offset + 1];
1887                let r = bmp_data[pixel_offset + 2];
1888                let a = if bytes_per_pixel == 4 {
1889                    bmp_data[pixel_offset + 3]
1890                } else {
1891                    255
1892                };
1893
1894                let color = Color(0xFF000000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32));
1895                self.draw_pixel_alpha(x + col, y + row, color, a);
1896            }
1897        }
1898        Ok(())
1899    }
1900
1901    /// PNG, JPG, BMP をデコードし、指定サイズへ Nearest Neighbor 拡大縮小して描画します
1902    pub fn draw_image(
1903        &self,
1904        x: u32,
1905        y: u32,
1906        target_width: u32,
1907        target_height: u32,
1908        data: &[u8],
1909    ) -> Result<(), &'static str> {
1910        if target_width == 0 || target_height == 0 {
1911            return Ok(());
1912        }
1913
1914        if data.len() > 8 && &data[0..8] == b"\x89PNG\r\n\x1a\n" {
1915            let mut decoder = zune_png::PngDecoder::new(data);
1916            match decoder.decode_raw() {
1917                Ok(pixels) => {
1918                    let info = decoder.get_info().ok_or("Failed to get PNG info")?;
1919                    let width = info.width as u32;
1920                    let height = info.height as u32;
1921                    let bytes_per_pixel = pixels.len() / (width as usize * height as usize).max(1);
1922
1923                    for row in 0..target_height {
1924                        let src_row = (row * height) / target_height;
1925                        for col in 0..target_width {
1926                            let src_col = (col * width) / target_width;
1927                            let idx = ((src_row * width + src_col) as usize) * bytes_per_pixel;
1928                            if idx + bytes_per_pixel <= pixels.len() {
1929                                let (r, g, b, a) = match bytes_per_pixel {
1930                                    1 => (pixels[idx], pixels[idx], pixels[idx], 255),
1931                                    2 => (pixels[idx], pixels[idx], pixels[idx], pixels[idx + 1]),
1932                                    3 => (pixels[idx], pixels[idx + 1], pixels[idx + 2], 255),
1933                                    4 => (
1934                                        pixels[idx],
1935                                        pixels[idx + 1],
1936                                        pixels[idx + 2],
1937                                        pixels[idx + 3],
1938                                    ),
1939                                    _ => (0, 0, 0, 255),
1940                                };
1941                                let color = Color(
1942                                    0xFF000000
1943                                        | ((r as u32) << 16)
1944                                        | ((g as u32) << 8)
1945                                        | (b as u32),
1946                                );
1947                                self.draw_pixel_alpha(x + col, y + row, color, a);
1948                            }
1949                        }
1950                    }
1951                    Ok(())
1952                }
1953                Err(_) => Err("Failed to decode PNG"),
1954            }
1955        } else if data.len() > 2 && data[0] == 0xFF && data[1] == 0xD8 {
1956            let mut decoder = zune_jpeg::JpegDecoder::new(data);
1957            match decoder.decode() {
1958                Ok(pixels) => {
1959                    let info = decoder.info().ok_or("Failed to get JPEG info")?;
1960                    let width = info.width as u32;
1961                    let height = info.height as u32;
1962                    let bytes_per_pixel = pixels.len() / (width as usize * height as usize).max(1);
1963
1964                    for row in 0..target_height {
1965                        let src_row = (row * height) / target_height;
1966                        for col in 0..target_width {
1967                            let src_col = (col * width) / target_width;
1968                            let idx = ((src_row * width + src_col) as usize) * bytes_per_pixel;
1969                            if idx + bytes_per_pixel <= pixels.len() {
1970                                let (r, g, b) = match bytes_per_pixel {
1971                                    1 => (pixels[idx], pixels[idx], pixels[idx]),
1972                                    3 => (pixels[idx], pixels[idx + 1], pixels[idx + 2]),
1973                                    _ => (0, 0, 0),
1974                                };
1975                                let color = Color(
1976                                    0xFF000000
1977                                        | ((r as u32) << 16)
1978                                        | ((g as u32) << 8)
1979                                        | (b as u32),
1980                                );
1981                                self.draw_pixel_alpha(x + col, y + row, color, 255);
1982                            }
1983                        }
1984                    }
1985                    Ok(())
1986                }
1987                Err(_) => Err("Failed to decode JPEG"),
1988            }
1989        } else if data.len() > 2 && &data[0..2] == b"BM" {
1990            self.draw_bmp(x, y, target_width, target_height, data)
1991        } else {
1992            Err("Unsupported image format")
1993        }
1994    }
1995
1996    /// Draw a linear gradient rectangle. Interpolates between c1 and c2.
1997    /// If vertical=true, gradient goes top→bottom; otherwise left→right.
1998    pub fn draw_gradient_rect(
1999        &self,
2000        x0: u32,
2001        y0: u32,
2002        x1: u32,
2003        y1: u32,
2004        c1: Color,
2005        c2: Color,
2006        vertical: bool,
2007    ) {
2008        if x1 <= x0 || y1 <= y0 {
2009            return;
2010        }
2011        let r1 = ((c1.0 >> 16) & 0xFF) as f32;
2012        let g1 = ((c1.0 >> 8) & 0xFF) as f32;
2013        let b1 = (c1.0 & 0xFF) as f32;
2014        let r2 = ((c2.0 >> 16) & 0xFF) as f32;
2015        let g2 = ((c2.0 >> 8) & 0xFF) as f32;
2016        let b2 = (c2.0 & 0xFF) as f32;
2017        if vertical {
2018            let h = (y1 - y0) as f32;
2019            for dy in 0..(y1 - y0) {
2020                let t = dy as f32 / h.max(1.0);
2021                let r = ((1.0 - t) * r1 + t * r2) as u32;
2022                let g = ((1.0 - t) * g1 + t * g2) as u32;
2023                let b = ((1.0 - t) * b1 + t * b2) as u32;
2024                let c = 0xFF000000 | (r << 16) | (g << 8) | b;
2025                self.boxfill(x0, y0 + dy, x1, y0 + dy + 1, Color(c));
2026            }
2027        } else {
2028            let w = (x1 - x0) as f32;
2029            for dx in 0..(x1 - x0) {
2030                let t = dx as f32 / w.max(1.0);
2031                let r = ((1.0 - t) * r1 + t * r2) as u32;
2032                let g = ((1.0 - t) * g1 + t * g2) as u32;
2033                let b = ((1.0 - t) * b1 + t * b2) as u32;
2034                let c = 0xFF000000 | (r << 16) | (g << 8) | b;
2035                self.boxfill(x0 + dx, y0, x0 + dx + 1, y1, Color(c));
2036            }
2037        }
2038    }
2039
2040    /// `radial-gradient(circle, c1, c2)` — 矩形の中心を起点に、中心から最も遠い角までの
2041    /// 距離を半径として c1(中心)→c2(外周) へ線形補間する。楕円/位置指定などは非対応の簡略実装。
2042    pub fn draw_radial_gradient_rect(&self, x0: u32, y0: u32, x1: u32, y1: u32, c1: Color, c2: Color) {
2043        if x1 <= x0 || y1 <= y0 {
2044            return;
2045        }
2046        let r1 = ((c1.0 >> 16) & 0xFF) as f32;
2047        let g1 = ((c1.0 >> 8) & 0xFF) as f32;
2048        let b1 = (c1.0 & 0xFF) as f32;
2049        let r2 = ((c2.0 >> 16) & 0xFF) as f32;
2050        let g2 = ((c2.0 >> 8) & 0xFF) as f32;
2051        let b2 = (c2.0 & 0xFF) as f32;
2052        let w = (x1 - x0) as f32;
2053        let h = (y1 - y0) as f32;
2054        let cx = w / 2.0;
2055        let cy = h / 2.0;
2056        let max_dist = libm::sqrtf(cx * cx + cy * cy).max(1.0);
2057        for dy in 0..(y1 - y0) {
2058            for dx in 0..(x1 - x0) {
2059                let px = dx as f32 - cx;
2060                let py = dy as f32 - cy;
2061                let dist = libm::sqrtf(px * px + py * py);
2062                let t = (dist / max_dist).min(1.0);
2063                let r = ((1.0 - t) * r1 + t * r2) as u32;
2064                let g = ((1.0 - t) * g1 + t * g2) as u32;
2065                let b = ((1.0 - t) * b1 + t * b2) as u32;
2066                let c = 0xFF000000 | (r << 16) | (g << 8) | b;
2067                self.boxfill(x0 + dx, y0 + dy, x0 + dx + 1, y0 + dy + 1, Color(c));
2068            }
2069        }
2070    }
2071
2072    /// `conic-gradient(c1, c2)` — 矩形の中心を起点に、12時方向(真上)を角度0として
2073    /// 時計回りに掃引し c1(0°)→c2(360°直前)へ線形補間する(実ブラウザの2色既定と同様、
2074    /// 0°/360° の境界に色が不連続に戻る「継ぎ目」ができるのが正しい挙動)。
2075    /// `from`/位置指定・複数カラーストップは非対応の簡略実装。
2076    pub fn draw_conic_gradient_rect(&self, x0: u32, y0: u32, x1: u32, y1: u32, c1: Color, c2: Color) {
2077        if x1 <= x0 || y1 <= y0 {
2078            return;
2079        }
2080        let r1 = ((c1.0 >> 16) & 0xFF) as f32;
2081        let g1 = ((c1.0 >> 8) & 0xFF) as f32;
2082        let b1 = (c1.0 & 0xFF) as f32;
2083        let r2 = ((c2.0 >> 16) & 0xFF) as f32;
2084        let g2 = ((c2.0 >> 8) & 0xFF) as f32;
2085        let b2 = (c2.0 & 0xFF) as f32;
2086        let w = (x1 - x0) as f32;
2087        let h = (y1 - y0) as f32;
2088        let cx = w / 2.0;
2089        let cy = h / 2.0;
2090        const PI: f32 = core::f32::consts::PI;
2091        for dy in 0..(y1 - y0) {
2092            for dx in 0..(x1 - x0) {
2093                let px = dx as f32 - cx;
2094                let py = dy as f32 - cy;
2095                // atan2(px, -py): 真上(0,-1)方向を角度0とし、時計回りに増加させる。
2096                let mut angle = libm::atan2f(px, -py);
2097                if angle < 0.0 {
2098                    angle += 2.0 * PI;
2099                }
2100                let t = angle / (2.0 * PI); // 0.0-1.0 (0度→c1, 360度直前→c2)
2101                let r = ((1.0 - t) * r1 + t * r2) as u32;
2102                let g = ((1.0 - t) * g1 + t * g2) as u32;
2103                let b = ((1.0 - t) * b1 + t * b2) as u32;
2104                let c = 0xFF000000 | (r << 16) | (g << 8) | b;
2105                self.boxfill(x0 + dx, y0 + dy, x0 + dx + 1, y0 + dy + 1, Color(c));
2106            }
2107        }
2108    }
2109
2110    /// 周期内の位置 `t`(0.0-1.0)における `colors` のN色均等割り補間色を得る。
2111    /// `colors` が空/1色の場合は先頭色(無ければ黒)を返す。
2112    fn sample_repeating_colors(colors: &[Color], t: f32) -> u32 {
2113        let Some(&first) = colors.first() else {
2114            return 0xFF000000;
2115        };
2116        if colors.len() < 2 {
2117            return first.0;
2118        }
2119        let scaled = t * (colors.len() - 1) as f32;
2120        let seg = (scaled as usize).min(colors.len() - 2);
2121        let local_t = scaled - seg as f32;
2122        let c1 = colors[seg];
2123        let c2 = colors[seg + 1];
2124        let r1 = ((c1.0 >> 16) & 0xFF) as f32;
2125        let g1 = ((c1.0 >> 8) & 0xFF) as f32;
2126        let b1 = (c1.0 & 0xFF) as f32;
2127        let r2 = ((c2.0 >> 16) & 0xFF) as f32;
2128        let g2 = ((c2.0 >> 8) & 0xFF) as f32;
2129        let b2 = (c2.0 & 0xFF) as f32;
2130        let r = ((1.0 - local_t) * r1 + local_t * r2) as u32;
2131        let g = ((1.0 - local_t) * g1 + local_t * g2) as u32;
2132        let b = ((1.0 - local_t) * b1 + local_t * b2) as u32;
2133        0xFF000000 | (r << 16) | (g << 8) | b
2134    }
2135
2136    /// `repeating-linear-gradient(c1, c2, ..., cycle_px)` — `cycle_px` を周期として全色を
2137    /// 均等割りで補間しながら軸方向に繰り返す(`draw_gradient_rect` の周期版)。
2138    /// 以前は先頭2色 `c1`/`c2` 固定で3色目以降が失われていた。角度指定は引き続き非対応。
2139    pub fn draw_repeating_linear_gradient_rect(
2140        &self,
2141        x0: u32,
2142        y0: u32,
2143        x1: u32,
2144        y1: u32,
2145        colors: &[Color],
2146        cycle_px: i32,
2147        vertical: bool,
2148    ) {
2149        if x1 <= x0 || y1 <= y0 || colors.is_empty() {
2150            return;
2151        }
2152        let cycle = cycle_px.max(1) as f32;
2153        if vertical {
2154            for dy in 0..(y1 - y0) {
2155                let t = (dy as f32 % cycle) / cycle;
2156                let c = Self::sample_repeating_colors(colors, t);
2157                self.boxfill(x0, y0 + dy, x1, y0 + dy + 1, Color(c));
2158            }
2159        } else {
2160            for dx in 0..(x1 - x0) {
2161                let t = (dx as f32 % cycle) / cycle;
2162                let c = Self::sample_repeating_colors(colors, t);
2163                self.boxfill(x0 + dx, y0, x0 + dx + 1, y1, Color(c));
2164            }
2165        }
2166    }
2167
2168    /// `repeating-radial-gradient(c1, c2, ..., cycle_px)` — 矩形中心からの距離を `cycle_px`
2169    /// で割った余りを使い、同心円状に全色を均等割りで繰り返す。以前は先頭2色固定で
2170    /// 3色目以降が失われていた。形状/位置指定は引き続き非対応。
2171    pub fn draw_repeating_radial_gradient_rect(
2172        &self,
2173        x0: u32,
2174        y0: u32,
2175        x1: u32,
2176        y1: u32,
2177        colors: &[Color],
2178        cycle_px: i32,
2179    ) {
2180        if x1 <= x0 || y1 <= y0 || colors.is_empty() {
2181            return;
2182        }
2183        let cycle = cycle_px.max(1) as f32;
2184        let w = (x1 - x0) as f32;
2185        let h = (y1 - y0) as f32;
2186        let cx = w / 2.0;
2187        let cy = h / 2.0;
2188        for dy in 0..(y1 - y0) {
2189            for dx in 0..(x1 - x0) {
2190                let px = dx as f32 - cx;
2191                let py = dy as f32 - cy;
2192                let dist = libm::sqrtf(px * px + py * py);
2193                let t = (dist % cycle) / cycle;
2194                let c = Self::sample_repeating_colors(colors, t);
2195                self.boxfill(x0 + dx, y0 + dy, x0 + dx + 1, y0 + dy + 1, Color(c));
2196            }
2197        }
2198    }
2199
2200    /// Draw a semi-transparent shadow rectangle at offset (ox, oy).
2201    pub fn draw_shadow_rect(
2202        &self,
2203        x0: i32,
2204        y0: i32,
2205        x1: i32,
2206        y1: i32,
2207        ox: i32,
2208        oy: i32,
2209        color: Color,
2210    ) {
2211        if x1 <= x0 || y1 <= y0 {
2212            return;
2213        }
2214        let sx0 = (x0 + ox).max(0) as u32;
2215        let sy0 = (y0 + oy).max(0) as u32;
2216        let sx1 = (x1 + ox).max(0) as u32;
2217        let sy1 = (y1 + oy).max(0) as u32;
2218        let alpha = ((color.0 >> 24) & 0xFF) as u8;
2219        let alpha = if alpha == 0 { 80u8 } else { alpha }; // default semi-transparent
2220        if sx1 <= sx0 || sy1 <= sy0 {
2221            return;
2222        }
2223        for y in sy0..sy1 {
2224            for x in sx0..sx1 {
2225                self.draw_pixel_alpha(x, y, Color(color.0 | 0xFF000000), alpha);
2226            }
2227        }
2228    }
2229
2230    /// Draw a semi-transparent shadow rectangle with optional rounded corners.
2231    ///
2232    /// 【2026-09-26】`blur`(ぼかし半径)に対応。影の矩形の縁からの距離に応じて
2233    /// 濃度を下げる(`web_engine::box_shadow::coverage`。縁で半分、幅 `blur` の勾配)。
2234    /// 従来はぼかしを無視して矩形をそのまま塗っており、Chrome の淡い影と見た目が大きく違った。
2235    /// ぼかしがあるときの角丸は、ぼかし自体が角を丸めるので矩形の距離で近似する。
2236    /// 計算量: **O((W+blur)×(H+blur))**。
2237    #[allow(clippy::too_many_arguments)]
2238    pub fn draw_shadow_rect_rounded(
2239        &self,
2240        x0: i32,
2241        y0: i32,
2242        x1: i32,
2243        y1: i32,
2244        ox: i32,
2245        oy: i32,
2246        blur: i32,
2247        radius: (i32, i32, i32, i32),
2248        color: Color,
2249    ) {
2250        if x1 <= x0 || y1 <= y0 {
2251            return;
2252        }
2253        let alpha = ((color.0 >> 24) & 0xFF) as u8;
2254        let alpha = if alpha == 0 { 80u8 } else { alpha };
2255        let blur = blur.max(0);
2256        let half = (blur + 1) / 2;
2257        // 影の矩形(ずらした後)と、ぼかしで広がる描画範囲。
2258        let (rx0, ry0, rx1, ry1) = (x0 + ox, y0 + oy, x1 + ox, y1 + oy);
2259        let sx0 = (rx0 - half).max(0);
2260        let sy0 = (ry0 - half).max(0);
2261        let sx1 = (rx1 + half).clamp(0, self.width as i32);
2262        let sy1 = (ry1 + half).clamp(0, self.height as i32);
2263        if sx1 <= sx0 || sy1 <= sy0 {
2264            return;
2265        }
2266        let w = x1 - x0;
2267        let h = y1 - y0;
2268        let has_radius = radius != (0, 0, 0, 0);
2269        let rgb = Color(color.0 | 0xFF000000);
2270
2271        for y in sy0..sy1 {
2272            for x in sx0..sx1 {
2273                let a = if blur == 0 {
2274                    if has_radius
2275                        && !crate::os_lib::web_engine::draw_helpers::point_in_rounded_rect(
2276                            x - rx0,
2277                            y - ry0,
2278                            w,
2279                            h,
2280                            radius,
2281                        )
2282                    {
2283                        continue;
2284                    }
2285                    alpha
2286                } else {
2287                    // 矩形の縁からの符号付き距離(外が正)。
2288                    let dx = (rx0 - x).max(x - (rx1 - 1)).max(0) as f32;
2289                    let dy = (ry0 - y).max(y - (ry1 - 1)).max(0) as f32;
2290                    let d = if dx > 0.0 || dy > 0.0 {
2291                        libm::sqrtf(dx * dx + dy * dy)
2292                    } else {
2293                        -((x - rx0).min(rx1 - 1 - x).min(y - ry0).min(ry1 - 1 - y) as f32)
2294                    };
2295                    let c = crate::os_lib::web_engine::box_shadow::coverage(d, blur);
2296                    if c <= 0.0 {
2297                        continue;
2298                    }
2299                    (alpha as f32 * c) as u8
2300                };
2301                if a > 0 {
2302                    self.draw_pixel_alpha(x as u32, y as u32, rgb, a);
2303                }
2304            }
2305        }
2306    }
2307
2308    /// Draw a semi-transparent inset shadow rectangle inside (x0, y0, x1, y1).
2309    pub fn draw_inset_shadow_rect(
2310        &self,
2311        x0: i32,
2312        y0: i32,
2313        x1: i32,
2314        y1: i32,
2315        ox: i32,
2316        oy: i32,
2317        blur: i32,
2318        radius: (i32, i32, i32, i32),
2319        color: Color,
2320    ) {
2321        if x1 <= x0 || y1 <= y0 {
2322            return;
2323        }
2324        let ix0 = x0.max(0) as u32;
2325        let iy0 = y0.max(0) as u32;
2326        let ix1 = x1.max(0) as u32;
2327        let iy1 = y1.max(0) as u32;
2328        let alpha = ((color.0 >> 24) & 0xFF) as u8;
2329        let alpha = if alpha == 0 { 90u8 } else { alpha };
2330        let shadow_color = color.0 | 0xFF000000;
2331        let w = x1 - x0;
2332        let h = y1 - y0;
2333        let has_radius = radius != (0, 0, 0, 0);
2334        let shadow_depth = blur.clamp(4, 30);
2335
2336        for y in iy0..iy1 {
2337            let ry = (y as i32) - y0;
2338            for x in ix0..ix1 {
2339                let rx = (x as i32) - x0;
2340                if has_radius && !crate::os_lib::web_engine::draw_helpers::point_in_rounded_rect(rx, ry, w, h, radius) {
2341                    continue;
2342                }
2343                let d_left = rx - ox;
2344                let d_top = ry - oy;
2345                let d_right = (w - rx) + ox;
2346                let d_bottom = (h - ry) + oy;
2347                let min_dist = d_left.min(d_top).min(d_right).min(d_bottom);
2348                if min_dist >= 0 && min_dist < shadow_depth {
2349                    let factor = 1.0 - (min_dist as f32 / shadow_depth as f32);
2350                    let pix_alpha = ((alpha as f32) * factor) as u8;
2351                    if pix_alpha > 0 {
2352                        self.draw_pixel_alpha(x, y, Color(shadow_color), pix_alpha);
2353                    }
2354                }
2355            }
2356        }
2357    }
2358
2359    pub fn draw_rounded_rect_fill(
2360        &self,
2361        x0: i32,
2362        y0: i32,
2363        x1: i32,
2364        y1: i32,
2365        radius: (i32, i32, i32, i32),
2366        color: Color,
2367    ) {
2368        let (tl, tr, br, bl) = radius;
2369        if tl <= 0 && tr <= 0 && br <= 0 && bl <= 0 {
2370            self.boxfill(x0 as u32, y0 as u32, x1 as u32, y1 as u32, color);
2371            return;
2372        }
2373        let w = x1 - x0;
2374        let h = y1 - y0;
2375        
2376        let tl = tl.min(w / 2).min(h / 2).max(0);
2377        let tr = tr.min(w / 2).min(h / 2).max(0);
2378        let br = br.min(w / 2).min(h / 2).max(0);
2379        let bl = bl.min(w / 2).min(h / 2).max(0);
2380        
2381        // 中央の十字領域と4つの角領域を分けて塗るなど最適化できるが、
2382        // ひとまずシンプルに point_in_rounded_rect 相当の判定でピクセル描画するか、
2383        // あるいは矩形領域分割で描画する。
2384        // ここは OS のカーネル描画プリミティブなので、最適化のため領域分割する。
2385        
2386        // 1. 中央水平帯 (全体幅、高さは 上端最大R から 下端最大R)
2387        let top_max_r = tl.max(tr);
2388        let bot_max_r = bl.max(br);
2389        if top_max_r + bot_max_r <= h {
2390            self.boxfill(x0 as u32, (y0 + top_max_r) as u32, x1 as u32, (y1 - bot_max_r) as u32, color);
2391        }
2392        
2393        // 2. 上部帯 (左右の角丸を除く)
2394        if top_max_r > 0 {
2395            self.boxfill((x0 + tl) as u32, y0 as u32, (x1 - tr) as u32, (y0 + top_max_r) as u32, color);
2396        }
2397        
2398        // 3. 下部帯 (左右の角丸を除く)
2399        if bot_max_r > 0 {
2400            self.boxfill((x0 + bl) as u32, (y1 - bot_max_r) as u32, (x1 - br) as u32, y1 as u32, color);
2401        }
2402        
2403        // 4. 左上・右上・左下・右下の余白領域で角丸内側のピクセルを描画
2404        // 左上
2405        if tl > 0 {
2406            let r2 = tl * tl;
2407            for dy in 0..tl {
2408                for dx in 0..tl {
2409                    if dx * dx + dy * dy <= r2 {
2410                        self.draw_pixel((x0 + tl - 1 - dx) as u32, (y0 + tl - 1 - dy) as u32, color);
2411                    }
2412                }
2413            }
2414            // 上端最大Rとの差分を埋める
2415            if top_max_r > tl {
2416                self.boxfill(x0 as u32, (y0 + tl) as u32, (x0 + tl) as u32, (y0 + top_max_r) as u32, color);
2417            }
2418        }
2419        // 右上
2420        if tr > 0 {
2421            let r2 = tr * tr;
2422            for dy in 0..tr {
2423                for dx in 0..tr {
2424                    if dx * dx + dy * dy <= r2 {
2425                        self.draw_pixel((x1 - tr + dx) as u32, (y0 + tr - 1 - dy) as u32, color);
2426                    }
2427                }
2428            }
2429            if top_max_r > tr {
2430                self.boxfill((x1 - tr) as u32, (y0 + tr) as u32, x1 as u32, (y0 + top_max_r) as u32, color);
2431            }
2432        }
2433        // 左下
2434        if bl > 0 {
2435            let r2 = bl * bl;
2436            for dy in 0..bl {
2437                for dx in 0..bl {
2438                    if dx * dx + dy * dy <= r2 {
2439                        self.draw_pixel((x0 + bl - 1 - dx) as u32, (y1 - bl + dy) as u32, color);
2440                    }
2441                }
2442            }
2443            if bot_max_r > bl {
2444                self.boxfill(x0 as u32, (y1 - bot_max_r) as u32, (x0 + bl) as u32, (y1 - bl) as u32, color);
2445            }
2446        }
2447        // 右下
2448        if br > 0 {
2449            let r2 = br * br;
2450            for dy in 0..br {
2451                for dx in 0..br {
2452                    if dx * dx + dy * dy <= r2 {
2453                        self.draw_pixel((x1 - br + dx) as u32, (y1 - br + dy) as u32, color);
2454                    }
2455                }
2456            }
2457            if bot_max_r > br {
2458                self.boxfill((x1 - br) as u32, (y1 - bot_max_r) as u32, x1 as u32, (y1 - br) as u32, color);
2459            }
2460        }
2461    }
2462
2463    pub fn draw_rounded_rect_border(
2464        &self,
2465        x0: i32,
2466        y0: i32,
2467        x1: i32,
2468        y1: i32,
2469        radius: (i32, i32, i32, i32),
2470        color: Color,
2471        _border_style: &str,
2472    ) {
2473        // 【2026-08-05 発見・修正】完全透明(アルファ 0)の枠線は描かない。
2474        //
2475        // `draw_pixel` は互換上アルファ 0 を「不透明」として扱うため
2476        // (下位のほぼ全ての呼び出しがアルファを設定せず 0 のまま渡すので、
2477        //  仕様どおり透明にすると画面が消える)、
2478        // `border-color: transparent` の角丸枠が**黒い輪郭**として描かれていた。
2479        //
2480        // 実測: セクション見出しの周りと `#apps` の一覧に、
2481        // 実サイトに無い黒枠(角丸の輪郭・☐)が出ていた。
2482        if (color.0 >> 24) & 0xFF == 0 {
2483            return;
2484        }
2485        let (tl, tr, br, bl) = radius;
2486        if tl <= 0 && tr <= 0 && br <= 0 && bl <= 0 {
2487            // 直線枠線
2488            self.boxfill(x0 as u32, y0 as u32, x1 as u32, (y0 + 1) as u32, color);
2489            self.boxfill(x0 as u32, (y1 - 1) as u32, x1 as u32, y1 as u32, color);
2490            self.boxfill(x0 as u32, y0 as u32, (x0 + 1) as u32, y1 as u32, color);
2491            self.boxfill((x1 - 1) as u32, y0 as u32, x1 as u32, y1 as u32, color);
2492            return;
2493        }
2494        let w = x1 - x0;
2495        let h = y1 - y0;
2496        let tl = tl.min(w / 2).min(h / 2).max(0);
2497        let tr = tr.min(w / 2).min(h / 2).max(0);
2498        let br = br.min(w / 2).min(h / 2).max(0);
2499        let bl = bl.min(w / 2).min(h / 2).max(0);
2500
2501        // 直線部分 (上下左右)
2502        self.boxfill((x0 + tl) as u32, y0 as u32, (x1 - tr) as u32, (y0 + 1) as u32, color);
2503        self.boxfill((x0 + bl) as u32, (y1 - 1) as u32, (x1 - br) as u32, y1 as u32, color);
2504        self.boxfill(x0 as u32, (y0 + tl) as u32, (x0 + 1) as u32, (y1 - bl) as u32, color);
2505        self.boxfill((x1 - 1) as u32, (y0 + tr) as u32, x1 as u32, (y1 - br) as u32, color);
2506
2507        // 曲線部分
2508        let draw_arc = |cx: i32, cy: i32, r: i32, quadrant: i32| {
2509            if r <= 0 { return; }
2510            let mut x = r;
2511            let mut y = 0;
2512            let mut err = 0;
2513            while x >= y {
2514                let px1 = cx + x - 1; let py1 = cy + y;
2515                let px2 = cx + y - 1; let py2 = cy + x - 1;
2516                let px3 = cx - x; let py3 = cy + y;
2517                let px4 = cx - y; let py4 = cy + x - 1;
2518                let px5 = cx - x; let py5 = cy - y - 1;
2519                let px6 = cx - y; let py6 = cy - x;
2520                let px7 = cx + x - 1; let py7 = cy - y - 1;
2521                let px8 = cx + y - 1; let py8 = cy - x;
2522
2523                match quadrant {
2524                    1 => { // 右下 (br)
2525                        self.draw_pixel(px1 as u32, py1 as u32, color);
2526                        self.draw_pixel(px2 as u32, py2 as u32, color);
2527                    }
2528                    2 => { // 左下 (bl)
2529                        self.draw_pixel(px3 as u32, py3 as u32, color);
2530                        self.draw_pixel(px4 as u32, py4 as u32, color);
2531                    }
2532                    3 => { // 左上 (tl)
2533                        self.draw_pixel(px5 as u32, py5 as u32, color);
2534                        self.draw_pixel(px6 as u32, py6 as u32, color);
2535                    }
2536                    4 => { // 右上 (tr)
2537                        self.draw_pixel(px7 as u32, py7 as u32, color);
2538                        self.draw_pixel(px8 as u32, py8 as u32, color);
2539                    }
2540                    _ => {}
2541                }
2542
2543                if err <= 0 {
2544                    y += 1;
2545                    err += 2 * y + 1;
2546                }
2547                if err > 0 {
2548                    x -= 1;
2549                    err -= 2 * x + 1;
2550                }
2551            }
2552        };
2553
2554        draw_arc(x0 + tl, y0 + tl, tl, 3);
2555        draw_arc(x1 - tr, y0 + tr, tr, 4);
2556        draw_arc(x1 - br, y1 - br, br, 1);
2557        draw_arc(x0 + bl, y1 - bl, bl, 2);
2558    }
2559
2560
2561}
2562
2563/// 背景退避・描き戻し方式による動的マウスカーソル制御構造体
2564pub struct Mouse {
2565    pub x: u32,
2566    pub y: u32,
2567    pub is_clicked: bool,
2568    pub click_frame: u8,
2569    /// CSS `cursor: text` 用。矢印の代わりに I-beam(縦棒)を手続き的に描く。
2570    /// それ以外の非対応 `cursor` キーワード(pointer/wait/help 等)は既定の矢印にフォールバックする。
2571    pub is_text_cursor: bool,
2572}
2573
2574impl Mouse {
2575    pub const SIZE: u32 = 32;
2576
2577    pub const fn new(x: u32, y: u32) -> Self {
2578        Self {
2579            x,
2580            y,
2581            is_clicked: false,
2582            click_frame: 0,
2583            is_text_cursor: false,
2584        }
2585    }
2586
2587    pub fn draw(&self, screen: &Screen) {
2588        let (target_ptr, w, h) = screen.resolve_target();
2589        let stride = screen.resolve_stride();
2590        let cursor_size = Self::SIZE;
2591
2592        // クリック状態によるシフト(押し込み感)
2593        let (draw_x, draw_y) = if self.is_clicked {
2594            (self.x + 1, self.y + 1)
2595        } else {
2596            (self.x, self.y)
2597        };
2598
2599        if self.is_text_cursor {
2600            self.draw_text_cursor(screen, draw_x, draw_y);
2601            return;
2602        }
2603
2604        if let Some(alpha_mask) = crate::os_lib::svg::get_or_rasterize(ICON_CURSOR_64, cursor_size)
2605        {
2606            let theme = crate::kernel::config::get_config().theme;
2607            let mut c_fill = theme.mouse_cursor;
2608            let c_stroke = theme.mouse_stroke;
2609
2610            // クリック時は Fill 色を少し暗くする(明度を落とす)
2611            if self.is_clicked {
2612                let r = ((c_fill >> 16) & 0xFF) * 8 / 10;
2613                let g = ((c_fill >> 8) & 0xFF) * 8 / 10;
2614                let b = (c_fill & 0xFF) * 8 / 10;
2615                c_fill = 0xFF000000 | (r << 16) | (g << 8) | b;
2616            }
2617
2618            for row in 0..cursor_size {
2619                let py = draw_y + row;
2620                if py >= h {
2621                    continue;
2622                }
2623
2624                let max_col = (w.saturating_sub(draw_x)).min(cursor_size) as usize;
2625                if max_col == 0 {
2626                    continue;
2627                }
2628
2629                let offset = (py * stride + draw_x) as usize;
2630                unsafe {
2631                    let mut row_buf = [0u32; 32];
2632                    let dst = target_ptr.add(offset);
2633
2634                    // 裏画面から背景を取得
2635                    let src = if !screen.back_buffer.is_null() {
2636                        let src_offset = (py * w + draw_x) as usize;
2637                        screen.back_buffer.add(src_offset)
2638                    } else {
2639                        dst
2640                    };
2641
2642                    core::ptr::copy_nonoverlapping(src, row_buf.as_mut_ptr(), max_col);
2643
2644                    // マウスカーソルを重ね合わせ (アルファブレンド)
2645                    for col in 0..max_col {
2646                        let mask_idx = (row * cursor_size + col as u32) as usize;
2647                        let alpha = alpha_mask[mask_idx];
2648                        if alpha > 0 {
2649                            // 近傍をチェックしてエッジ(輪郭線)判定を行う
2650                            let mut is_edge = false;
2651                            if row == 0
2652                                || row == cursor_size - 1
2653                                || col == 0
2654                                || col as u32 == cursor_size - 1
2655                            {
2656                                is_edge = true;
2657                            } else {
2658                                // 上下左右斜め8マスのアルファ値をチェック
2659                                'outer: for dy in -1..=1 {
2660                                    for dx in -1..=1 {
2661                                        let ny = row as i32 + dy;
2662                                        let nx = col as i32 + dx;
2663                                        let n_idx = (ny * cursor_size as i32 + nx) as usize;
2664                                        if alpha_mask[n_idx] < 80 {
2665                                            is_edge = true;
2666                                            break 'outer;
2667                                        }
2668                                    }
2669                                }
2670                            }
2671
2672                            let bg = row_buf[col];
2673                            let r_bg = (bg >> 16) & 0xFF;
2674                            let g_bg = (bg >> 8) & 0xFF;
2675                            let b_bg = bg & 0xFF;
2676
2677                            // エッジなら縁の色 (c_stroke)、内側なら本体の色 (c_fill) を適用
2678                            let fg_color = if is_edge { c_stroke } else { c_fill };
2679                            let r_fg = (fg_color >> 16) & 0xFF;
2680                            let g_fg = (fg_color >> 8) & 0xFF;
2681                            let b_fg = fg_color & 0xFF;
2682
2683                            let a = alpha as u32;
2684                            let inv_a = 255 - a;
2685
2686                            let r = (r_fg * a + r_bg * inv_a) / 255;
2687                            let g = (g_fg * a + g_bg * inv_a) / 255;
2688                            let b = (b_fg * a + b_bg * inv_a) / 255;
2689
2690                            row_buf[col] = (r << 16) | (g << 8) | b;
2691                        }
2692                    }
2693
2694                    // VRAMへ一気に書き戻し
2695                    core::ptr::copy_nonoverlapping(row_buf.as_ptr(), dst, max_col);
2696                }
2697            }
2698
2699            // 集中線(クリック火花)エフェクト
2700            if self.is_clicked && self.click_frame > 0 && self.click_frame < 8 {
2701                let spark_color = theme.mouse_cursor; // カーソル本体と同色の火花
2702                let center_x = draw_x as i32;
2703                let center_y = draw_y as i32;
2704                let r1 = 4 + (self.click_frame as i32) * 2; // フレームと共に外へ広がる
2705                let r2 = r1 + 4; // 線の長さ
2706
2707                // 8方向の簡易ベクトル (dx, dy)
2708                let dirs = [
2709                    (0, -1), (1, -1), (1, 0), (1, 1),
2710                    (0, 1), (-1, 1), (-1, 0), (-1, -1)
2711                ];
2712
2713                for &(dx, dy) in dirs.iter() {
2714                    for r in r1..=r2 {
2715                        let px = center_x + dx * r;
2716                        let py = center_y + dy * r;
2717                        if px >= 0 && px < w as i32 && py >= 0 && py < h as i32 {
2718                            let offset = (py as u32 * stride + px as u32) as usize;
2719                            unsafe {
2720                                let dst = target_ptr.add(offset);
2721                                *dst = spark_color;
2722                            }
2723                        }
2724                    }
2725                }
2726            }
2727        }
2728    }
2729
2730    /// CSS `cursor: text` 用の I-beam カーソルを手続き的に描画する。
2731    /// 新規 SVG アイコン資産を追加せず、`draw_pixel` の直描きだけで完結させる
2732    /// (矢印(SVGラスタライズ)と異なり、コード自体の正しさだけで見た目を保証できる形状)。
2733    /// `Self::SIZE` 四方のバウンディングボックス内に収め、既存のダーティ領域管理
2734    /// (`flush_with_cursor`/`flush_dirty_pageflip` が `Self::SIZE` 四方を前提に
2735    /// 保存/復元・ダーティ判定している)と矛盾しないようにする。
2736    fn draw_text_cursor(&self, screen: &Screen, draw_x: u32, draw_y: u32) {
2737        let theme = crate::kernel::config::get_config().theme;
2738        let color = Color(theme.mouse_cursor);
2739        let size = Self::SIZE;
2740        let bar_h = size.saturating_sub(4);
2741        let cx = draw_x + size / 2;
2742        let top_y = draw_y + 2;
2743        let bottom_y = top_y + bar_h;
2744        // 上下の「serif」(横棒)
2745        for dx in 0..=(size / 4) {
2746            screen.draw_pixel(cx.saturating_sub(size / 8) + dx, top_y, color);
2747            screen.draw_pixel(cx.saturating_sub(size / 8) + dx, bottom_y, color);
2748        }
2749        // 縦棒
2750        for y in top_y..=bottom_y {
2751            screen.draw_pixel(cx, y, color);
2752        }
2753    }
2754
2755    /// マウスカーソルを新しい位置に移動します
2756    pub fn move_to(&mut self, screen: &Screen, new_x: u32, new_y: u32) {
2757        self.x = new_x.clamp(0, screen.width - 1);
2758        self.y = new_y.clamp(0, screen.height - 1);
2759    }
2760}
2761
2762// --------------------------------------------------------------------
2763// 起動時スプラッシュ画面表示 (Logo & Log Scroll)
2764// --------------------------------------------------------------------
2765pub static mut SPLASH_ACTIVE: bool = false;
2766pub static LOGO_PNG: &[u8] = include_bytes!("../../images/os_logo.png");
2767
2768static mut BOOT_LOG_COUNT: usize = 0;
2769static mut BOOT_LOG_BUFFER: [[u8; 128]; 16] = [[0; 128]; 16];
2770static mut BOOT_LOG_LENS: [usize; 16] = [0; 16];
2771
2772static mut CURRENT_LINE_BUFFER: [u8; 256] = [0; 256];
2773static mut CURRENT_LINE_LEN: usize = 0;
2774
2775/// # Safety
2776/// ブート時スプラッシュ専用。可変 static (`CURRENT_LINE_*`) にアクセスするため、
2777/// 単一スレッドのブートコンテキストからのみ呼び出すこと。
2778pub unsafe fn write_splash_char(c: u8) {
2779    if !SPLASH_ACTIVE {
2780        return;
2781    }
2782    if c == b'\n' {
2783        if CURRENT_LINE_LEN > 0 {
2784            if let Ok(s) = core::str::from_utf8(&CURRENT_LINE_BUFFER[..CURRENT_LINE_LEN]) {
2785                add_boot_log(s);
2786            }
2787            CURRENT_LINE_LEN = 0;
2788        }
2789    } else {
2790        if CURRENT_LINE_LEN < 255 {
2791            CURRENT_LINE_BUFFER[CURRENT_LINE_LEN] = c;
2792            CURRENT_LINE_LEN += 1;
2793        }
2794    }
2795}
2796
2797/// # Safety
2798/// ブート時スプラッシュ専用。可変 static にアクセスするため、
2799/// 単一スレッドのブートコンテキストからのみ呼び出すこと。
2800pub unsafe fn write_splash_str(s: &str) {
2801    if !SPLASH_ACTIVE {
2802        return;
2803    }
2804    for &b in s.as_bytes() {
2805        write_splash_char(b);
2806    }
2807}
2808
2809/// # Safety
2810/// ブート時スプラッシュ専用。可変 static (`BOOT_LOG*`) にアクセスするため、
2811/// 単一スレッドのブートコンテキストからのみ呼び出すこと。
2812pub unsafe fn add_boot_log(s: &str) {
2813    if !SPLASH_ACTIVE {
2814        return;
2815    }
2816    const MAX_LINE_CHARS: usize = 55;
2817
2818    for line in s.lines() {
2819        let line = line.trim_end();
2820        if line.is_empty() {
2821            continue;
2822        }
2823
2824        let mut chars = line.as_bytes();
2825        while !chars.is_empty() {
2826            let chunk_len = chars.len().min(MAX_LINE_CHARS);
2827            let chunk = &chars[..chunk_len];
2828            chars = &chars[chunk_len..];
2829
2830            if BOOT_LOG_COUNT < 16 {
2831                let idx = BOOT_LOG_COUNT;
2832                BOOT_LOG_BUFFER[idx][..chunk_len].copy_from_slice(chunk);
2833                BOOT_LOG_LENS[idx] = chunk_len;
2834                BOOT_LOG_COUNT += 1;
2835            } else {
2836                for i in 0..15 {
2837                    BOOT_LOG_BUFFER[i] = BOOT_LOG_BUFFER[i + 1];
2838                    BOOT_LOG_LENS[i] = BOOT_LOG_LENS[i + 1];
2839                }
2840                BOOT_LOG_BUFFER[15][..chunk_len].copy_from_slice(chunk);
2841                BOOT_LOG_LENS[15] = chunk_len;
2842            }
2843        }
2844    }
2845    redraw_splash();
2846}
2847
2848/// # Safety
2849/// ブート時スプラッシュ専用。可変 static (`SPLASH_ACTIVE` 等) と画面バッファに
2850/// アクセスするため、単一スレッドのブートコンテキストからのみ呼び出すこと。
2851pub unsafe fn draw_splash_logo() {
2852    if !SPLASH_ACTIVE {
2853        return;
2854    }
2855    if let Some(ref screen) = crate::CURRENT_SCREEN {
2856        screen.clear(Color(0xFF000000));
2857        let mut decoder = zune_png::PngDecoder::new(LOGO_PNG);
2858        if decoder.decode_raw().is_ok() {
2859            if let Some(info) = decoder.get_info() {
2860                let img_w = info.width as u32;
2861                let img_h = info.height as u32;
2862                let area_w = 480u32;
2863                let area_h = screen.height;
2864                let target_w = area_w.min(img_w);
2865                let target_h = (target_w * img_h) / img_w;
2866                let target_h = target_h.min(area_h);
2867                let target_w = (target_h * img_w) / img_h;
2868
2869                let x = (area_w - target_w) / 2 + 20;
2870                let y = (screen.height - target_h) / 2;
2871                let _ = screen.draw_image(x, y, target_w, target_h, LOGO_PNG);
2872            }
2873        }
2874        redraw_splash();
2875    }
2876}
2877
2878/// # Safety
2879/// ブート時スプラッシュ専用。可変 static と画面バッファにアクセスするため、
2880/// 単一スレッドのブートコンテキストからのみ呼び出すこと。
2881pub unsafe fn redraw_splash() {
2882    if !SPLASH_ACTIVE {
2883        return;
2884    }
2885    if let Some(ref screen) = crate::CURRENT_SCREEN {
2886        let area_x = 520u32;
2887        let area_y = 20u32;
2888        let area_w = screen.width.saturating_sub(area_x).saturating_sub(20);
2889        let area_h = screen.height.saturating_sub(area_y).saturating_sub(20);
2890        screen.boxfill(
2891            area_x,
2892            area_y,
2893            area_x + area_w,
2894            area_y + area_h,
2895            Color(0xFF000000),
2896        );
2897
2898        let start_y = (screen.height.saturating_sub(40)) as i32;
2899        let line_height = 28i32;
2900
2901        for i in 0..BOOT_LOG_COUNT {
2902            let y = start_y - (BOOT_LOG_COUNT - 1 - i) as i32 * line_height;
2903            if y >= area_y as i32 {
2904                if let Ok(s) = core::str::from_utf8(&BOOT_LOG_BUFFER[i][..BOOT_LOG_LENS[i]]) {
2905                    screen.draw_string_monospace(area_x + 10, y as u32, s, Color(0xFFCCCCCC));
2906                }
2907            }
2908        }
2909    }
2910}
2911
2912pub struct SplashWriter;
2913
2914impl core::fmt::Write for SplashWriter {
2915    fn write_str(&mut self, s: &str) -> core::fmt::Result {
2916        unsafe {
2917            write_splash_str(s);
2918        }
2919        Ok(())
2920    }
2921}