Skip to main content

atmos/os_lib/web_engine/
image.rs

1// web_engine/image.rs - Image decode & async loader thread
2
3use alloc::string::String;
4use alloc::vec::Vec;
5use spin::Mutex;
6
7use super::cache::{read_cache, write_cache};
8
9// ---------------------------------------------------------------------------
10// 画像非同期ロードキュー
11// ---------------------------------------------------------------------------
12
13pub(super) struct ImageLoadQueue {
14    /// フェッチ待ちの画像 src キー(URL またはパス)
15    pub pending: Vec<ImageFetchReq>,
16    /// デコード完了した (src_key, decoded_image)
17    pub results: Vec<(String, super::DecodedImage)>,
18    /// 【2026-08-04】稼働中のワーカースレッド数。
19    ///
20    /// 以前は `thread_active: bool` で**ワーカーが 1 本だけ**だった。
21    /// 画像は通信が重く、1 枚取っている間は同じキューの後続が
22    /// 一切進まない(実測: ギャラリー画像 3 枚が alt テキストのまま)。
23    /// 通信待ちの間はスケジューラへ譲るので、複数本走らせれば
24    /// 重い 1 枚が他を止めなくなる。
25    pub active_workers: usize,
26}
27
28#[derive(Clone)]
29pub(super) struct ImageFetchReq {
30    pub src_key: String, // image_cache のキーとして使う文字列
31    pub is_https: bool,
32    pub host: String,
33    pub path: String,
34    pub is_local: bool, // SylFS からの読み込みの場合 true
35    pub owner: u64,     // 発行元エンジン(タブ)の ID。ページ遷移時に保留分を破棄するために使う
36}
37
38pub(super) static IMAGE_QUEUE: Mutex<ImageLoadQueue> = Mutex::new(ImageLoadQueue {
39    pending: Vec::new(),
40    results: Vec::new(),
41    active_workers: 0,
42});
43
44/// 同時に走らせる画像取得ワーカーの上限。
45///
46/// TCP ソケット表(32 件)を圧迫しない範囲で、
47/// 重い 1 枚が他を止めないだけの本数を確保する。
48const MAX_IMAGE_WORKERS: usize = 4;
49
50/// SVG をラスタライズする目標解像度(自然サイズ基準・上限512・最低1)を決める
51fn svg_target_size(data: &[u8]) -> (u32, u32) {
52    let (w, h) = crate::os_lib::svg::natural_size(data);
53    let cap = 512.0f32;
54    let scale = (cap / w.max(1.0)).min(cap / h.max(1.0)).clamp(0.01, 1.0);
55    // 自然サイズが小さい場合は等倍、大きい場合のみ縮小
56    let tw = (w * if w > cap || h > cap { scale } else { 1.0 }).max(1.0) as u32;
57    let th = (h * if w > cap || h > cap { scale } else { 1.0 }).max(1.0) as u32;
58    (tw.max(1), th.max(1))
59}
60
61pub(super) fn decode_image(data: &[u8]) -> Option<super::DecodedImage> {
62    crate::debug!("[GPU] TRACE: decode_image start. len={}", data.len());
63    // SVG(テキスト): マジックバイトが無いので先頭を覗いて判定
64    {
65        let head_len = data.len().min(512);
66        if let Ok(head) = core::str::from_utf8(&data[..head_len]) {
67            let ht = head.trim_start();
68            if ht.starts_with("<svg")
69                || ht.starts_with("<?xml")
70                || ht.starts_with("<!--") && head.contains("<svg")
71            {
72                crate::debug!("[GPU] TRACE: decode_image SVG target size parsing...");
73                let (tw, th) = svg_target_size(data);
74                crate::debug!("[GPU] TRACE: decode_image SVG decoding...");
75                if let Some((w, h, rgba)) = crate::os_lib::svg::decode_svg_image(data, tw, th) {
76                    crate::debug!("[GPU] TRACE: decode_image SVG success");
77                    return Some(super::DecodedImage {
78                        width: w,
79                        height: h,
80                        rgba,
81                    });
82                }
83                crate::debug!("[GPU] TRACE: decode_image SVG failed");
84                return None;
85            }
86        }
87    }
88    if data.len() > 8 && &data[0..8] == b"\x89PNG\r\n\x1a\n" {
89        crate::debug!("[GPU] TRACE: decode_image trying PNG...");
90        let mut decoder = zune_png::PngDecoder::new(data);
91        if let Ok(pixels) = decoder.decode_raw() {
92            if let Some(info) = decoder.get_info() {
93                let width = info.width as u32;
94                let height = info.height as u32;
95                let bpp = pixels.len() / (width as usize * height as usize).max(1);
96                const MAX_DIM: u32 = 1024;
97                let (out_w, out_h) = if width > MAX_DIM || height > MAX_DIM {
98                    if width * MAX_DIM > height * MAX_DIM {
99                        (MAX_DIM, (height * MAX_DIM / width).max(1))
100                    } else {
101                        ((width * MAX_DIM / height).max(1), MAX_DIM)
102                    }
103                } else {
104                    (width, height)
105                };
106                let mut rgba = alloc::vec::Vec::with_capacity((out_w * out_h * 4) as usize);
107                for row in 0..out_h {
108                    let src_row = (row * height) / out_h;
109                    for col in 0..out_w {
110                        let src_col = (col * width) / out_w;
111                        let i = (src_row * width + src_col) as usize;
112                        let idx = i * bpp;
113                        if idx + bpp <= pixels.len() {
114                            let (r, g, b, a) = match bpp {
115                                1 => (pixels[idx], pixels[idx], pixels[idx], 255),
116                                2 => (pixels[idx], pixels[idx], pixels[idx], pixels[idx + 1]),
117                                3 => (pixels[idx], pixels[idx + 1], pixels[idx + 2], 255),
118                                4 => (
119                                    pixels[idx],
120                                    pixels[idx + 1],
121                                    pixels[idx + 2],
122                                    pixels[idx + 3],
123                                ),
124                                _ => (0, 0, 0, 255),
125                            };
126                            rgba.push(r);
127                            rgba.push(g);
128                            rgba.push(b);
129                            rgba.push(a);
130                        }
131                    }
132                }
133                crate::debug!(
134                    "[GPU] TRACE: decode_image PNG success {}x{} -> {}x{}",
135                    width,
136                    height,
137                    out_w,
138                    out_h
139                );
140                return Some(super::DecodedImage {
141                    width: out_w,
142                    height: out_h,
143                    rgba,
144                });
145            }
146        }
147        crate::debug!("[GPU] TRACE: decode_image PNG failed");
148    } else if data.len() > 2 && data[0] == 0xFF && data[1] == 0xD8 {
149        crate::debug!("[GPU] TRACE: decode_image trying JPEG...");
150        // 全体をデコードする前に SOF マーカから寸法だけ読む。
151        //
152        // 【2026-08-05 訂正】ここには「協調マルチタスク(プリエンプション無し)なので
153        // UI が固まる」と書いてあったが、**プリエンプションは実装済みで配線もされている**
154        // (`interrupt.rs:104`)。UI が止まっていた真因はヒープの spin ロックが
155        // プリエンプション可能だったことで、`allocator.rs` 側で修正済み
156        // (42 秒 → 10 秒、`spec/resource_loading.md`)。
157        //
158        // この寸法チェックは、桁違いの画像でメモリを食い潰さないための上限として残す。
159        const MAX_JPEG_DIM: u32 = 8192;
160        {
161            let mut i = 2usize;
162            let mut skip = false;
163            while i + 3 < data.len() {
164                if data[i] != 0xFF {
165                    break;
166                }
167                let marker = data[i + 1];
168                let is_sof =
169                    matches!(marker, 0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF);
170                if is_sof && i + 8 < data.len() {
171                    let h = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
172                    let w = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
173                    if w > MAX_JPEG_DIM || h > MAX_JPEG_DIM {
174                        crate::debug!(
175                            "[GPU] TRACE: decode_image JPEG too large {}x{}, skipping (max {})",
176                            w,
177                            h,
178                            MAX_JPEG_DIM
179                        );
180                        skip = true;
181                    }
182                    break;
183                }
184                if i + 3 >= data.len() {
185                    break;
186                }
187                let seg_len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
188                if seg_len < 2 {
189                    break;
190                }
191                i += 2 + seg_len;
192            }
193            if skip {
194                return None;
195            }
196        }
197        let mut decoder = zune_jpeg::JpegDecoder::new(data);
198        // 【2026-07-30診断】協調マルチタスクでは CPU 律速のデコードが
199        // 別スレッドでも全体を止める。所要時間を実測する。
200        let t0 = crate::kernel::timer::get_ticks();
201        let decoded = decoder.decode();
202        crate::warn!(
203            "[IMGDIAG] jpeg decode ok={} bytes={} elapsed_ticks={}",
204            decoded.is_ok(),
205            data.len(),
206            crate::kernel::timer::get_ticks().wrapping_sub(t0)
207        );
208        if let Ok(pixels) = decoded {
209            if let Some(info) = decoder.info() {
210                let width = info.width as u32;
211                let height = info.height as u32;
212                let bpp = pixels.len() / (width as usize * height as usize).max(1);
213                const MAX_DIM: u32 = 1024;
214                let (out_w, out_h) = if width > MAX_DIM || height > MAX_DIM {
215                    if width * MAX_DIM > height * MAX_DIM {
216                        (MAX_DIM, (height * MAX_DIM / width).max(1))
217                    } else {
218                        ((width * MAX_DIM / height).max(1), MAX_DIM)
219                    }
220                } else {
221                    (width, height)
222                };
223                let mut rgba = alloc::vec::Vec::with_capacity((out_w * out_h * 4) as usize);
224                for row in 0..out_h {
225                    let src_row = (row * height) / out_h;
226                    for col in 0..out_w {
227                        let src_col = (col * width) / out_w;
228                        let i = (src_row * width + src_col) as usize;
229                        let idx = i * bpp;
230                        if idx + bpp <= pixels.len() {
231                            let (r, g, b) = match bpp {
232                                1 => (pixels[idx], pixels[idx], pixels[idx]),
233                                3 => (pixels[idx], pixels[idx + 1], pixels[idx + 2]),
234                                _ => (0, 0, 0),
235                            };
236                            rgba.push(r);
237                            rgba.push(g);
238                            rgba.push(b);
239                            rgba.push(255);
240                        }
241                    }
242                }
243                crate::debug!(
244                    "[GPU] TRACE: decode_image JPEG success {}x{} -> {}x{}",
245                    width,
246                    height,
247                    out_w,
248                    out_h
249                );
250                return Some(super::DecodedImage {
251                    width: out_w,
252                    height: out_h,
253                    rgba,
254                });
255            }
256        }
257        crate::debug!("[GPU] TRACE: decode_image JPEG failed");
258    } else if data.len() > 6 && &data[0..3] == b"GIF" {
259        crate::debug!("[GPU] TRACE: decode_image trying GIF...");
260        if let Some(img) = decode_gif(data) {
261            crate::debug!("[GPU] TRACE: decode_image GIF success");
262            return Some(img);
263        }
264        crate::debug!("[GPU] TRACE: decode_image GIF failed");
265    } else if data.len() > 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
266        crate::debug!("[GPU] TRACE: decode_image trying WEBP...");
267        if let Some((w, h, rgba)) = crate::os_lib::webp::decode_webp(data) {
268            crate::debug!("[GPU] TRACE: decode_image WEBP success");
269            return Some(super::DecodedImage {
270                width: w,
271                height: h,
272                rgba,
273            });
274        }
275        crate::debug!("[GPU] TRACE: decode_image WEBP failed");
276    } else if data.len() >= 54 && &data[0..2] == b"BM" {
277        crate::debug!("[GPU] TRACE: decode_image trying BMP...");
278        let off_bits = u32::from_le_bytes(data[10..14].try_into().unwrap_or([0; 4])) as usize;
279        let width = i32::from_le_bytes(data[18..22].try_into().unwrap_or([0; 4]));
280        let height = i32::from_le_bytes(data[22..26].try_into().unwrap_or([0; 4]));
281        let bit_count = u16::from_le_bytes(data[28..30].try_into().unwrap_or([0; 2]));
282        let compression = u32::from_le_bytes(data[30..34].try_into().unwrap_or([0; 4]));
283        if compression == 0 && (bit_count == 24 || bit_count == 32) {
284            let abs_h = height.unsigned_abs();
285            let bpp = (bit_count / 8) as usize;
286            let row_stride = (width.unsigned_abs() as usize * bpp).div_ceil(4) * 4;
287            let mut rgba =
288                alloc::vec::Vec::with_capacity((width.unsigned_abs() * abs_h * 4) as usize);
289            for row in 0..abs_h {
290                let src_row = if height > 0 { abs_h - 1 - row } else { row };
291                let row_off = off_bits + (src_row as usize * row_stride);
292                for col in 0..width.unsigned_abs() {
293                    let pix_off = row_off + (col as usize * bpp);
294                    if pix_off + bpp <= data.len() {
295                        let b = data[pix_off];
296                        let g = data[pix_off + 1];
297                        let r = data[pix_off + 2];
298                        let a = if bpp == 4 { data[pix_off + 3] } else { 255 };
299                        rgba.push(r);
300                        rgba.push(g);
301                        rgba.push(b);
302                        rgba.push(a);
303                    } else {
304                        rgba.push(0);
305                        rgba.push(0);
306                        rgba.push(0);
307                        rgba.push(255);
308                    }
309                }
310            }
311            crate::debug!("[GPU] TRACE: decode_image BMP success");
312            return Some(super::DecodedImage {
313                width: width.unsigned_abs(),
314                height: abs_h,
315                rgba,
316            });
317        }
318        crate::debug!("[GPU] TRACE: decode_image BMP failed");
319    }
320    crate::debug!("[GPU] TRACE: decode_image unsupported/unknown");
321    None
322}
323
324/// GIF (87a/89a) デコーダ。先頭イメージフレームを RGBA で返す。
325/// グローバル/ローカルパレット・LZW・透過(Graphic Control Extension)・インターレース対応。
326/// アニメーションは先頭フレームのみ描画(静止画扱い)。
327fn decode_gif(data: &[u8]) -> Option<super::DecodedImage> {
328    let mut p = 6usize; // "GIF87a"/"GIF89a" をスキップ
329    if data.len() < 13 {
330        return None;
331    }
332
333    let screen_w = u16::from_le_bytes([data[p], data[p + 1]]) as u32;
334    let screen_h = u16::from_le_bytes([data[p + 2], data[p + 3]]) as u32;
335    let packed = data[6 + 4];
336    p = 13;
337
338    // グローバルカラーテーブル
339    let gct_flag = (packed & 0x80) != 0;
340    let gct_size = 2usize << (packed & 0x07); // 2^(n+1)
341    let mut global_palette: Vec<[u8; 3]> = Vec::new();
342    if gct_flag {
343        for _ in 0..gct_size {
344            if p + 3 > data.len() {
345                return None;
346            }
347            global_palette.push([data[p], data[p + 1], data[p + 2]]);
348            p += 3;
349        }
350    }
351
352    let mut transparent_idx: Option<u8> = None;
353
354    // ブロックを走査して先頭イメージを見つける
355    while p < data.len() {
356        match data[p] {
357            0x21 => {
358                // 拡張ブロック
359                p += 1;
360                if p >= data.len() {
361                    return None;
362                }
363                let label = data[p];
364                p += 1;
365                if label == 0xF9 {
366                    // Graphic Control Extension: 透過色インデックスを取得
367                    if p < data.len() {
368                        let blk_size = data[p] as usize; // 通常 4
369                        if p + 1 + blk_size <= data.len() && blk_size >= 4 {
370                            let gce_packed = data[p + 1];
371                            if gce_packed & 0x01 != 0 {
372                                transparent_idx = Some(data[p + 4]);
373                            }
374                        }
375                    }
376                }
377                // サブブロックをスキップ
378                p = skip_gif_subblocks(data, p)?;
379            }
380            0x2C => {
381                // イメージ記述子
382                if p + 10 > data.len() {
383                    return None;
384                }
385                let img_w = u16::from_le_bytes([data[p + 5], data[p + 6]]) as u32;
386                let img_h = u16::from_le_bytes([data[p + 7], data[p + 8]]) as u32;
387                let ipacked = data[p + 9];
388                let lct_flag = (ipacked & 0x80) != 0;
389                let interlace = (ipacked & 0x40) != 0;
390                let lct_size = 2usize << (ipacked & 0x07);
391                p += 10;
392
393                // ローカルカラーテーブル
394                let palette = if lct_flag {
395                    let mut lp: Vec<[u8; 3]> = Vec::new();
396                    for _ in 0..lct_size {
397                        if p + 3 > data.len() {
398                            return None;
399                        }
400                        lp.push([data[p], data[p + 1], data[p + 2]]);
401                        p += 3;
402                    }
403                    lp
404                } else {
405                    global_palette.clone()
406                };
407                if palette.is_empty() {
408                    return None;
409                }
410                if img_w == 0 || img_h == 0 || img_w > 4096 || img_h > 4096 {
411                    return None;
412                }
413
414                // LZW 最小コードサイズ
415                if p >= data.len() {
416                    return None;
417                }
418                let min_code_size = data[p];
419                p += 1;
420
421                // データサブブロックを結合
422                let mut lzw_data: Vec<u8> = Vec::new();
423                loop {
424                    if p >= data.len() {
425                        return None;
426                    }
427                    let bs = data[p] as usize;
428                    p += 1;
429                    if bs == 0 {
430                        break;
431                    }
432                    if p + bs > data.len() {
433                        return None;
434                    }
435                    lzw_data.extend_from_slice(&data[p..p + bs]);
436                    p += bs;
437                }
438
439                let indices = gif_lzw_decode(&lzw_data, min_code_size, (img_w * img_h) as usize)?;
440
441                // インデックス → RGBA(インターレース解除込み)
442                let total = (img_w * img_h) as usize;
443                let mut rgba = alloc::vec![0u8; total * 4];
444                let row_order: Vec<u32> = if interlace {
445                    gif_interlace_rows(img_h)
446                } else {
447                    (0..img_h).collect()
448                };
449                let mut src = 0usize;
450                for &dst_row in row_order.iter() {
451                    for col in 0..img_w {
452                        if src >= indices.len() {
453                            break;
454                        }
455                        let ci = indices[src];
456                        src += 1;
457                        let dst = ((dst_row * img_w + col) as usize) * 4;
458                        if Some(ci) == transparent_idx {
459                            rgba[dst + 3] = 0; // 透過
460                        } else {
461                            let c = palette.get(ci as usize).copied().unwrap_or([0, 0, 0]);
462                            rgba[dst] = c[0];
463                            rgba[dst + 1] = c[1];
464                            rgba[dst + 2] = c[2];
465                            rgba[dst + 3] = 255;
466                        }
467                    }
468                }
469                let _ = (screen_w, screen_h);
470                return Some(super::DecodedImage {
471                    width: img_w,
472                    height: img_h,
473                    rgba,
474                });
475            }
476            0x3B => break, // トレーラ
477            _ => {
478                p += 1;
479            }
480        }
481    }
482    None
483}
484
485/// GIF サブブロック列を読み飛ばし、終端(0x00)の次位置を返す
486fn skip_gif_subblocks(data: &[u8], mut p: usize) -> Option<usize> {
487    loop {
488        if p >= data.len() {
489            return None;
490        }
491        let bs = data[p] as usize;
492        p += 1;
493        if bs == 0 {
494            return Some(p);
495        }
496        p += bs;
497    }
498}
499
500/// GIF インターレース行順を展開(4パス)
501fn gif_interlace_rows(h: u32) -> Vec<u32> {
502    let mut rows = Vec::with_capacity(h as usize);
503    let mut y = 0;
504    while y < h {
505        rows.push(y);
506        y += 8;
507    }
508    y = 4;
509    while y < h {
510        rows.push(y);
511        y += 8;
512    }
513    y = 2;
514    while y < h {
515        rows.push(y);
516        y += 4;
517    }
518    y = 1;
519    while y < h {
520        rows.push(y);
521        y += 2;
522    }
523    rows
524}
525
526/// GIF の可変長 LZW デコード。出力はパレットインデックス列。
527fn gif_lzw_decode(data: &[u8], min_code_size: u8, expected: usize) -> Option<Vec<u8>> {
528    let clear_code = 1u16 << min_code_size;
529    let end_code = clear_code + 1;
530    let mut code_size = min_code_size + 1;
531
532    // 辞書: 各エントリは「前エントリ index」と「末尾バイト」。文字列は逆引きで復元。
533    let mut prefix: Vec<i32> = Vec::with_capacity(4096);
534    let mut suffix: Vec<u8> = Vec::with_capacity(4096);
535    let reset_dict = |prefix: &mut Vec<i32>, suffix: &mut Vec<u8>| {
536        prefix.clear();
537        suffix.clear();
538        for i in 0..clear_code {
539            prefix.push(-1);
540            suffix.push(i as u8);
541        }
542        prefix.push(-1);
543        suffix.push(0); // clear_code
544        prefix.push(-1);
545        suffix.push(0); // end_code
546    };
547    reset_dict(&mut prefix, &mut suffix);
548
549    let mut out: Vec<u8> = Vec::with_capacity(expected);
550    let mut bit_pos = 0usize;
551    let total_bits = data.len() * 8;
552    let mut prev_code: i32 = -1;
553    let mut stack: Vec<u8> = Vec::with_capacity(4096);
554
555    let read_code = |bit_pos: &mut usize, code_size: u8| -> Option<u16> {
556        if *bit_pos + code_size as usize > total_bits {
557            return None;
558        }
559        let mut val = 0u32;
560        for i in 0..code_size as usize {
561            let byte = data[(*bit_pos + i) >> 3];
562            let bit = (byte >> ((*bit_pos + i) & 7)) & 1;
563            val |= (bit as u32) << i;
564        }
565        *bit_pos += code_size as usize;
566        Some(val as u16)
567    };
568
569    while let Some(code) = read_code(&mut bit_pos, code_size) {
570        if code == clear_code {
571            reset_dict(&mut prefix, &mut suffix);
572            code_size = min_code_size + 1;
573            prev_code = -1;
574            continue;
575        }
576        if code == end_code {
577            break;
578        }
579
580        // 現コードを文字列に展開(stack に逆順で積む)
581        stack.clear();
582        let mut cur = code as i32;
583        let first_byte;
584        if (cur as usize) < suffix.len() {
585            // 既知コード
586            while cur >= 0 && (cur as usize) < suffix.len() {
587                stack.push(suffix[cur as usize]);
588                cur = prefix[cur as usize];
589            }
590            first_byte = *stack.last()?;
591        } else {
592            // 未知コード(KwKwK ケース): prev + prev先頭
593            if prev_code < 0 {
594                return None;
595            }
596            let mut c2 = prev_code;
597            while c2 >= 0 && (c2 as usize) < suffix.len() {
598                stack.push(suffix[c2 as usize]);
599                c2 = prefix[c2 as usize];
600            }
601            first_byte = *stack.last()?;
602            stack.insert(0, first_byte); // 末尾に first を追加(逆順なので先頭 insert)
603        }
604
605        // stack を正順で出力
606        for &b in stack.iter().rev() {
607            out.push(b);
608        }
609
610        // 辞書追加: prev + first_byte
611        if prev_code >= 0 && suffix.len() < 4096 {
612            prefix.push(prev_code);
613            suffix.push(first_byte);
614            // コードサイズ拡張
615            if suffix.len() == (1usize << code_size) && code_size < 12 {
616                code_size += 1;
617            }
618        }
619        prev_code = code as i32;
620
621        if out.len() >= expected {
622            break;
623        }
624    }
625
626    Some(out)
627}
628
629/// 溜まっている取得要求に対してワーカーを起こし直す。
630/// 理由は `font_queue::pump` と同じ(門が閉じている間の投入を拾い直す)。
631pub(super) fn pump() {
632    // 【2026-09-06 バグ修正】`font_queue::pump` と同じ理由で `try_lock`。
633    // 毎フレーム `draw()` から呼ばれるため、高優先度側がここで
634    // 回り続けると低優先度のワーカーが走れなくなる。
635    let Some(mut q) = IMAGE_QUEUE.try_lock() else {
636        return;
637    };
638    // 【2026-08-25】パニックで死んだワーカーぶんの枠を戻す。
639    let dead = super::fetch_limit::take_image_deaths();
640    if dead > 0 {
641        crate::warn!("[IMG] パニックで死んだワーカー {} 本を回収", dead);
642        q.active_workers = q.active_workers.saturating_sub(dead);
643        store_worker_count(q.active_workers);
644    }
645    if q.pending.is_empty() {
646        return;
647    }
648    // CSS など、より優先度の高いリソースが待っている間は起こさない。
649    let higher = super::font_queue::pending_higher_than(
650        super::fetch_limit::Kind::Image.priority(),
651    );
652    if q.active_workers < MAX_IMAGE_WORKERS
653        && super::fetch_limit::may_spawn_lower(
654            super::fetch_limit::main_load_in_flight(),
655            super::fetch_limit::active(),
656            q.pending.len(),
657            higher,
658        )
659        && super::fetch_limit::try_acquire(q.pending.len())
660    {
661        q.active_workers += 1;
662        store_worker_count(q.active_workers);
663        // 【2026-08-05 バグ修正】`spawn` はヒープ不足で **0 を返す**。
664        // 戻り値を見ずに枠だけ確保していたため、失敗すると
665        // 「ワーカーは居ないのに枠は埋まったまま」になり、
666        // 以後**永久にワーカーを起こせなくなる**。失敗したら必ず返す。
667        let pid = crate::kernel::scheduler::spawn(
668            image_loader_thread,
669            crate::kernel::scheduler::Priority::Low,
670            "img_load",
671        );
672        if pid == 0 {
673            q.active_workers = q.active_workers.saturating_sub(1);
674            store_worker_count(q.active_workers);
675            super::fetch_limit::release();
676        }
677    }
678}
679
680fn image_loader_thread() {
681    // 枠の所有を自分の PID で宣言する(`fetch_limit` 参照)。
682    let my_pid = crate::kernel::scheduler::get_current_process_id().unwrap_or(0);
683    super::fetch_limit::claim(my_pid, super::fetch_limit::KIND_IMAGE);
684    loop {
685        // pop と「キューが空なら終了」の判定を同一ロック内で行い、
686        // enqueue_image との終了レース(アイテムが永久に処理されない)を防ぐ。
687        let req = {
688            let mut q = IMAGE_QUEUE.lock();
689            let popped = q.pending.pop();
690            IMG_PENDING.store(q.pending.len(), core::sync::atomic::Ordering::Release);
691            match popped {
692                Some(r) => r,
693                None => {
694                    q.active_workers = q.active_workers.saturating_sub(1);
695                    store_worker_count(q.active_workers);
696                    drop(q);
697                    super::fetch_limit::unclaim(my_pid);
698                    super::fetch_limit::release();
699                    // 【2026-08-05 重大】ここは `return` だった。
700                    // カーネルスレッドの入口関数から `return` しても
701                    // スレッドは終わらず、入口から再実行され続ける
702                    // (フォント側で実測 3 億回。`font_queue.rs` 参照)。
703                    crate::kernel::scheduler::exit();
704                }
705            }
706        };
707
708        // 【2026-08-05 計測】取得とデコードのどちらが支配的かを切り分ける。
709        // これが分からないまま片方だけ速くしても意味が無い。
710        // 目的: 画像パスの律速の特定。1 画像につき 1 回で毎フレームではない。
711        // 破棄条件: 律速が確定して対処が済んだら削除する。
712        let t_fetch_start = crate::kernel::timer::get_ticks();
713        let polls_before =
714            crate::kernel::net::tcp::RECV_POLLS.load(core::sync::atomic::Ordering::Relaxed);
715        let ec_before =
716            crate::kernel::usb::ETH_POLL_CALLS.load(core::sync::atomic::Ordering::Relaxed);
717        let et_before =
718            crate::kernel::usb::ETH_POLL_US.load(core::sync::atomic::Ordering::Relaxed);
719        let em_before =
720            crate::kernel::usb::ETH_POLL_LOCK_MISS.load(core::sync::atomic::Ordering::Relaxed);
721        let hc_before =
722            crate::kernel::usb::HID_POLL_CALLS.load(core::sync::atomic::Ordering::Relaxed);
723        let ht_before =
724            crate::kernel::usb::HID_POLL_US.load(core::sync::atomic::Ordering::Relaxed);
725        let he_before =
726            crate::kernel::usb::HID_ETH_US.load(core::sync::atomic::Ordering::Relaxed);
727        let frames_before =
728            crate::kernel::usb::RX_FRAME_COUNT.load(core::sync::atomic::Ordering::Relaxed);
729        let mut from_cache = false;
730        let raw: Option<Vec<u8>> = if req.is_local {
731            // 【2026-08-05】ここは**ワーカースレッド**(最大 4 本)の中。
732            // `cache.rs` 側だけロックしても、この直接呼び出しが残っていると
733            // 同じビットマップを無防備に触ることになる
734            // (`kernel::fs::FS_LOCK` の説明を参照)。
735            let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
736            let fs = crate::kernel::fs::get_fs();
737            fs.read_file(&req.path).map(|(_m, d)| d)
738        } else {
739            if let Some(cached) = read_cache(&req.host, &req.path) {
740                from_cache = true;
741                Some(cached)
742            } else {
743                let stack = crate::kernel::net_stack::TcpIpStack::new();
744                match stack.web_get_binary(req.is_https, &req.host, &req.path) {
745                    Ok(d) => {
746                        write_cache(&req.host, &req.path, &d);
747                        Some(d)
748                    }
749                    Err(e) => {
750                        crate::warn!(
751                            "IMAGE_LOADER: fetch failed for {}{}: {}",
752                            req.host,
753                            req.path,
754                            e.message
755                        );
756                        None
757                    }
758                }
759            }
760        };
761
762        if let Some(bytes) = raw {
763            // 【2026-07-30】サイズ・画素数による打ち切りは撤去した。
764            // 取得は非同期(このスレッド)なので描画をブロックしない。
765            // 「大きいから捨てる」のはブラウザの挙動として誤り
766            // (`spec/resource_loading.md` の R-2)。
767            // 時間がかかっても最後まで取得し、デコードできたら反映する。
768            let t_fetch_end = crate::kernel::timer::get_ticks();
769            let _t_dec = super::perf::start();
770            let decoded_result = decode_image(&bytes);
771            super::perf::add(super::perf::Slot::ImageDecode, _t_dec);
772            crate::warn!(
773                "[IMGPERF] path={} bytes={} cache={} fetch={}tick decode={}tick polls={} eth_calls={} eth_us={} eth_lockmiss={} frames={} hid_calls={} hid_us={} hid_eth_us={}",
774                req.path,
775                bytes.len(),
776                from_cache,
777                t_fetch_end.wrapping_sub(t_fetch_start),
778                crate::kernel::timer::get_ticks().wrapping_sub(t_fetch_end),
779                crate::kernel::net::tcp::RECV_POLLS
780                    .load(core::sync::atomic::Ordering::Relaxed)
781                    .wrapping_sub(polls_before),
782                crate::kernel::usb::ETH_POLL_CALLS
783                    .load(core::sync::atomic::Ordering::Relaxed)
784                    .wrapping_sub(ec_before),
785                crate::kernel::usb::ETH_POLL_US
786                    .load(core::sync::atomic::Ordering::Relaxed)
787                    .wrapping_sub(et_before),
788                crate::kernel::usb::ETH_POLL_LOCK_MISS
789                    .load(core::sync::atomic::Ordering::Relaxed)
790                    .wrapping_sub(em_before),
791                crate::kernel::usb::RX_FRAME_COUNT
792                    .load(core::sync::atomic::Ordering::Relaxed)
793                    .wrapping_sub(frames_before),
794                crate::kernel::usb::HID_POLL_CALLS
795                    .load(core::sync::atomic::Ordering::Relaxed)
796                    .wrapping_sub(hc_before),
797                crate::kernel::usb::HID_POLL_US
798                    .load(core::sync::atomic::Ordering::Relaxed)
799                    .wrapping_sub(ht_before),
800                crate::kernel::usb::HID_ETH_US
801                    .load(core::sync::atomic::Ordering::Relaxed)
802                    .wrapping_sub(he_before)
803            );
804            match decoded_result {
805                Some(decoded) => {
806                    // 【2026-08-02】取得・デコードの完了が**どこにも出ていなかった**ため、
807                    // 「画像が届いていないのか、届いたが描かれていないのか」を
808                    // 切り分けられなかった。フォント側(`[FONT] 背景取得完了`)と
809                    // 揃えて 1 画像につき 1 回だけ出す(`spec/logging_policy.md` L-3 の
810                    // 「節目で 1 回」に該当。毎フレームは出さない)。
811                    crate::warn!(
812                        "[IMG] 取得・デコード完了 host={} path={} bytes={} {}x{}",
813                        req.host,
814                        req.path,
815                        bytes.len(),
816                        decoded.width,
817                        decoded.height
818                    );
819                    let mut q = IMAGE_QUEUE.lock();
820                    q.results.push((req.src_key, decoded));
821                    store_results_len(q.results.len());
822                }
823                None => {
824                    // 非対応フォーマット(WebP/GIF/SVG 等)や破損データ
825                    let fmt =
826                        if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP"
827                        {
828                            "WebP (unsupported)"
829                        } else if bytes.len() >= 6
830                            && (&bytes[0..6] == b"GIF89a" || &bytes[0..6] == b"GIF87a")
831                        {
832                            "GIF (unsupported)"
833                        } else if bytes.len() >= 5 && &bytes[0..5] == b"<?xml"
834                            || (bytes.len() >= 4 && &bytes[0..4] == b"<svg")
835                        {
836                            "SVG (unsupported)"
837                        } else {
838                            "unknown/corrupt"
839                        };
840                    crate::warn!(
841                        "IMAGE_LOADER: decode failed for {} ({} bytes, format={})",
842                        req.src_key,
843                        bytes.len(),
844                        fmt
845                    );
846                }
847            }
848        }
849    }
850}
851
852/// デコード完了した画像が「取り込み待ち」で存在するか(エッジトリガ再描画用)。
853/// 旧実装は pending/thread_active も見ていたため、画像のロード待ちの間ずっと
854/// 全画面再描画が走り続け(画面が波打つ)原因になっていた。
855/// 完了結果がある時だけ true を返すことで、完了ごとに1回だけ再描画する。
856pub fn image_results_ready() -> bool {
857    IMG_RESULTS.load(core::sync::atomic::Ordering::Acquire) > 0
858}
859
860/// 取り込み待ちの完了件数の写し。
861///
862/// 【2026-09-04 バグ修正】ここは `IMAGE_QUEUE.lock()` していた。
863///
864/// この関数は**毎フレームの再描画判定**(`needs_timer_redraw`)から
865/// 呼ばれる。呼び出し元は提示ループ側の高優先度な文脈で、
866/// `spin::Mutex` は譲らずに回り続けるので、低優先度のワーカーが
867/// このロックを握っている間に入ると**ワーカーが走れず解放もされない**。
868/// 同じ優先度逆転は `pending_count`/`font_queue::pending_len` で
869/// 既に踏んでおり(実測で取得が残ったまま静止)、
870/// **待機・再描画の判定はロックを取ってはいけない**という規則になっている。
871/// ここはその規則から漏れていた。
872///
873/// 正確さは、`results` を変更する側(既にロックを持っている側)が
874/// 毎回ここへ書き写すことで保つ。
875static IMG_RESULTS: core::sync::atomic::AtomicUsize =
876    core::sync::atomic::AtomicUsize::new(0);
877
878/// `results` の件数を写す。ロックを持っている側から呼ぶ。
879pub(super) fn store_results_len(n: usize) {
880    IMG_RESULTS.store(n, core::sync::atomic::Ordering::Release);
881}
882
883/// 画像 src をキューに追加し、必要ならローダースレッドを起動する
884/// 取得待ちの画像枚数の写し。
885///
886/// 【2026-08-25 バグ修正】以前はここで `IMAGE_QUEUE.lock()` していた。
887/// 待機判定(`[IDLE]` 行)はウィンドウマネージャの提示ループから
888/// 呼ばれる。あちらは高優先度で、`spin::Mutex` は譲らずに回るので、
889/// 低優先度のワーカーがこのロックを握っている間に提示ループが
890/// スピンに入ると、**ワーカーが走れず解放もされない**(優先度逆転)。
891/// 実測で取得が 7 件やり残しのまま 900 秒動かなくなった。
892///
893/// 待機判定は**ロックを取らずに読む**。正確さは、キューを
894/// 変更する側(ロックを持っている側)が毎回書き写すことで保つ。
895static IMG_PENDING: core::sync::atomic::AtomicUsize =
896    core::sync::atomic::AtomicUsize::new(0);
897
898pub(super) fn pending_count() -> usize {
899    IMG_PENDING.load(core::sync::atomic::Ordering::Acquire)
900}
901
902/// 稼働中の画像ワーカー本数の写し(`font_queue::worker_count` と同じ理由)。
903static IMG_WORKERS: core::sync::atomic::AtomicUsize =
904    core::sync::atomic::AtomicUsize::new(0);
905
906pub(super) fn worker_count() -> usize {
907    IMG_WORKERS.load(core::sync::atomic::Ordering::Acquire)
908}
909
910/// 稼働本数を写す。ロックを持っている側から呼ぶ。
911pub(super) fn store_worker_count(n: usize) {
912    IMG_WORKERS.store(n, core::sync::atomic::Ordering::Release);
913}
914
915pub(super) fn enqueue_image(req: ImageFetchReq) {
916    let mut q = IMAGE_QUEUE.lock();
917    // 既にキューにある場合はスキップ
918    if q.pending.iter().any(|r| r.src_key == req.src_key) {
919        return;
920    }
921    q.pending.push(req);
922    IMG_PENDING.store(q.pending.len(), core::sync::atomic::Ordering::Release);
923    // 待ち行列があるうちはワーカーを増やす(上限まで)。
924    // 【2026-08-05】同上。上限は `fetch_limit` が全体で持つ。
925    if q.active_workers < MAX_IMAGE_WORKERS
926        && super::fetch_limit::try_acquire(q.pending.len())
927    {
928        q.active_workers += 1;
929        store_worker_count(q.active_workers);
930        // 【2026-08-05 バグ修正】`spawn` はヒープ不足で **0 を返す**。
931        // 戻り値を見ずに枠だけ確保していたため、失敗すると
932        // 「ワーカーは居ないのに枠は埋まったまま」になり、
933        // 以後**永久にワーカーを起こせなくなる**。失敗したら必ず返す。
934        let pid = crate::kernel::scheduler::spawn(
935            image_loader_thread,
936            crate::kernel::scheduler::Priority::Normal,
937            "img_load",
938        );
939        if pid == 0 {
940            q.active_workers = q.active_workers.saturating_sub(1);
941            store_worker_count(q.active_workers);
942            super::fetch_limit::release();
943        }
944    }
945}
946
947// ───────────── canvas から読むための共有 ─────────────
948
949/// デコード済み画像を `src` で引ける共有の置き場。
950///
951/// `WebEngine::image_cache` は `WebEngine` の中にあり、JS 組み込み
952/// (`drawImage`)からは届かない。かといって複製すると数 MB の写真を
953/// 二重に持つことになるので、`Arc` で**同じ画素を共有**する。
954///
955/// 【注意】ここのロックは**待機・再描画の判定から取ってはいけない**。
956/// 提示ループ側は高優先度で `spin::Mutex` は譲らないため、
957/// 低優先度のワーカーが握っていると優先度逆転で止まる
958/// (`image_results_ready` で実際に踏んだ)。
959/// 読むのは JS の `drawImage` からだけにし、`Arc` を複製して即座に放す。
960static SHARED_IMAGES: spin::Mutex<
961    alloc::collections::BTreeMap<alloc::string::String, alloc::sync::Arc<super::DecodedImage>>,
962> = spin::Mutex::new(alloc::collections::BTreeMap::new());
963
964/// 共有の置き場へ登録する。取り込み側から呼ぶ。
965pub fn share_image(key: &str, img: alloc::sync::Arc<super::DecodedImage>) {
966    SHARED_IMAGES.lock().insert(alloc::string::String::from(key), img);
967}
968
969/// `src` からデコード済み画像を引く。無ければ `None`。
970pub fn shared_image(key: &str) -> Option<alloc::sync::Arc<super::DecodedImage>> {
971    SHARED_IMAGES.lock().get(key).cloned()
972}
973
974/// ページを移ったら捨てる(`image_cache` の破棄と対で呼ぶ)。
975pub fn clear_shared_images() {
976    SHARED_IMAGES.lock().clear();
977}