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            // 【2026-08-05】ウェイト付きキーを優先して引く。
1406            //
1407            // 同じ family でウェイト違いを定義する CSS(Font Awesome の
1408            // `.fas`=900 / `.far`=400 など)では、family 名だけで引くと
1409            // 後勝ちで登録された別ウェイトが返り、字形が見つからない。
1410            //
1411            // `RenderElement` は `font_bold: bool` しか持たないので、
1412            // 太字なら 900 → 700、通常なら 400 の順に試し、
1413            // 最後に family 名だけのキーへ落とす。
1414            let want: &[u16] = if bold { &[900, 700] } else { &[400] };
1415            let mut fonts = crate::kernel::vector_font::CUSTOM_FONTS.lock();
1416            for fam in family_list.split(',') {
1417                let key = fam
1418                    .trim()
1419                    .trim_matches('"')
1420                    .trim_matches('\'')
1421                    .to_lowercase();
1422                if key.is_empty() {
1423                    continue;
1424                }
1425                // ウェイト付きキーを先に試す。
1426                //
1427                // 【2026-08-05】ただし**そのフォント自身がこの文字列を描ける場合だけ**。
1428                // Google Fonts は unicode-range でファイルを分割するため、
1429                // 「ウェイト 700 の Noto Sans JP」が Latin だけのサブセットのことがある。
1430                // 確かめずに選ぶと見出しの漢字が欠ける
1431                // (実測: 「開発アプリケーション」→「□ 発プリケーション」)。
1432                //
1433                // 【2026-08-05 訂正】当初は先頭の非空白文字 1 つだけを見て
1434                // 「同じ文字体系が続くはず」としていたが、**誤りだった**。
1435                // 「IoT、情報家電、AI技術を駆使して」のように 1 つの文字列で
1436                // 書体が混ざる例はふつうにあり、先頭の `I` で Latin サブセットが
1437                // 選ばれて後続の日本語が豆腐(■)になっていた。
1438                // 判定は `kernel::font_select::covers_all`(純粋モジュール・試験あり)。
1439                let mut hit: Option<alloc::string::String> = None;
1440                for w in want {
1441                    let wk = crate::kernel::vector_font::font_key(&key, *w);
1442                    let ok = match fonts.get(&wk) {
1443                        Some(f) => crate::kernel::font_select::covers_all(s, |c| {
1444                            f.primary_has_glyph(c)
1445                        }),
1446                        None => false,
1447                    };
1448                    if ok {
1449                        hit = Some(wk);
1450                        break;
1451                    }
1452                }
1453                if let Some(wk) = hit {
1454                    if let Some(font) = fonts.get_mut(&wk) {
1455                        self.draw_glyphs_with_font(font, x, y, s, size, color, italic, bg, ls, ws);
1456                        return;
1457                    }
1458                }
1459                if let Some(font) = fonts.get_mut(&key) {
1460                    self.draw_glyphs_with_font(font, x, y, s, size, color, italic, bg, ls, ws);
1461                    return;
1462                }
1463            }
1464            // 一致するフォントが無くて既定へ落ちたことを記録する。
1465            // 黙ってフォールバックすると「登録したのに使われない」理由が
1466            // 分からない(`spec/logging_policy.md` L-1)。
1467            if !family_list.is_empty() {
1468                static MISS: spin::Mutex<alloc::collections::BTreeSet<alloc::string::String>> =
1469                    spin::Mutex::new(alloc::collections::BTreeSet::new());
1470                let mut m = MISS.lock();
1471                if m.insert(alloc::string::String::from(family_list)) {
1472                    crate::warn!("[FONT] 該当フォント無しで既定へ family={:?}", family_list);
1473                }
1474            }
1475        }
1476        self.draw_string_ja_size_ext_bg_ls_ws(x, y, s, size, color, italic, bg, ls, ws);
1477    }
1478
1479    #[allow(clippy::too_many_arguments)]
1480    pub fn draw_string_ja_size_ext_bg_ls_ws(
1481        &self,
1482        x: u32,
1483        y: u32,
1484        s: &str,
1485        size: u32,
1486        color: Color,
1487        italic: bool,
1488        bg: Color,
1489        ls: i32,
1490        ws: i32,
1491    ) {
1492        let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
1493        if let Some(font) = font_lock.as_mut() {
1494            self.draw_glyphs_with_font(font, x, y, s, size, color, italic, bg, ls, ws);
1495        }
1496    }
1497
1498    /// 実際にグリフを並べて描く本体。フォントは呼び出し側が選んで渡す。
1499    #[allow(clippy::too_many_arguments)]
1500    fn draw_glyphs_with_font(
1501        &self,
1502        font: &mut crate::kernel::vector_font::VectorFont,
1503        x: u32,
1504        y: u32,
1505        s: &str,
1506        size: u32,
1507        color: Color,
1508        italic: bool,
1509        bg: Color,
1510        ls: i32,
1511        ws: i32,
1512    ) {
1513        // 引数で受け取ったフォントを使う。**ここで `GLOBAL_VECTOR_FONT` を
1514        // ロックしてはいけない**(呼び出し元が既に保持しており、
1515        // 再入不可の spin ロックで停止する。2026-08-03 に実際に踏んだ)。
1516        //
1517        // 下の `else` はベクタフォントが無い場合のビットマップ代替経路。
1518        // ここへは常にフォントが渡ってくるので実行されないが、
1519        // 元の構造をそのまま残す(削るとフォールバックの実装ごと失われる)。
1520        let font_opt: Option<&mut crate::kernel::vector_font::VectorFont> = Some(font);
1521        if let Some(font) = font_opt {
1522            let mut cur_x = x as i32;
1523            let mut cur_y = y as i32;
1524            let (target_ptr, w, h) = self.resolve_target();
1525            let stride = self.resolve_stride();
1526            let clip = self.clip.get();
1527
1528            for c in s.chars() {
1529                if c == '\n' {
1530                    cur_x = x as i32;
1531                    cur_y += size as i32;
1532                    continue;
1533                }
1534
1535                // 【2026-07-26診断】ヒーロー見出し(fs>=40)が画面に出ない件の切り分け。
1536                if size >= 40 {
1537                    static GLYPH_DIAG: core::sync::atomic::AtomicU32 =
1538                        core::sync::atomic::AtomicU32::new(0);
1539                    let n = GLYPH_DIAG.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1540                    if n < 6 {
1541                        let g = font.get_color_glyph(c, size, color.0, bg.0);
1542                        match g {
1543                            Some(gl) => crate::warn!(
1544                                "[GLYPH][DIAG] c={:?} size={} target_w={} target_h={} cur=({},{}) gw={} gh={} xoff={} yoff={} adv={} px0=0x{:08x}",
1545                                c, size, w, h, cur_x, cur_y,
1546                                gl.width, gl.height, gl.x_offset, gl.y_offset, gl.advance,
1547                                gl.data.first().copied().unwrap_or(0)
1548                            ),
1549                            None => crate::warn!(
1550                                "[GLYPH][DIAG] c={:?} size={} target_w={} target_h={} -> NO GLYPH (fallback path)",
1551                                c, size, w, h
1552                            ),
1553                        }
1554                    }
1555                }
1556
1557                if let Some(glyph) = font.get_color_glyph(c, size, color.0, bg.0) {
1558                    let gx = cur_x + glyph.x_offset;
1559                    let gy = cur_y + glyph.y_offset + (size as i32 * 14 / 16);
1560
1561                    let mut written = 0u32;
1562
1563                    for row in 0..glyph.height {
1564                        let py = gy + row as i32;
1565                        if py < 0 || py >= h as i32 {
1566                            continue;
1567                        }
1568                        if let Some((_, cy0, _, cy1)) = clip {
1569                            if (py as u32) < cy0 || (py as u32) >= cy1 {
1570                                continue;
1571                            }
1572                        }
1573                        let row_offset = (py as usize) * (stride as usize);
1574
1575                        for col in 0..glyph.width {
1576                            let mut px = gx + col as i32;
1577                            if italic {
1578                                let shift = ((glyph.height as i32 - 1 - row as i32) * 2) / 7;
1579                                px += shift;
1580                            }
1581                            if px < 0 || px >= w as i32 {
1582                                continue;
1583                            }
1584                            if let Some((cx0, _, cx1, _)) = clip {
1585                                if (px as u32) < cx0 || (px as u32) >= cx1 {
1586                                    continue;
1587                                }
1588                            }
1589                            let pixel_color = glyph.data[(row * glyph.width + col) as usize];
1590                            let a = (pixel_color >> 24) & 0xFF;
1591                            if a > 0 {
1592                                unsafe {
1593                                    let ptr = target_ptr.add(row_offset + px as usize);
1594                                    if a == 0xFF {
1595                                        core::ptr::write_volatile(ptr, Color(pixel_color).to_hardware_format());
1596                                    } else {
1597                                        let bg_val = Color::from_hardware_format(core::ptr::read_volatile(ptr)).0 | 0xFF00_0000;
1598                                        let blended = crate::os_lib::web_engine::alpha_blend::blend_argb_over(pixel_color, bg_val);
1599                                        core::ptr::write_volatile(ptr, Color(blended).to_hardware_format());
1600                                    }
1601                                }
1602                                written += 1;
1603                            }
1604                        }
1605                    }
1606                    if size >= 40 {
1607                        static PX_DIAG: core::sync::atomic::AtomicU32 =
1608                            core::sync::atomic::AtomicU32::new(0);
1609                        // どのフレームでヒーロー文字を描いたかを記録する
1610                        // (表示中フレームとの突き合わせ用。上限なしで常に更新)。
1611                        HERO_TEXT_FRAME.store(
1612                            FRAME_NO.load(core::sync::atomic::Ordering::Relaxed),
1613                            core::sync::atomic::Ordering::Relaxed,
1614                        );
1615                        if PX_DIAG.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 4 {
1616                            let (tp, tw, th) = self.resolve_target();
1617                            // 【2026-07-26】`written` は draw_pixel の**呼び出し回数**であって
1618                            // 実書き込み数ではない。draw_pixel はクリップ矩形の外なら
1619                            // 黙って return するため、クリップ状態を併せて記録する。
1620                            crate::warn!(
1621                                "[TARGET][TEXT] c={:?} target=0x{:x} w={} h={} calls={} gx={} gy={} clip={:?} rt=0x{:x} bb=0x{:x} vram=0x{:x} use_bb={}",
1622                                c, tp as usize, tw, th, written, gx, gy, self.clip.get(),
1623                                self.render_target.get() as usize,
1624                                self.back_buffer as usize,
1625                                self.vram as usize,
1626                                self.use_back_buffer.get()
1627                            );
1628                        }
1629                    }
1630                    cur_x += glyph.advance as i32 + ls + if c == ' ' { ws } else { 0 };
1631                } else {
1632                    let code = c as u32;
1633                    let char_width = if code > 127 {
1634                        size as i32
1635                    } else {
1636                        size as i32 / 2
1637                    };
1638                    if code < 128 {
1639                        let mut cx = cur_x;
1640                        if italic {
1641                            cx += 2;
1642                        }
1643                        // 背景色でクリアしてからフォールバック文字を描画
1644                        self.boxfill(
1645                            cx as u32,
1646                            cur_y as u32,
1647                            cx as u32 + (size / 2),
1648                            cur_y as u32 + size,
1649                            bg,
1650                        );
1651                        self.draw_char(cx as u32, cur_y as u32, c, color);
1652                    } else {
1653                        // ビットマップフォントでフォールバック描画
1654                        let mut cx = cur_x;
1655                        if italic {
1656                            cx += 3;
1657                        }
1658                        self.boxfill(
1659                            cx as u32,
1660                            cur_y as u32,
1661                            cx as u32 + size,
1662                            cur_y as u32 + size,
1663                            bg,
1664                        );
1665                        if let Some(bitmap) = crate::kernel::font_ja::get_font_bitmap(code) {
1666                            self.draw_char_ja_16x16(cx as u32, cur_y as u32, bitmap, color);
1667                        } else {
1668                            self.draw_char_ja_tofu(cx as u32, cur_y as u32, color);
1669                        }
1670                    }
1671                    cur_x += char_width + ls + if c == ' ' { ws } else { 0 };
1672                }
1673
1674                if cur_x > w as i32 - size as i32 {
1675                    cur_x = x as i32;
1676                    cur_y += size as i32;
1677                    if cur_y >= h as i32 {
1678                        break;
1679                    }
1680                }
1681            }
1682        } else {
1683            // ベクターフォント未初期化時の代替日本語・英語描画 (背景色クリア付き)
1684            let mut cur_x = x;
1685            let (_, _w, _) = self.resolve_target();
1686            for c in s.chars() {
1687                let code = c as u32;
1688                let mut cx = cur_x;
1689                if italic {
1690                    cx += 2;
1691                }
1692                let char_w = if code < 128 { size / 2 } else { size };
1693                self.boxfill(cx, y, cx + char_w, y + size, bg);
1694                if code < 128 {
1695                    self.draw_char(cx, y, c, color);
1696                    cur_x = (cur_x as i32 + size as i32 / 2 + ls).max(0) as u32;
1697                } else {
1698                    if let Some(bitmap) = crate::kernel::font_ja::get_font_bitmap(code) {
1699                        self.draw_char_ja_16x16(cx, y, bitmap, color);
1700                    } else {
1701                        self.draw_char_ja_tofu(cx, y, color);
1702                    }
1703                    cur_x = (cur_x as i32 + size as i32 + ls).max(0) as u32;
1704                }
1705            }
1706        }
1707    }
1708
1709    /// `text-emphasis-style`(傍点・圏点): `draw_string_ja_size_ext_bg_ls_ws` と同じ
1710    /// 文字送り幅の計算を再利用し、各文字(空白を除く)の中央上部に小さな印を描く。
1711    /// 横書き前提(`text-emphasis-position` は非対応で常に上に描く)。
1712    /// `mark` は "dot"(●)/"circle"(○)/"double-circle"(◎近似)/"triangle"(▲)/
1713    /// "sesame"(,に近い小さな2点)、またはそれ以外はカスタム1文字をそのまま
1714    /// 小さめのフォントで描く。
1715    pub fn draw_text_emphasis(
1716        &self,
1717        x: u32,
1718        y: u32,
1719        s: &str,
1720        size: u32,
1721        mark: &str,
1722        color: Color,
1723        ls: i32,
1724        ws: i32,
1725    ) {
1726        if mark.is_empty() {
1727            return;
1728        }
1729        let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
1730        let Some(font) = font_lock.as_mut() else {
1731            return;
1732        };
1733        let mut cur_x = x as i32;
1734        let mut cur_y = y as i32;
1735        let (_, w, h) = self.resolve_target();
1736        // マーク自体の半径(文字サイズの1/6程度。小さめに固定)。
1737        let r = ((size as i32) / 6).max(1);
1738        for c in s.chars() {
1739            if c == '\n' {
1740                cur_x = x as i32;
1741                cur_y += size as i32;
1742                continue;
1743            }
1744            // 文字送り幅は実描画(draw_string_ja_size_ext_bg_ls_ws)と同じ規則で測る。
1745            let advance = if let Some(glyph) = font.get_color_glyph(c, size, color.0, color.0) {
1746                glyph.advance as i32
1747            } else {
1748                let code = c as u32;
1749                if code > 127 {
1750                    size as i32
1751                } else {
1752                    size as i32 / 2
1753                }
1754            };
1755            if !c.is_whitespace() {
1756                let cx = cur_x + advance / 2;
1757                let cy = cur_y - r * 3;
1758                if cx >= 0 && cy >= 0 && (cx as u32) < w && (cy as u32) < h {
1759                    match mark {
1760                        "circle" => self.draw_circle_outline(cx as u32, cy as u32, r as u32, color),
1761                        "triangle" => self.draw_triangle_fill(cx as u32, cy as u32, r as u32, color),
1762                        "sesame" => {
1763                            self.draw_circle_fill((cx - r) as u32, cy as u32, (r / 2).max(1) as u32, color);
1764                            self.draw_circle_fill((cx + r) as u32, cy as u32, (r / 2).max(1) as u32, color);
1765                        }
1766                        // "dot"/"double-circle"/カスタム文字は全て塗りつぶし円で近似する
1767                        // (二重丸・カスタム字形の専用描画は非対応の簡略実装)。
1768                        _ => self.draw_circle_fill(cx as u32, cy as u32, r as u32, color),
1769                    }
1770                }
1771            }
1772            cur_x += advance + ls + if c == ' ' { ws } else { 0 };
1773        }
1774    }
1775
1776    /// 塗りつぶし円(emphasis mark 用の小さな円)。
1777    fn draw_circle_fill(&self, cx: u32, cy: u32, r: u32, color: Color) {
1778        let r_i = r as i32;
1779        for dy in -r_i..=r_i {
1780            for dx in -r_i..=r_i {
1781                if dx * dx + dy * dy <= r_i * r_i {
1782                    let px = cx as i32 + dx;
1783                    let py = cy as i32 + dy;
1784                    if px >= 0 && py >= 0 {
1785                        self.draw_pixel(px as u32, py as u32, color);
1786                    }
1787                }
1788            }
1789        }
1790    }
1791
1792    /// 輪郭のみの円(`circle` 用)。
1793    fn draw_circle_outline(&self, cx: u32, cy: u32, r: u32, color: Color) {
1794        let r_i = r as i32;
1795        let r_inner = (r_i - 1).max(0);
1796        for dy in -r_i..=r_i {
1797            for dx in -r_i..=r_i {
1798                let d2 = dx * dx + dy * dy;
1799                if d2 <= r_i * r_i && d2 >= r_inner * r_inner {
1800                    let px = cx as i32 + dx;
1801                    let py = cy as i32 + dy;
1802                    if px >= 0 && py >= 0 {
1803                        self.draw_pixel(px as u32, py as u32, color);
1804                    }
1805                }
1806            }
1807        }
1808    }
1809
1810    /// 塗りつぶし三角形(`triangle` 用。上向き)。
1811    fn draw_triangle_fill(&self, cx: u32, cy: u32, r: u32, color: Color) {
1812        let r_i = r as i32;
1813        for dy in 0..=(r_i * 2) {
1814            let py = cy as i32 - r_i + dy;
1815            if py < 0 {
1816                continue;
1817            }
1818            let half_w = (dy * r_i) / (r_i * 2).max(1);
1819            for dx in -half_w..=half_w {
1820                let px = cx as i32 + dx;
1821                if px >= 0 {
1822                    self.draw_pixel(px as u32, py as u32, color);
1823                }
1824            }
1825        }
1826    }
1827
1828    /// 8x8の日本語ビットマップデータを縦横2倍にして 16x16 で描画します (最近傍補間)
1829    fn draw_char_ja_16x16(&self, x: u32, y: u32, bitmap: &[u8; 8], color: Color) {
1830        for row in 0..8 {
1831            let byte = bitmap[row];
1832            for col in 0..8 {
1833                if (byte & (0x80 >> col)) != 0 {
1834                    let px = x + col as u32 * 2;
1835                    let py = y + row as u32 * 2;
1836                    self.draw_pixel(px, py, color);
1837                    self.draw_pixel(px + 1, py, color);
1838                    self.draw_pixel(px, py + 1, color);
1839                    self.draw_pixel(px + 1, py + 1, color);
1840                }
1841            }
1842        }
1843    }
1844
1845    /// フォントデータが不足している場合の 16x16 豆腐 (枠線) 描画
1846    fn draw_char_ja_tofu(&self, x: u32, y: u32, color: Color) {
1847        for dy in 0..16 {
1848            for dx in 0..16 {
1849                if dx == 0 || dx == 15 || dy == 0 || dy == 15 {
1850                    self.draw_pixel(x + dx, y + dy, color);
1851                }
1852            }
1853        }
1854    }
1855
1856    // チャットターミナル用の描画関数は terminal.rs 側で実装するため、
1857    // ここでは基本的な draw_pixel, boxfill, draw_string のみを提供する
1858
1859    /// SVGアイコンをラスタライズしてアルファブレンドで描画します。
1860    /// tint: 描画色(SVGの塗り色を上書きする)。alphaは元のアルファマスクを使用。
1861    pub fn draw_svg_icon(&self, x: u32, y: u32, svg_data: &[u8], size: u32, tint: Color) {
1862        if let Some(alpha_mask) = crate::os_lib::svg::get_or_rasterize(svg_data, size) {
1863            for row in 0..size {
1864                for col in 0..size {
1865                    let alpha = alpha_mask[(row * size + col) as usize];
1866                    if alpha > 0 {
1867                        self.draw_pixel_alpha(x + col, y + row, tint, alpha);
1868                    }
1869                }
1870            }
1871        }
1872    }
1873
1874    /// 簡易 BMP デコーダ。24ビットまたは32ビット無圧縮の BMP データを指定座標に、指定サイズへ Nearest Neighbor 拡大縮小して描画します。
1875    pub fn draw_bmp(
1876        &self,
1877        x: u32,
1878        y: u32,
1879        target_width: u32,
1880        target_height: u32,
1881        bmp_data: &[u8],
1882    ) -> Result<(), &'static str> {
1883        if bmp_data.len() < 54 {
1884            return Err("Data too short");
1885        }
1886        if &bmp_data[0..2] != b"BM" {
1887            return Err("Not a BMP file");
1888        }
1889
1890        let off_bits =
1891            u32::from_le_bytes(bmp_data[10..14].try_into().map_err(|_| "Invalid offset")?) as usize;
1892        let width = i32::from_le_bytes(bmp_data[18..22].try_into().map_err(|_| "Invalid width")?);
1893        let height = i32::from_le_bytes(bmp_data[22..26].try_into().map_err(|_| "Invalid height")?);
1894        let bit_count = u16::from_le_bytes(
1895            bmp_data[28..30]
1896                .try_into()
1897                .map_err(|_| "Invalid bitcount")?,
1898        );
1899        let compression = u32::from_le_bytes(
1900            bmp_data[30..34]
1901                .try_into()
1902                .map_err(|_| "Invalid compression")?,
1903        );
1904
1905        if compression != 0 {
1906            return Err("Only uncompressed BMP is supported");
1907        }
1908        if bit_count != 24 && bit_count != 32 {
1909            return Err("Only 24-bit or 32-bit BMP is supported");
1910        }
1911
1912        let abs_height = height.unsigned_abs();
1913        let is_bottom_up = height > 0;
1914        let bytes_per_pixel = (bit_count / 8) as usize;
1915        let row_stride = (width as usize * bytes_per_pixel).div_ceil(4) * 4;
1916
1917        if target_width == 0 || target_height == 0 {
1918            return Ok(());
1919        }
1920
1921        for row in 0..target_height {
1922            let src_row_idx = (row * abs_height) / target_height;
1923            let src_row = if is_bottom_up {
1924                abs_height - 1 - src_row_idx
1925            } else {
1926                src_row_idx
1927            };
1928            let row_offset = off_bits + (src_row as usize * row_stride);
1929            if row_offset + (width as usize * bytes_per_pixel) > bmp_data.len() {
1930                return Err("BMP pixel data out of bounds");
1931            }
1932            for col in 0..target_width {
1933                let src_col = (col * width as u32) / target_width;
1934                let pixel_offset = row_offset + (src_col as usize * bytes_per_pixel);
1935                let b = bmp_data[pixel_offset];
1936                let g = bmp_data[pixel_offset + 1];
1937                let r = bmp_data[pixel_offset + 2];
1938                let a = if bytes_per_pixel == 4 {
1939                    bmp_data[pixel_offset + 3]
1940                } else {
1941                    255
1942                };
1943
1944                let color = Color(0xFF000000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32));
1945                self.draw_pixel_alpha(x + col, y + row, color, a);
1946            }
1947        }
1948        Ok(())
1949    }
1950
1951    /// PNG, JPG, BMP をデコードし、指定サイズへ Nearest Neighbor 拡大縮小して描画します
1952    pub fn draw_image(
1953        &self,
1954        x: u32,
1955        y: u32,
1956        target_width: u32,
1957        target_height: u32,
1958        data: &[u8],
1959    ) -> Result<(), &'static str> {
1960        if target_width == 0 || target_height == 0 {
1961            return Ok(());
1962        }
1963
1964        if data.len() > 8 && &data[0..8] == b"\x89PNG\r\n\x1a\n" {
1965            let mut decoder = zune_png::PngDecoder::new(data);
1966            match decoder.decode_raw() {
1967                Ok(pixels) => {
1968                    let info = decoder.get_info().ok_or("Failed to get PNG info")?;
1969                    let width = info.width as u32;
1970                    let height = info.height as u32;
1971                    let bytes_per_pixel = pixels.len() / (width as usize * height as usize).max(1);
1972
1973                    for row in 0..target_height {
1974                        let src_row = (row * height) / target_height;
1975                        for col in 0..target_width {
1976                            let src_col = (col * width) / target_width;
1977                            let idx = ((src_row * width + src_col) as usize) * bytes_per_pixel;
1978                            if idx + bytes_per_pixel <= pixels.len() {
1979                                let (r, g, b, a) = match bytes_per_pixel {
1980                                    1 => (pixels[idx], pixels[idx], pixels[idx], 255),
1981                                    2 => (pixels[idx], pixels[idx], pixels[idx], pixels[idx + 1]),
1982                                    3 => (pixels[idx], pixels[idx + 1], pixels[idx + 2], 255),
1983                                    4 => (
1984                                        pixels[idx],
1985                                        pixels[idx + 1],
1986                                        pixels[idx + 2],
1987                                        pixels[idx + 3],
1988                                    ),
1989                                    _ => (0, 0, 0, 255),
1990                                };
1991                                let color = Color(
1992                                    0xFF000000
1993                                        | ((r as u32) << 16)
1994                                        | ((g as u32) << 8)
1995                                        | (b as u32),
1996                                );
1997                                self.draw_pixel_alpha(x + col, y + row, color, a);
1998                            }
1999                        }
2000                    }
2001                    Ok(())
2002                }
2003                Err(_) => Err("Failed to decode PNG"),
2004            }
2005        } else if data.len() > 2 && data[0] == 0xFF && data[1] == 0xD8 {
2006            let mut decoder = zune_jpeg::JpegDecoder::new(data);
2007            match decoder.decode() {
2008                Ok(pixels) => {
2009                    let info = decoder.info().ok_or("Failed to get JPEG info")?;
2010                    let width = info.width as u32;
2011                    let height = info.height as u32;
2012                    let bytes_per_pixel = pixels.len() / (width as usize * height as usize).max(1);
2013
2014                    for row in 0..target_height {
2015                        let src_row = (row * height) / target_height;
2016                        for col in 0..target_width {
2017                            let src_col = (col * width) / target_width;
2018                            let idx = ((src_row * width + src_col) as usize) * bytes_per_pixel;
2019                            if idx + bytes_per_pixel <= pixels.len() {
2020                                let (r, g, b) = match bytes_per_pixel {
2021                                    1 => (pixels[idx], pixels[idx], pixels[idx]),
2022                                    3 => (pixels[idx], pixels[idx + 1], pixels[idx + 2]),
2023                                    _ => (0, 0, 0),
2024                                };
2025                                let color = Color(
2026                                    0xFF000000
2027                                        | ((r as u32) << 16)
2028                                        | ((g as u32) << 8)
2029                                        | (b as u32),
2030                                );
2031                                self.draw_pixel_alpha(x + col, y + row, color, 255);
2032                            }
2033                        }
2034                    }
2035                    Ok(())
2036                }
2037                Err(_) => Err("Failed to decode JPEG"),
2038            }
2039        } else if data.len() > 2 && &data[0..2] == b"BM" {
2040            self.draw_bmp(x, y, target_width, target_height, data)
2041        } else {
2042            Err("Unsupported image format")
2043        }
2044    }
2045
2046    /// Draw a linear gradient rectangle. Interpolates between c1 and c2.
2047    /// If vertical=true, gradient goes top→bottom; otherwise left→right.
2048    pub fn draw_gradient_rect(
2049        &self,
2050        x0: u32,
2051        y0: u32,
2052        x1: u32,
2053        y1: u32,
2054        c1: Color,
2055        c2: Color,
2056        vertical: bool,
2057    ) {
2058        if x1 <= x0 || y1 <= y0 {
2059            return;
2060        }
2061        let r1 = ((c1.0 >> 16) & 0xFF) as f32;
2062        let g1 = ((c1.0 >> 8) & 0xFF) as f32;
2063        let b1 = (c1.0 & 0xFF) as f32;
2064        let r2 = ((c2.0 >> 16) & 0xFF) as f32;
2065        let g2 = ((c2.0 >> 8) & 0xFF) as f32;
2066        let b2 = (c2.0 & 0xFF) as f32;
2067        if vertical {
2068            let h = (y1 - y0) as f32;
2069            for dy in 0..(y1 - y0) {
2070                let t = dy as f32 / h.max(1.0);
2071                let r = ((1.0 - t) * r1 + t * r2) as u32;
2072                let g = ((1.0 - t) * g1 + t * g2) as u32;
2073                let b = ((1.0 - t) * b1 + t * b2) as u32;
2074                let c = 0xFF000000 | (r << 16) | (g << 8) | b;
2075                self.boxfill(x0, y0 + dy, x1, y0 + dy + 1, Color(c));
2076            }
2077        } else {
2078            let w = (x1 - x0) as f32;
2079            for dx in 0..(x1 - x0) {
2080                let t = dx as f32 / w.max(1.0);
2081                let r = ((1.0 - t) * r1 + t * r2) as u32;
2082                let g = ((1.0 - t) * g1 + t * g2) as u32;
2083                let b = ((1.0 - t) * b1 + t * b2) as u32;
2084                let c = 0xFF000000 | (r << 16) | (g << 8) | b;
2085                self.boxfill(x0 + dx, y0, x0 + dx + 1, y1, Color(c));
2086            }
2087        }
2088    }
2089
2090    /// `radial-gradient(circle, c1, c2)` — 矩形の中心を起点に、中心から最も遠い角までの
2091    /// 距離を半径として c1(中心)→c2(外周) へ線形補間する。楕円/位置指定などは非対応の簡略実装。
2092    pub fn draw_radial_gradient_rect(&self, x0: u32, y0: u32, x1: u32, y1: u32, c1: Color, c2: Color) {
2093        if x1 <= x0 || y1 <= y0 {
2094            return;
2095        }
2096        let r1 = ((c1.0 >> 16) & 0xFF) as f32;
2097        let g1 = ((c1.0 >> 8) & 0xFF) as f32;
2098        let b1 = (c1.0 & 0xFF) as f32;
2099        let r2 = ((c2.0 >> 16) & 0xFF) as f32;
2100        let g2 = ((c2.0 >> 8) & 0xFF) as f32;
2101        let b2 = (c2.0 & 0xFF) as f32;
2102        let w = (x1 - x0) as f32;
2103        let h = (y1 - y0) as f32;
2104        let cx = w / 2.0;
2105        let cy = h / 2.0;
2106        let max_dist = libm::sqrtf(cx * cx + cy * cy).max(1.0);
2107        for dy in 0..(y1 - y0) {
2108            for dx in 0..(x1 - x0) {
2109                let px = dx as f32 - cx;
2110                let py = dy as f32 - cy;
2111                let dist = libm::sqrtf(px * px + py * py);
2112                let t = (dist / max_dist).min(1.0);
2113                let r = ((1.0 - t) * r1 + t * r2) as u32;
2114                let g = ((1.0 - t) * g1 + t * g2) as u32;
2115                let b = ((1.0 - t) * b1 + t * b2) as u32;
2116                let c = 0xFF000000 | (r << 16) | (g << 8) | b;
2117                self.boxfill(x0 + dx, y0 + dy, x0 + dx + 1, y0 + dy + 1, Color(c));
2118            }
2119        }
2120    }
2121
2122    /// `conic-gradient(c1, c2)` — 矩形の中心を起点に、12時方向(真上)を角度0として
2123    /// 時計回りに掃引し c1(0°)→c2(360°直前)へ線形補間する(実ブラウザの2色既定と同様、
2124    /// 0°/360° の境界に色が不連続に戻る「継ぎ目」ができるのが正しい挙動)。
2125    /// `from`/位置指定・複数カラーストップは非対応の簡略実装。
2126    pub fn draw_conic_gradient_rect(&self, x0: u32, y0: u32, x1: u32, y1: u32, c1: Color, c2: Color) {
2127        if x1 <= x0 || y1 <= y0 {
2128            return;
2129        }
2130        let r1 = ((c1.0 >> 16) & 0xFF) as f32;
2131        let g1 = ((c1.0 >> 8) & 0xFF) as f32;
2132        let b1 = (c1.0 & 0xFF) as f32;
2133        let r2 = ((c2.0 >> 16) & 0xFF) as f32;
2134        let g2 = ((c2.0 >> 8) & 0xFF) as f32;
2135        let b2 = (c2.0 & 0xFF) as f32;
2136        let w = (x1 - x0) as f32;
2137        let h = (y1 - y0) as f32;
2138        let cx = w / 2.0;
2139        let cy = h / 2.0;
2140        const PI: f32 = core::f32::consts::PI;
2141        for dy in 0..(y1 - y0) {
2142            for dx in 0..(x1 - x0) {
2143                let px = dx as f32 - cx;
2144                let py = dy as f32 - cy;
2145                // atan2(px, -py): 真上(0,-1)方向を角度0とし、時計回りに増加させる。
2146                let mut angle = libm::atan2f(px, -py);
2147                if angle < 0.0 {
2148                    angle += 2.0 * PI;
2149                }
2150                let t = angle / (2.0 * PI); // 0.0-1.0 (0度→c1, 360度直前→c2)
2151                let r = ((1.0 - t) * r1 + t * r2) as u32;
2152                let g = ((1.0 - t) * g1 + t * g2) as u32;
2153                let b = ((1.0 - t) * b1 + t * b2) as u32;
2154                let c = 0xFF000000 | (r << 16) | (g << 8) | b;
2155                self.boxfill(x0 + dx, y0 + dy, x0 + dx + 1, y0 + dy + 1, Color(c));
2156            }
2157        }
2158    }
2159
2160    /// 周期内の位置 `t`(0.0-1.0)における `colors` のN色均等割り補間色を得る。
2161    /// `colors` が空/1色の場合は先頭色(無ければ黒)を返す。
2162    fn sample_repeating_colors(colors: &[Color], t: f32) -> u32 {
2163        let Some(&first) = colors.first() else {
2164            return 0xFF000000;
2165        };
2166        if colors.len() < 2 {
2167            return first.0;
2168        }
2169        let scaled = t * (colors.len() - 1) as f32;
2170        let seg = (scaled as usize).min(colors.len() - 2);
2171        let local_t = scaled - seg as f32;
2172        let c1 = colors[seg];
2173        let c2 = colors[seg + 1];
2174        let r1 = ((c1.0 >> 16) & 0xFF) as f32;
2175        let g1 = ((c1.0 >> 8) & 0xFF) as f32;
2176        let b1 = (c1.0 & 0xFF) as f32;
2177        let r2 = ((c2.0 >> 16) & 0xFF) as f32;
2178        let g2 = ((c2.0 >> 8) & 0xFF) as f32;
2179        let b2 = (c2.0 & 0xFF) as f32;
2180        let r = ((1.0 - local_t) * r1 + local_t * r2) as u32;
2181        let g = ((1.0 - local_t) * g1 + local_t * g2) as u32;
2182        let b = ((1.0 - local_t) * b1 + local_t * b2) as u32;
2183        0xFF000000 | (r << 16) | (g << 8) | b
2184    }
2185
2186    /// `repeating-linear-gradient(c1, c2, ..., cycle_px)` — `cycle_px` を周期として全色を
2187    /// 均等割りで補間しながら軸方向に繰り返す(`draw_gradient_rect` の周期版)。
2188    /// 以前は先頭2色 `c1`/`c2` 固定で3色目以降が失われていた。角度指定は引き続き非対応。
2189    pub fn draw_repeating_linear_gradient_rect(
2190        &self,
2191        x0: u32,
2192        y0: u32,
2193        x1: u32,
2194        y1: u32,
2195        colors: &[Color],
2196        cycle_px: i32,
2197        vertical: bool,
2198    ) {
2199        if x1 <= x0 || y1 <= y0 || colors.is_empty() {
2200            return;
2201        }
2202        let cycle = cycle_px.max(1) as f32;
2203        if vertical {
2204            for dy in 0..(y1 - y0) {
2205                let t = (dy as f32 % cycle) / cycle;
2206                let c = Self::sample_repeating_colors(colors, t);
2207                self.boxfill(x0, y0 + dy, x1, y0 + dy + 1, Color(c));
2208            }
2209        } else {
2210            for dx in 0..(x1 - x0) {
2211                let t = (dx as f32 % cycle) / cycle;
2212                let c = Self::sample_repeating_colors(colors, t);
2213                self.boxfill(x0 + dx, y0, x0 + dx + 1, y1, Color(c));
2214            }
2215        }
2216    }
2217
2218    /// `repeating-radial-gradient(c1, c2, ..., cycle_px)` — 矩形中心からの距離を `cycle_px`
2219    /// で割った余りを使い、同心円状に全色を均等割りで繰り返す。以前は先頭2色固定で
2220    /// 3色目以降が失われていた。形状/位置指定は引き続き非対応。
2221    pub fn draw_repeating_radial_gradient_rect(
2222        &self,
2223        x0: u32,
2224        y0: u32,
2225        x1: u32,
2226        y1: u32,
2227        colors: &[Color],
2228        cycle_px: i32,
2229    ) {
2230        if x1 <= x0 || y1 <= y0 || colors.is_empty() {
2231            return;
2232        }
2233        let cycle = cycle_px.max(1) as f32;
2234        let w = (x1 - x0) as f32;
2235        let h = (y1 - y0) as f32;
2236        let cx = w / 2.0;
2237        let cy = h / 2.0;
2238        for dy in 0..(y1 - y0) {
2239            for dx in 0..(x1 - x0) {
2240                let px = dx as f32 - cx;
2241                let py = dy as f32 - cy;
2242                let dist = libm::sqrtf(px * px + py * py);
2243                let t = (dist % cycle) / cycle;
2244                let c = Self::sample_repeating_colors(colors, t);
2245                self.boxfill(x0 + dx, y0 + dy, x0 + dx + 1, y0 + dy + 1, Color(c));
2246            }
2247        }
2248    }
2249
2250    /// Draw a semi-transparent shadow rectangle at offset (ox, oy).
2251    pub fn draw_shadow_rect(
2252        &self,
2253        x0: i32,
2254        y0: i32,
2255        x1: i32,
2256        y1: i32,
2257        ox: i32,
2258        oy: i32,
2259        color: Color,
2260    ) {
2261        if x1 <= x0 || y1 <= y0 {
2262            return;
2263        }
2264        let sx0 = (x0 + ox).max(0) as u32;
2265        let sy0 = (y0 + oy).max(0) as u32;
2266        let sx1 = (x1 + ox).max(0) as u32;
2267        let sy1 = (y1 + oy).max(0) as u32;
2268        let alpha = ((color.0 >> 24) & 0xFF) as u8;
2269        let alpha = if alpha == 0 { 80u8 } else { alpha }; // default semi-transparent
2270        if sx1 <= sx0 || sy1 <= sy0 {
2271            return;
2272        }
2273        for y in sy0..sy1 {
2274            for x in sx0..sx1 {
2275                self.draw_pixel_alpha(x, y, Color(color.0 | 0xFF000000), alpha);
2276            }
2277        }
2278    }
2279
2280    /// Draw a semi-transparent shadow rectangle with optional rounded corners.
2281    /// 計算量: **O(W×H)** — W,H は影の矩形サイズ。半透明合成のため
2282    /// 1 ピクセルずつ処理する(定数倍が重い)。
2283    pub fn draw_shadow_rect_rounded(
2284        &self,
2285        x0: i32,
2286        y0: i32,
2287        x1: i32,
2288        y1: i32,
2289        ox: i32,
2290        oy: i32,
2291        radius: (i32, i32, i32, i32),
2292        color: Color,
2293    ) {
2294        if x1 <= x0 || y1 <= y0 {
2295            return;
2296        }
2297        let sx0 = (x0 + ox).max(0) as u32;
2298        let sy0 = (y0 + oy).max(0) as u32;
2299        let sx1 = (x1 + ox).max(0) as u32;
2300        let sy1 = (y1 + oy).max(0) as u32;
2301        let alpha = ((color.0 >> 24) & 0xFF) as u8;
2302        let alpha = if alpha == 0 { 80u8 } else { alpha };
2303        if sx1 <= sx0 || sy1 <= sy0 {
2304            return;
2305        }
2306        let w = x1 - x0;
2307        let h = y1 - y0;
2308        let has_radius = radius != (0, 0, 0, 0);
2309
2310        for y in sy0..sy1 {
2311            let ry = (y as i32) - oy - y0;
2312            for x in sx0..sx1 {
2313                let rx = (x as i32) - ox - x0;
2314                if has_radius && !crate::os_lib::web_engine::draw_helpers::point_in_rounded_rect(rx, ry, w, h, radius) {
2315                    continue;
2316                }
2317                self.draw_pixel_alpha(x, y, Color(color.0 | 0xFF000000), alpha);
2318            }
2319        }
2320    }
2321
2322    /// Draw a semi-transparent inset shadow rectangle inside (x0, y0, x1, y1).
2323    pub fn draw_inset_shadow_rect(
2324        &self,
2325        x0: i32,
2326        y0: i32,
2327        x1: i32,
2328        y1: i32,
2329        ox: i32,
2330        oy: i32,
2331        blur: i32,
2332        radius: (i32, i32, i32, i32),
2333        color: Color,
2334    ) {
2335        if x1 <= x0 || y1 <= y0 {
2336            return;
2337        }
2338        let ix0 = x0.max(0) as u32;
2339        let iy0 = y0.max(0) as u32;
2340        let ix1 = x1.max(0) as u32;
2341        let iy1 = y1.max(0) as u32;
2342        let alpha = ((color.0 >> 24) & 0xFF) as u8;
2343        let alpha = if alpha == 0 { 90u8 } else { alpha };
2344        let shadow_color = color.0 | 0xFF000000;
2345        let w = x1 - x0;
2346        let h = y1 - y0;
2347        let has_radius = radius != (0, 0, 0, 0);
2348        let shadow_depth = blur.clamp(4, 30);
2349
2350        for y in iy0..iy1 {
2351            let ry = (y as i32) - y0;
2352            for x in ix0..ix1 {
2353                let rx = (x as i32) - x0;
2354                if has_radius && !crate::os_lib::web_engine::draw_helpers::point_in_rounded_rect(rx, ry, w, h, radius) {
2355                    continue;
2356                }
2357                let d_left = rx - ox;
2358                let d_top = ry - oy;
2359                let d_right = (w - rx) + ox;
2360                let d_bottom = (h - ry) + oy;
2361                let min_dist = d_left.min(d_top).min(d_right).min(d_bottom);
2362                if min_dist >= 0 && min_dist < shadow_depth {
2363                    let factor = 1.0 - (min_dist as f32 / shadow_depth as f32);
2364                    let pix_alpha = ((alpha as f32) * factor) as u8;
2365                    if pix_alpha > 0 {
2366                        self.draw_pixel_alpha(x, y, Color(shadow_color), pix_alpha);
2367                    }
2368                }
2369            }
2370        }
2371    }
2372
2373    pub fn draw_rounded_rect_fill(
2374        &self,
2375        x0: i32,
2376        y0: i32,
2377        x1: i32,
2378        y1: i32,
2379        radius: (i32, i32, i32, i32),
2380        color: Color,
2381    ) {
2382        let (tl, tr, br, bl) = radius;
2383        if tl <= 0 && tr <= 0 && br <= 0 && bl <= 0 {
2384            self.boxfill(x0 as u32, y0 as u32, x1 as u32, y1 as u32, color);
2385            return;
2386        }
2387        let w = x1 - x0;
2388        let h = y1 - y0;
2389        
2390        let tl = tl.min(w / 2).min(h / 2).max(0);
2391        let tr = tr.min(w / 2).min(h / 2).max(0);
2392        let br = br.min(w / 2).min(h / 2).max(0);
2393        let bl = bl.min(w / 2).min(h / 2).max(0);
2394        
2395        // 中央の十字領域と4つの角領域を分けて塗るなど最適化できるが、
2396        // ひとまずシンプルに point_in_rounded_rect 相当の判定でピクセル描画するか、
2397        // あるいは矩形領域分割で描画する。
2398        // ここは OS のカーネル描画プリミティブなので、最適化のため領域分割する。
2399        
2400        // 1. 中央水平帯 (全体幅、高さは 上端最大R から 下端最大R)
2401        let top_max_r = tl.max(tr);
2402        let bot_max_r = bl.max(br);
2403        if top_max_r + bot_max_r <= h {
2404            self.boxfill(x0 as u32, (y0 + top_max_r) as u32, x1 as u32, (y1 - bot_max_r) as u32, color);
2405        }
2406        
2407        // 2. 上部帯 (左右の角丸を除く)
2408        if top_max_r > 0 {
2409            self.boxfill((x0 + tl) as u32, y0 as u32, (x1 - tr) as u32, (y0 + top_max_r) as u32, color);
2410        }
2411        
2412        // 3. 下部帯 (左右の角丸を除く)
2413        if bot_max_r > 0 {
2414            self.boxfill((x0 + bl) as u32, (y1 - bot_max_r) as u32, (x1 - br) as u32, y1 as u32, color);
2415        }
2416        
2417        // 4. 左上・右上・左下・右下の余白領域で角丸内側のピクセルを描画
2418        // 左上
2419        if tl > 0 {
2420            let r2 = tl * tl;
2421            for dy in 0..tl {
2422                for dx in 0..tl {
2423                    if dx * dx + dy * dy <= r2 {
2424                        self.draw_pixel((x0 + tl - 1 - dx) as u32, (y0 + tl - 1 - dy) as u32, color);
2425                    }
2426                }
2427            }
2428            // 上端最大Rとの差分を埋める
2429            if top_max_r > tl {
2430                self.boxfill(x0 as u32, (y0 + tl) as u32, (x0 + tl) as u32, (y0 + top_max_r) as u32, color);
2431            }
2432        }
2433        // 右上
2434        if tr > 0 {
2435            let r2 = tr * tr;
2436            for dy in 0..tr {
2437                for dx in 0..tr {
2438                    if dx * dx + dy * dy <= r2 {
2439                        self.draw_pixel((x1 - tr + dx) as u32, (y0 + tr - 1 - dy) as u32, color);
2440                    }
2441                }
2442            }
2443            if top_max_r > tr {
2444                self.boxfill((x1 - tr) as u32, (y0 + tr) as u32, x1 as u32, (y0 + top_max_r) as u32, color);
2445            }
2446        }
2447        // 左下
2448        if bl > 0 {
2449            let r2 = bl * bl;
2450            for dy in 0..bl {
2451                for dx in 0..bl {
2452                    if dx * dx + dy * dy <= r2 {
2453                        self.draw_pixel((x0 + bl - 1 - dx) as u32, (y1 - bl + dy) as u32, color);
2454                    }
2455                }
2456            }
2457            if bot_max_r > bl {
2458                self.boxfill(x0 as u32, (y1 - bot_max_r) as u32, (x0 + bl) as u32, (y1 - bl) as u32, color);
2459            }
2460        }
2461        // 右下
2462        if br > 0 {
2463            let r2 = br * br;
2464            for dy in 0..br {
2465                for dx in 0..br {
2466                    if dx * dx + dy * dy <= r2 {
2467                        self.draw_pixel((x1 - br + dx) as u32, (y1 - br + dy) as u32, color);
2468                    }
2469                }
2470            }
2471            if bot_max_r > br {
2472                self.boxfill((x1 - br) as u32, (y1 - bot_max_r) as u32, x1 as u32, (y1 - br) as u32, color);
2473            }
2474        }
2475    }
2476
2477    pub fn draw_rounded_rect_border(
2478        &self,
2479        x0: i32,
2480        y0: i32,
2481        x1: i32,
2482        y1: i32,
2483        radius: (i32, i32, i32, i32),
2484        color: Color,
2485        _border_style: &str,
2486    ) {
2487        // 【2026-08-05 発見・修正】完全透明(アルファ 0)の枠線は描かない。
2488        //
2489        // `draw_pixel` は互換上アルファ 0 を「不透明」として扱うため
2490        // (下位のほぼ全ての呼び出しがアルファを設定せず 0 のまま渡すので、
2491        //  仕様どおり透明にすると画面が消える)、
2492        // `border-color: transparent` の角丸枠が**黒い輪郭**として描かれていた。
2493        //
2494        // 実測: セクション見出しの周りと `#apps` の一覧に、
2495        // 実サイトに無い黒枠(角丸の輪郭・☐)が出ていた。
2496        if (color.0 >> 24) & 0xFF == 0 {
2497            return;
2498        }
2499        let (tl, tr, br, bl) = radius;
2500        if tl <= 0 && tr <= 0 && br <= 0 && bl <= 0 {
2501            // 直線枠線
2502            self.boxfill(x0 as u32, y0 as u32, x1 as u32, (y0 + 1) as u32, color);
2503            self.boxfill(x0 as u32, (y1 - 1) as u32, x1 as u32, y1 as u32, color);
2504            self.boxfill(x0 as u32, y0 as u32, (x0 + 1) as u32, y1 as u32, color);
2505            self.boxfill((x1 - 1) as u32, y0 as u32, x1 as u32, y1 as u32, color);
2506            return;
2507        }
2508        let w = x1 - x0;
2509        let h = y1 - y0;
2510        let tl = tl.min(w / 2).min(h / 2).max(0);
2511        let tr = tr.min(w / 2).min(h / 2).max(0);
2512        let br = br.min(w / 2).min(h / 2).max(0);
2513        let bl = bl.min(w / 2).min(h / 2).max(0);
2514
2515        // 直線部分 (上下左右)
2516        self.boxfill((x0 + tl) as u32, y0 as u32, (x1 - tr) as u32, (y0 + 1) as u32, color);
2517        self.boxfill((x0 + bl) as u32, (y1 - 1) as u32, (x1 - br) as u32, y1 as u32, color);
2518        self.boxfill(x0 as u32, (y0 + tl) as u32, (x0 + 1) as u32, (y1 - bl) as u32, color);
2519        self.boxfill((x1 - 1) as u32, (y0 + tr) as u32, x1 as u32, (y1 - br) as u32, color);
2520
2521        // 曲線部分
2522        let draw_arc = |cx: i32, cy: i32, r: i32, quadrant: i32| {
2523            if r <= 0 { return; }
2524            let mut x = r;
2525            let mut y = 0;
2526            let mut err = 0;
2527            while x >= y {
2528                let px1 = cx + x - 1; let py1 = cy + y;
2529                let px2 = cx + y - 1; let py2 = cy + x - 1;
2530                let px3 = cx - x; let py3 = cy + y;
2531                let px4 = cx - y; let py4 = cy + x - 1;
2532                let px5 = cx - x; let py5 = cy - y - 1;
2533                let px6 = cx - y; let py6 = cy - x;
2534                let px7 = cx + x - 1; let py7 = cy - y - 1;
2535                let px8 = cx + y - 1; let py8 = cy - x;
2536
2537                match quadrant {
2538                    1 => { // 右下 (br)
2539                        self.draw_pixel(px1 as u32, py1 as u32, color);
2540                        self.draw_pixel(px2 as u32, py2 as u32, color);
2541                    }
2542                    2 => { // 左下 (bl)
2543                        self.draw_pixel(px3 as u32, py3 as u32, color);
2544                        self.draw_pixel(px4 as u32, py4 as u32, color);
2545                    }
2546                    3 => { // 左上 (tl)
2547                        self.draw_pixel(px5 as u32, py5 as u32, color);
2548                        self.draw_pixel(px6 as u32, py6 as u32, color);
2549                    }
2550                    4 => { // 右上 (tr)
2551                        self.draw_pixel(px7 as u32, py7 as u32, color);
2552                        self.draw_pixel(px8 as u32, py8 as u32, color);
2553                    }
2554                    _ => {}
2555                }
2556
2557                if err <= 0 {
2558                    y += 1;
2559                    err += 2 * y + 1;
2560                }
2561                if err > 0 {
2562                    x -= 1;
2563                    err -= 2 * x + 1;
2564                }
2565            }
2566        };
2567
2568        draw_arc(x0 + tl, y0 + tl, tl, 3);
2569        draw_arc(x1 - tr, y0 + tr, tr, 4);
2570        draw_arc(x1 - br, y1 - br, br, 1);
2571        draw_arc(x0 + bl, y1 - bl, bl, 2);
2572    }
2573
2574
2575}
2576
2577/// 背景退避・描き戻し方式による動的マウスカーソル制御構造体
2578pub struct Mouse {
2579    pub x: u32,
2580    pub y: u32,
2581    pub is_clicked: bool,
2582    pub click_frame: u8,
2583    /// CSS `cursor: text` 用。矢印の代わりに I-beam(縦棒)を手続き的に描く。
2584    /// それ以外の非対応 `cursor` キーワード(pointer/wait/help 等)は既定の矢印にフォールバックする。
2585    pub is_text_cursor: bool,
2586}
2587
2588impl Mouse {
2589    pub const SIZE: u32 = 32;
2590
2591    pub const fn new(x: u32, y: u32) -> Self {
2592        Self {
2593            x,
2594            y,
2595            is_clicked: false,
2596            click_frame: 0,
2597            is_text_cursor: false,
2598        }
2599    }
2600
2601    pub fn draw(&self, screen: &Screen) {
2602        let (target_ptr, w, h) = screen.resolve_target();
2603        let stride = screen.resolve_stride();
2604        let cursor_size = Self::SIZE;
2605
2606        // クリック状態によるシフト(押し込み感)
2607        let (draw_x, draw_y) = if self.is_clicked {
2608            (self.x + 1, self.y + 1)
2609        } else {
2610            (self.x, self.y)
2611        };
2612
2613        if self.is_text_cursor {
2614            self.draw_text_cursor(screen, draw_x, draw_y);
2615            return;
2616        }
2617
2618        if let Some(alpha_mask) = crate::os_lib::svg::get_or_rasterize(ICON_CURSOR_64, cursor_size)
2619        {
2620            let theme = crate::kernel::config::get_config().theme;
2621            let mut c_fill = theme.mouse_cursor;
2622            let c_stroke = theme.mouse_stroke;
2623
2624            // クリック時は Fill 色を少し暗くする(明度を落とす)
2625            if self.is_clicked {
2626                let r = ((c_fill >> 16) & 0xFF) * 8 / 10;
2627                let g = ((c_fill >> 8) & 0xFF) * 8 / 10;
2628                let b = (c_fill & 0xFF) * 8 / 10;
2629                c_fill = 0xFF000000 | (r << 16) | (g << 8) | b;
2630            }
2631
2632            for row in 0..cursor_size {
2633                let py = draw_y + row;
2634                if py >= h {
2635                    continue;
2636                }
2637
2638                let max_col = (w.saturating_sub(draw_x)).min(cursor_size) as usize;
2639                if max_col == 0 {
2640                    continue;
2641                }
2642
2643                let offset = (py * stride + draw_x) as usize;
2644                unsafe {
2645                    let mut row_buf = [0u32; 32];
2646                    let dst = target_ptr.add(offset);
2647
2648                    // 裏画面から背景を取得
2649                    let src = if !screen.back_buffer.is_null() {
2650                        let src_offset = (py * w + draw_x) as usize;
2651                        screen.back_buffer.add(src_offset)
2652                    } else {
2653                        dst
2654                    };
2655
2656                    core::ptr::copy_nonoverlapping(src, row_buf.as_mut_ptr(), max_col);
2657
2658                    // マウスカーソルを重ね合わせ (アルファブレンド)
2659                    for col in 0..max_col {
2660                        let mask_idx = (row * cursor_size + col as u32) as usize;
2661                        let alpha = alpha_mask[mask_idx];
2662                        if alpha > 0 {
2663                            // 近傍をチェックしてエッジ(輪郭線)判定を行う
2664                            let mut is_edge = false;
2665                            if row == 0
2666                                || row == cursor_size - 1
2667                                || col == 0
2668                                || col as u32 == cursor_size - 1
2669                            {
2670                                is_edge = true;
2671                            } else {
2672                                // 上下左右斜め8マスのアルファ値をチェック
2673                                'outer: for dy in -1..=1 {
2674                                    for dx in -1..=1 {
2675                                        let ny = row as i32 + dy;
2676                                        let nx = col as i32 + dx;
2677                                        let n_idx = (ny * cursor_size as i32 + nx) as usize;
2678                                        if alpha_mask[n_idx] < 80 {
2679                                            is_edge = true;
2680                                            break 'outer;
2681                                        }
2682                                    }
2683                                }
2684                            }
2685
2686                            let bg = row_buf[col];
2687                            let r_bg = (bg >> 16) & 0xFF;
2688                            let g_bg = (bg >> 8) & 0xFF;
2689                            let b_bg = bg & 0xFF;
2690
2691                            // エッジなら縁の色 (c_stroke)、内側なら本体の色 (c_fill) を適用
2692                            let fg_color = if is_edge { c_stroke } else { c_fill };
2693                            let r_fg = (fg_color >> 16) & 0xFF;
2694                            let g_fg = (fg_color >> 8) & 0xFF;
2695                            let b_fg = fg_color & 0xFF;
2696
2697                            let a = alpha as u32;
2698                            let inv_a = 255 - a;
2699
2700                            let r = (r_fg * a + r_bg * inv_a) / 255;
2701                            let g = (g_fg * a + g_bg * inv_a) / 255;
2702                            let b = (b_fg * a + b_bg * inv_a) / 255;
2703
2704                            row_buf[col] = (r << 16) | (g << 8) | b;
2705                        }
2706                    }
2707
2708                    // VRAMへ一気に書き戻し
2709                    core::ptr::copy_nonoverlapping(row_buf.as_ptr(), dst, max_col);
2710                }
2711            }
2712
2713            // 集中線(クリック火花)エフェクト
2714            if self.is_clicked && self.click_frame > 0 && self.click_frame < 8 {
2715                let spark_color = theme.mouse_cursor; // カーソル本体と同色の火花
2716                let center_x = draw_x as i32;
2717                let center_y = draw_y as i32;
2718                let r1 = 4 + (self.click_frame as i32) * 2; // フレームと共に外へ広がる
2719                let r2 = r1 + 4; // 線の長さ
2720
2721                // 8方向の簡易ベクトル (dx, dy)
2722                let dirs = [
2723                    (0, -1), (1, -1), (1, 0), (1, 1),
2724                    (0, 1), (-1, 1), (-1, 0), (-1, -1)
2725                ];
2726
2727                for &(dx, dy) in dirs.iter() {
2728                    for r in r1..=r2 {
2729                        let px = center_x + dx * r;
2730                        let py = center_y + dy * r;
2731                        if px >= 0 && px < w as i32 && py >= 0 && py < h as i32 {
2732                            let offset = (py as u32 * stride + px as u32) as usize;
2733                            unsafe {
2734                                let dst = target_ptr.add(offset);
2735                                *dst = spark_color;
2736                            }
2737                        }
2738                    }
2739                }
2740            }
2741        }
2742    }
2743
2744    /// CSS `cursor: text` 用の I-beam カーソルを手続き的に描画する。
2745    /// 新規 SVG アイコン資産を追加せず、`draw_pixel` の直描きだけで完結させる
2746    /// (矢印(SVGラスタライズ)と異なり、コード自体の正しさだけで見た目を保証できる形状)。
2747    /// `Self::SIZE` 四方のバウンディングボックス内に収め、既存のダーティ領域管理
2748    /// (`flush_with_cursor`/`flush_dirty_pageflip` が `Self::SIZE` 四方を前提に
2749    /// 保存/復元・ダーティ判定している)と矛盾しないようにする。
2750    fn draw_text_cursor(&self, screen: &Screen, draw_x: u32, draw_y: u32) {
2751        let theme = crate::kernel::config::get_config().theme;
2752        let color = Color(theme.mouse_cursor);
2753        let size = Self::SIZE;
2754        let bar_h = size.saturating_sub(4);
2755        let cx = draw_x + size / 2;
2756        let top_y = draw_y + 2;
2757        let bottom_y = top_y + bar_h;
2758        // 上下の「serif」(横棒)
2759        for dx in 0..=(size / 4) {
2760            screen.draw_pixel(cx.saturating_sub(size / 8) + dx, top_y, color);
2761            screen.draw_pixel(cx.saturating_sub(size / 8) + dx, bottom_y, color);
2762        }
2763        // 縦棒
2764        for y in top_y..=bottom_y {
2765            screen.draw_pixel(cx, y, color);
2766        }
2767    }
2768
2769    /// マウスカーソルを新しい位置に移動します
2770    pub fn move_to(&mut self, screen: &Screen, new_x: u32, new_y: u32) {
2771        self.x = new_x.clamp(0, screen.width - 1);
2772        self.y = new_y.clamp(0, screen.height - 1);
2773    }
2774}
2775
2776// --------------------------------------------------------------------
2777// 起動時スプラッシュ画面表示 (Logo & Log Scroll)
2778// --------------------------------------------------------------------
2779pub static mut SPLASH_ACTIVE: bool = false;
2780pub static LOGO_PNG: &[u8] = include_bytes!("../../images/os_logo.png");
2781
2782static mut BOOT_LOG_COUNT: usize = 0;
2783static mut BOOT_LOG_BUFFER: [[u8; 128]; 16] = [[0; 128]; 16];
2784static mut BOOT_LOG_LENS: [usize; 16] = [0; 16];
2785
2786static mut CURRENT_LINE_BUFFER: [u8; 256] = [0; 256];
2787static mut CURRENT_LINE_LEN: usize = 0;
2788
2789/// # Safety
2790/// ブート時スプラッシュ専用。可変 static (`CURRENT_LINE_*`) にアクセスするため、
2791/// 単一スレッドのブートコンテキストからのみ呼び出すこと。
2792pub unsafe fn write_splash_char(c: u8) {
2793    if !SPLASH_ACTIVE {
2794        return;
2795    }
2796    if c == b'\n' {
2797        if CURRENT_LINE_LEN > 0 {
2798            if let Ok(s) = core::str::from_utf8(&CURRENT_LINE_BUFFER[..CURRENT_LINE_LEN]) {
2799                add_boot_log(s);
2800            }
2801            CURRENT_LINE_LEN = 0;
2802        }
2803    } else {
2804        if CURRENT_LINE_LEN < 255 {
2805            CURRENT_LINE_BUFFER[CURRENT_LINE_LEN] = c;
2806            CURRENT_LINE_LEN += 1;
2807        }
2808    }
2809}
2810
2811/// # Safety
2812/// ブート時スプラッシュ専用。可変 static にアクセスするため、
2813/// 単一スレッドのブートコンテキストからのみ呼び出すこと。
2814pub unsafe fn write_splash_str(s: &str) {
2815    if !SPLASH_ACTIVE {
2816        return;
2817    }
2818    for &b in s.as_bytes() {
2819        write_splash_char(b);
2820    }
2821}
2822
2823/// # Safety
2824/// ブート時スプラッシュ専用。可変 static (`BOOT_LOG*`) にアクセスするため、
2825/// 単一スレッドのブートコンテキストからのみ呼び出すこと。
2826pub unsafe fn add_boot_log(s: &str) {
2827    if !SPLASH_ACTIVE {
2828        return;
2829    }
2830    const MAX_LINE_CHARS: usize = 55;
2831
2832    for line in s.lines() {
2833        let line = line.trim_end();
2834        if line.is_empty() {
2835            continue;
2836        }
2837
2838        let mut chars = line.as_bytes();
2839        while !chars.is_empty() {
2840            let chunk_len = chars.len().min(MAX_LINE_CHARS);
2841            let chunk = &chars[..chunk_len];
2842            chars = &chars[chunk_len..];
2843
2844            if BOOT_LOG_COUNT < 16 {
2845                let idx = BOOT_LOG_COUNT;
2846                BOOT_LOG_BUFFER[idx][..chunk_len].copy_from_slice(chunk);
2847                BOOT_LOG_LENS[idx] = chunk_len;
2848                BOOT_LOG_COUNT += 1;
2849            } else {
2850                for i in 0..15 {
2851                    BOOT_LOG_BUFFER[i] = BOOT_LOG_BUFFER[i + 1];
2852                    BOOT_LOG_LENS[i] = BOOT_LOG_LENS[i + 1];
2853                }
2854                BOOT_LOG_BUFFER[15][..chunk_len].copy_from_slice(chunk);
2855                BOOT_LOG_LENS[15] = chunk_len;
2856            }
2857        }
2858    }
2859    redraw_splash();
2860}
2861
2862/// # Safety
2863/// ブート時スプラッシュ専用。可変 static (`SPLASH_ACTIVE` 等) と画面バッファに
2864/// アクセスするため、単一スレッドのブートコンテキストからのみ呼び出すこと。
2865pub unsafe fn draw_splash_logo() {
2866    if !SPLASH_ACTIVE {
2867        return;
2868    }
2869    if let Some(ref screen) = crate::CURRENT_SCREEN {
2870        screen.clear(Color(0xFF000000));
2871        let mut decoder = zune_png::PngDecoder::new(LOGO_PNG);
2872        if decoder.decode_raw().is_ok() {
2873            if let Some(info) = decoder.get_info() {
2874                let img_w = info.width as u32;
2875                let img_h = info.height as u32;
2876                let area_w = 480u32;
2877                let area_h = screen.height;
2878                let target_w = area_w.min(img_w);
2879                let target_h = (target_w * img_h) / img_w;
2880                let target_h = target_h.min(area_h);
2881                let target_w = (target_h * img_w) / img_h;
2882
2883                let x = (area_w - target_w) / 2 + 20;
2884                let y = (screen.height - target_h) / 2;
2885                let _ = screen.draw_image(x, y, target_w, target_h, LOGO_PNG);
2886            }
2887        }
2888        redraw_splash();
2889    }
2890}
2891
2892/// # Safety
2893/// ブート時スプラッシュ専用。可変 static と画面バッファにアクセスするため、
2894/// 単一スレッドのブートコンテキストからのみ呼び出すこと。
2895pub unsafe fn redraw_splash() {
2896    if !SPLASH_ACTIVE {
2897        return;
2898    }
2899    if let Some(ref screen) = crate::CURRENT_SCREEN {
2900        let area_x = 520u32;
2901        let area_y = 20u32;
2902        let area_w = screen.width.saturating_sub(area_x).saturating_sub(20);
2903        let area_h = screen.height.saturating_sub(area_y).saturating_sub(20);
2904        screen.boxfill(
2905            area_x,
2906            area_y,
2907            area_x + area_w,
2908            area_y + area_h,
2909            Color(0xFF000000),
2910        );
2911
2912        let start_y = (screen.height.saturating_sub(40)) as i32;
2913        let line_height = 28i32;
2914
2915        for i in 0..BOOT_LOG_COUNT {
2916            let y = start_y - (BOOT_LOG_COUNT - 1 - i) as i32 * line_height;
2917            if y >= area_y as i32 {
2918                if let Ok(s) = core::str::from_utf8(&BOOT_LOG_BUFFER[i][..BOOT_LOG_LENS[i]]) {
2919                    screen.draw_string_monospace(area_x + 10, y as u32, s, Color(0xFFCCCCCC));
2920                }
2921            }
2922        }
2923    }
2924}
2925
2926pub struct SplashWriter;
2927
2928impl core::fmt::Write for SplashWriter {
2929    fn write_str(&mut self, s: &str) -> core::fmt::Result {
2930        unsafe {
2931            write_splash_str(s);
2932        }
2933        Ok(())
2934    }
2935}