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