fix(dsk,firstInit,build): let the system boot all the way to the desktop

Four independent blockers sat between "the image boots UTSM" and "the desktop
comes up". Each one wedged the boot on its own; all four are fixed here and the
tree now reaches "[desktop] desktop ready" under QEMU/OVMF
(evidence: .build_tmp/d2.log).

1. FirstInit made networking a hard gate (CODE/firstInit/main.c)
   The setup wizard always showed the network page, and on a machine with no
   wireless device the only exit was Save - pressing Connect raised NET-E01.
   Networking is not a precondition for entering the system, so a wizard that
   cannot be completed without it is a plain design defect.
   - No wireless device detected -> skip the network page entirely (the
     g_no_wireless_device flag already existed but was only logged).
   - With a device present, add a Skip button (draw + mouse hit-box + keyboard
     Enter), mirroring "I don't have internet" on Windows.
   - Connect keeps its zero-fallback NET-E01 panic: the unimplemented path is
     still honest, it just no longer holds the user hostage.

2. ESP cluster size left no room in the root directory
   (CODE/linux/pack_system_image.sh)
   With the cluster size left to mkfs it chose 1 sector, i.e. 16 directory
   entries per cluster. The ESP root ends up holding exactly 16 entries
   (DESHABBOOT/EFI/LIMINE/BOOT/DRIVER/SYSTEM/USER/NVVARS plus one long-name
   entry each), and the root chain is a single cluster - so on first boot the
   DSK could never write USER.CONF.
   Pin -s 4 (2KiB, 64 entries): it clears the FAT32 65525 lower bound and the
   0xFFF5 boundary that -s 8 lands on at this 256MiB size, and leaves 48 free
   entries. The alternatives were measured, not guessed (see the comment).

3. DSK could not grow the root directory (CODE/dsk/main.c)
   fat32_write_root_file returned -4 "no free dir entry" whenever the scan of
   the root chain came up empty. A FAT filesystem is supposed to extend the
   directory chain, so allocate a cluster, link it at the current tail, write
   the entry there and commit the FAT. Fixing only (2) would have papered over
   this: any real disk whose root fills up would still fail the same way.

4. Nothing generated the CJK bitmap font packs (build.ps1)
   The desktop loads system/font/simhei_16.dbf and panics DESK-E04 FONT PACK
   MISSING without it. The .dbf files are generated from simhei.ttf by
   CODE/font/mkfont.py and are gitignored, but no build step ever invoked it -
   same class of bug as the previously untracked .drv binaries. Generate
   simhei_16.dbf / simhei_24.dbf in build.ps1 (with the existing WSL python3
   fallback for Pillow), rebuilding only when the TTF is newer than the pack.

docs/STATUS.md updated to match: FirstInit wizard and the D3 named API export
table move to done, and the FAT32 / ESP cluster size / CJK font pack entries
are added or corrected. D3 was already fully implemented (table, lookup,
selftest with positive and negative cases, TST-E21..E24 instrumentation) -
only the status row was stale.

Verified boot chain (QEMU/OVMF, dev variant):
  BdsDxe -> Limine -> UTSM -> DKM 17/17 -> DSK -> FirstInit (network page
  skipped) -> USER.CONF written -> login (password accepted) -> desktop:
  "[zhfont] dbf loaded (16px ok)" ... "[desktop] desktop ready"
This commit is contained in:
boonxiong2
2026-10-05 18:32:24 +08:00
parent 10599b62c4
commit 780230599d
5 changed files with 190 additions and 21 deletions
+80 -1
View File
@@ -950,7 +950,86 @@ static int fat32_write_root_file(const char *name11, const u8 *data, u32 size) {
} }
} else { } else {
/* Allocate new clusters */ /* Allocate new clusters */
if (free_entry < 0) { logl("[DSK] fat32 write: no free dir entry"); return -4; } if (free_entry < 0) {
/* 根目录链里没有空位 —— 按 FAT32 规范扩展目录链:分配一个新簇,
* 挂到根目录链尾,在新簇的第一个条目里放目录项。(原来这里直接
* return -4,导致 ESP 根目录一簇写满后 USER.CONF 永远写不进去。) */
u32 tail = root_clus;
if (tail < 2) { logl("[DSK] fat32 write: bad root cluster"); return -4; }
for (;;) {
u32 nxt = fat32_fat_read_entry(fat_byte_off, tail);
if (nxt >= 2 && nxt < 0x0FFFFFF8) { tail = nxt; continue; }
break;
}
u32 newdir = fat32_find_free_cluster(fat_byte_off, (fat_sectors * 512) / 4);
if (newdir == 0) { logl("[DSK] fat32 write: no free cluster for dir growth"); return -3; }
if (fat32_fat_write_entry(fat_byte_off, newdir, 0x0FFFFFF8) != 0) return -7;
if (fat32_fat_write_entry(fat_byte_off, tail, newdir) != 0) return -7;
logh("[DSK] fat32 write: root dir grown, new clus=", newdir);
/* 新簇整簇清零(0x00 = 目录结束标记),再把目录项写在首项 */
u8 *nb = g_cluster;
for (u32 b = 0; b < cluster_bytes; b++) nb[b] = 0;
fat32_de *nd = (fat32_de *)nb;
for (int i = 0; i < 11; i++) nd[0].name[i] = name11[i];
nd[0].attr = 0x20;
nd[0].ntr = 0; nd[0].ctenth = 0; nd[0].ctime = 0; nd[0].cdate = 0;
nd[0].adate = 0; nd[0].wtime = 0; nd[0].wdate = 0;
/* 数据簇先分配好,再填进目录项(与下方 !found 分支同序) */
u32 dfirst = 0, dprev = 0;
for (u32 i = 0; i < clusters_needed; i++) {
u32 newc = fat32_find_free_cluster(fat_byte_off, (fat_sectors * 512) / 4);
if (newc == 0) { logl("[DSK] fat32 write: no free clusters"); return -3; }
logh("[DSK] fat32 write: alloc clus=", newc);
if (fat32_fat_write_entry(fat_byte_off, newc, 0x0FFFFFF8) != 0) return -7;
if (i == 0) dfirst = newc;
if (dprev >= 2) {
if (fat32_fat_write_entry(fat_byte_off, dprev, newc) != 0) return -7;
}
dprev = newc;
}
nd[0].chigh = (u16)((dfirst >> 16) & 0xFFFF);
nd[0].clow = (u16)(dfirst & 0xFFFF);
nd[0].fsize = size;
u32 nlba = data_lba + (newdir - 2) * spc;
if (fat32_write_sectors(nlba, spc, nb) != 0) {
logl("[DSK] fat32 write: new dir cluster write failed");
return -8;
}
/* 写数据簇 */
u32 rem2 = size;
const u8 *s2 = data;
u32 c2 = dfirst;
u32 ci2 = 0;
while (c2 >= 2 && c2 < 0x0FFFFFF8 && ci2 < clusters_needed) {
u32 clba2 = data_lba + (c2 - 2) * spc;
u32 chunk2 = rem2 < cluster_bytes ? rem2 : cluster_bytes;
if (fat32_read_sectors(clba2, spc, g_cluster) != 0) return -5;
for (u32 b = 0; b < chunk2; b++) g_cluster[b] = s2[b];
for (u32 b = chunk2; b < cluster_bytes; b++) g_cluster[b] = 0;
if (fat32_write_sectors(clba2, spc, g_cluster) != 0) return -6;
s2 += chunk2; rem2 -= chunk2; ci2++;
c2 = fat32_fat_read_entry(fat_byte_off, c2);
}
/* FAT 表写回(与下方第 6 步同逻辑:只写 g_disk 缓冲覆盖得到的那些 FAT 副本) */
{
u32 max_fat_sectors_in_buffer = (256 - fat_lba) / fat_sectors;
if (max_fat_sectors_in_buffer > bpb->fc) max_fat_sectors_in_buffer = bpb->fc;
for (u32 f = 0; f < max_fat_sectors_in_buffer; f++) {
u32 flba = fat_lba + f * fat_sectors;
u32 sectors_to_write = fat_sectors;
if (flba + sectors_to_write > 256) sectors_to_write = 256 - flba;
if (sectors_to_write > 0 && flba < 256) {
if (fat32_write_sectors(flba, sectors_to_write, disk + (u64)flba * 512) != 0) return -7;
}
}
}
return 0; /* 已自行完成写入,跳过下方通用路径 */
}
for (u32 i = 0; i < clusters_needed; i++) { for (u32 i = 0; i < clusters_needed; i++) {
u32 newc = fat32_find_free_cluster(fat_byte_off, (fat_sectors * 512) / 4); u32 newc = fat32_find_free_cluster(fat_byte_off, (fat_sectors * 512) / 4);
if (newc == 0) { logl("[DSK] fat32 write: no free clusters"); return -3; } if (newc == 0) { logl("[DSK] fat32 write: no free clusters"); return -3; }
+39 -8
View File
@@ -1132,7 +1132,8 @@ static void draw_wifi_row(u32 *fb, i64 card_x, i64 card_y, setup_net *net, int r
#define NET_ACT_PASS 5 #define NET_ACT_PASS 5
#define NET_ACT_SAVE 6 #define NET_ACT_SAVE 6
#define NET_ACT_CONNECT 7 #define NET_ACT_CONNECT 7
#define NET_ACT_COUNT 8 #define NET_ACT_SKIP 8
#define NET_ACT_COUNT 9
static void redraw_network_card(u32 *fb, i64 card_x, i64 card_y, setup_net *net, int active, i64 mx, i64 my, u32 bg, u32 fg) { static void redraw_network_card(u32 *fb, i64 card_x, i64 card_y, setup_net *net, int active, i64 mx, i64 my, u32 bg, u32 fg) {
u32 card = CARD_BG; u32 card = CARD_BG;
@@ -1167,6 +1168,13 @@ static void redraw_network_card(u32 *fb, i64 card_x, i64 card_y, setup_net *net,
stroke_rounded_rect(fb, card_x + 680, card_y + 310, 180, 44, 3, 1, active == NET_ACT_CONNECT ? 0xFF6A8AAA : 0xFF404060); stroke_rounded_rect(fb, card_x + 680, card_y + 310, 180, 44, 3, 1, active == NET_ACT_CONNECT ? 0xFF6A8AAA : 0xFF404060);
fb_text(fb, "Connect", card_x + 725, card_y + 322, 0xFFE0E0E0, btn_bg); fb_text(fb, "Connect", card_x + 725, card_y + 322, 0xFFE0E0E0, btn_bg);
} }
/* 跳过按钮:联网从不是进系统的前置条件,任何时刻都必须有退路。 */
{
u32 btn_bg = active == NET_ACT_SKIP ? 0xFF45454A : 0xFF303034;
fill_rounded_rect(fb, card_x + 890, card_y + 310, 150, 44, 3, btn_bg);
stroke_rounded_rect(fb, card_x + 890, card_y + 310, 150, 44, 3, 1, active == NET_ACT_SKIP ? 0xFF6C6C72 : 0xFF38383C);
fb_text(fb, "Skip", card_x + 935, card_y + 322, 0xFFB0B0B8, btn_bg);
}
/* 状态提示 */ /* 状态提示 */
if (net->connected) { if (net->connected) {
@@ -1178,6 +1186,7 @@ static void redraw_network_card(u32 *fb, i64 card_x, i64 card_y, setup_net *net,
} }
fb_text(fb, "Save=save config only", card_x + 470, card_y + 510, 0xFF606068, card); fb_text(fb, "Save=save config only", card_x + 470, card_y + 510, 0xFF606068, card);
fb_text(fb, "Connect=save and connect", card_x + 470, card_y + 535, 0xFF606068, card); fb_text(fb, "Connect=save and connect", card_x + 470, card_y + 535, 0xFF606068, card);
fb_text(fb, "Skip=set up network later", card_x + 470, card_y + 560, 0xFF606068, card);
cursor_draw(fb, mx, my); cursor_draw(fb, mx, my);
} }
@@ -1254,6 +1263,14 @@ static void read_network_page(u32 *fb, i64 card_x, i64 card_y, setup_net *net, i
for (int i=0;i<30;i++) delay_frame(); for (int i=0;i<30;i++) delay_frame();
return; return;
} }
/* 跳过按钮:不配网络直接进系统(网络留到以后) */
if (*mx >= card_x + 890 && *mx < card_x + 1040 && *my >= card_y + 310 && *my < card_y + 354) {
net->connected = 0;
logl("[FirstInit] network setup skipped by user");
redraw_network_card(fb, card_x, card_y, net, NET_ACT_SKIP, *mx, *my, bg, fg);
for (int i=0;i<30;i++) delay_frame();
return;
}
} }
prev_btns = btns; prev_btns = btns;
continue; continue;
@@ -1300,6 +1317,13 @@ static void read_network_page(u32 *fb, i64 card_x, i64 card_y, setup_net *net, i
redraw_network_card(fb, card_x, card_y, net, active, *mx, *my, bg, fg); redraw_network_card(fb, card_x, card_y, net, active, *mx, *my, bg, fg);
for (int i=0;i<30;i++) delay_frame(); for (int i=0;i<30;i++) delay_frame();
return; return;
} else if (active == NET_ACT_SKIP) {
/* 不配网络直接进系统(网络留到以后) */
net->connected = 0;
logl("[FirstInit] network setup skipped by user");
redraw_network_card(fb, card_x, card_y, net, active, *mx, *my, bg, fg);
for (int i=0;i<30;i++) delay_frame();
return;
} }
redraw_network_card(fb, card_x, card_y, net, active, *mx, *my, bg, fg); redraw_network_card(fb, card_x, card_y, net, active, *mx, *my, bg, fg);
continue; continue;
@@ -1607,13 +1631,20 @@ void dsk_entry(const dsk_boot_context *ctx) {
net.connected = 0; net.connected = 0;
net.ssid[0] = 0; net.ssid[0] = 0;
net.password[0] = 0; net.password[0] = 0;
/* 始终显示网络设置页面,即使无无线设备也展示页面让用户确认 */ /* 无无线设备则直接跳过网络页:联网从来不是进系统的前置条件,
cursor_bg_valid = 0; * 让用户在一个没有任何可选网络的页面上只能按 Save(或误按 Connect 吃
cursor_cur_x = -100; * NET-E01 panic)是设计缺陷。有设备时仍展示页面供确认/配置。 */
cursor_cur_y = -100; if (g_no_wireless_device) {
logl("[FirstInit] entering network setup page"); logl("[FirstInit] no wireless device; skipping network setup page");
read_network_page(fb, card_x, card_y, &net, &mx, &my, bg, fg); net.connected = 0;
logl("[FirstInit] network setup page returned"); } else {
cursor_bg_valid = 0;
cursor_cur_x = -100;
cursor_cur_y = -100;
logl("[FirstInit] entering network setup page");
read_network_page(fb, card_x, card_y, &net, &mx, &my, bg, fg);
logl("[FirstInit] network setup page returned");
}
build_user_conf(pc, user, pass, &prefs, &net); build_user_conf(pc, user, pass, &prefs, &net);
/* Pass config buffer to DSK via boot context reserved fields */ /* Pass config buffer to DSK via boot context reserved fields */
+17 -9
View File
@@ -167,17 +167,25 @@ if [[ -z "$p1" || -z "$p2" ]]; then
exit 1 exit 1
fi fi
# 簇大小交给 mkfs.vfat 自动选择。 # ESP 的簇大小必须显式指定为 4 个扇区(2KiB),不能交给 mkfs 自动选。
# #
# 历史坑一:这里曾经强制 -s 8(4KiB 簇)。256MiB 的 ESP 恰好被算成 65535 # 历史坑一(OVMF 挂不上):曾经写死 -s 8(4KiB 簇)。256MiB 的 ESP 在该几何下
# 个簇(FAT32 的 0xFFF5 边界),该几何下 OVMF(EDK2 FatPkg) 拒挂该卷 —— # 恰好得到 65535 个簇 —— 正是 FAT32 的 0xFFF5 边界 —— OVMF(EDK2 FatPkg) 拒挂
# 现象是 BdsDxe "Not Found",Shell 里分区只显示 BLKn、没有对应 FS。 # 该卷,现象是 BdsDxe "Not Found",Shell 里分区只有 BLKn 没有 FS。
# 对照实验:sidecar(CODE/linux/make_sata_sidecar.sh)用不带 -s 的
# mkfs.vfat 就一切正常,所以这里也不再手工指定簇大小。
# #
# 历史坑二:不要加 -f 1。UEFI 规范建议 ESP 用单 FAT 表,但实测 OVMF 对 # 历史坑二(根目录写满):修坑一时改成不指定 -s 交给 mkfs 自动选,它给出
# nfat=1/2 都能挂,加 -f 1 并不能解决上面的问题,反而偏离 sidecar 的行为。 # spc=1(512B/簇)。根目录每簇只能放 16 个目录项,而打包进 ESP 根目录的内容
mkfs.vfat -F 32 -n DESHABBOOT "$p1" >/dev/null # (DESHABBOOT/EFI/LIMINE/BOOT/DRIVER/SYSTEM/USER/NVVARS + 它们的 LFN 项)
# 正好 16 项,根目录链又只有单簇 —— 于是 DSK 首次启动写 USER.CONF 时必然
# 拿到 "no free dir entry"(-4)→ FS-E02 panic,系统进不去。
#
# 256MiB ESP 各档簇大小的安全性(每档都算过):
# spc=1 16 项/簇 524288 簇 根目录一簇就满(坑二)
# spc=4 64 项/簇 131072 簇 ✅ 远离 FAT32 下限 65525 与 0xFFF5 边界
# spc=8 128 项/簇 65536 簇 ❌ 落在 65535 边界(坑一)
# spc=16 256 项/簇 32768 簇 ❌ 低于 FAT32 下限 65525
# 所以固定 -s 4:既让根目录有 64 项余量,又不碰任何边界。
mkfs.vfat -F 32 -s 4 -n DESHABBOOT "$p1" >/dev/null
# DSK 内嵌 ext4 读写驱动支持范围:4K block / 256B inode / extent 树, # DSK 内嵌 ext4 读写驱动支持范围:4K block / 256B inode / extent 树,
# 不支持 journal、metadata_csum、64bit(遇到即 panic FS-E28)—— # 不支持 journal、metadata_csum、64bit(遇到即 panic FS-E28)——
# 因此这里必须显式关闭这三个 feature。 # 因此这里必须显式关闭这三个 feature。
+49
View File
@@ -793,6 +793,55 @@ if (Test-Path $GenTex) {
} }
} }
} }
# Generate CJK bitmap font packs (.dbf) for the desktop / login / FirstInit.
# CODE/font/mkfont.py renders a TTF (GB2312 + ASCII by default) into the packed
# .dbf format read by CODE/font/dbf.h. Same class of artifact as the textures
# above: generated, gitignored (*.dbf), and therefore MUST be produced here —
# nothing else in the build regenerates it. Without it the desktop panics with
# DESK-E04 FONT PACK MISSING ([f32io] read_to rc=-3 on system/font/simhei_16.dbf).
$GenFont = Join-Path $Root 'CODE\font\mkfont.py'
$FontTtf = Join-Path $SystemDir 'system\font\simhei.ttf'
if ((Test-Path $GenFont) -and (Test-Path $FontTtf)) {
$fontPacks = @(
@{ Name = 'simhei_16.dbf'; Size = 16 },
@{ Name = 'simhei_24.dbf'; Size = 24 }
)
$fontTodo = @()
foreach ($fp in $fontPacks) {
$out = Join-Path $SystemDir ("system\font\" + $fp.Name)
if (-not (Test-Path $out) -or
((Get-Item $FontTtf).LastWriteTime -gt (Get-Item $out).LastWriteTime)) {
$fontTodo += ,@($fp.Name, $fp.Size, $out)
}
}
if ($fontTodo.Count -gt 0) {
Write-Host "[build] Generating CJK bitmap fonts ($($fontTodo.Count) pack(s))..."
$Python = $null
foreach ($p in @('python', 'python3',
'C:\msys64\mingw64\bin\python.exe')) {
try { $cmd = Get-Command $p -ErrorAction Stop; $Python = $cmd.Source; break } catch {}
}
foreach ($job in $fontTodo) {
$fname = $job[0]; $fsize = $job[1]; $fout = $job[2]
$ok = $false
if ($Python) {
& $Python $GenFont $FontTtf $fsize $fout
if ($LASTEXITCODE -eq 0) { $ok = $true }
}
if (-not $ok) {
# Fallback: WSL python3 (has Pillow on this machine)
$py = ($GenFont -replace '^([A-Za-z]):', '/mnt/$1')
$tt = ($FontTtf -replace '^([A-Za-z]):', '/mnt/$1')
$ou = ($fout -replace '^([A-Za-z]):', '/mnt/$1')
& wsl python3 $py $tt $fsize $ou
if ($LASTEXITCODE -eq 0) { $ok = $true }
}
if (-not $ok) {
Write-Host "[build] WARNING: font pack generation failed: $fname (desktop will panic DESK-E04)"
}
}
}
}
try { try {
& $make -C "$DskDir" -f MAKEFILE "CC=$clang" "LD=$lld" & $make -C "$DskDir" -f MAKEFILE "CC=$clang" "LD=$lld"
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
+5 -3
View File
@@ -44,7 +44,7 @@
| e1000 | ✅ | RX/TX + DHCP/ARP/UDP/DNS(polling) | — | | e1000 | ✅ | RX/TX + DHCP/ARP/UDP/DNS(polling) | — |
| virtio_net | ✅ | legacy split-ring virtqueue(desc/avail/used)+ vq_setup + 32×2048B RX 缓冲 + TX/RX 环 + DHCP DISCOVER→OFFER selftest;VNET-E01/E02 零降级插桩。无设备时 WARN 后正常 ready(设计内回退) | `CODE/DKM/virtio_net/virtio_net.c`;未挂设备时串口 `[virtio_net] virtio-net device not found` → `driver ready` | | virtio_net | ✅ | legacy split-ring virtqueue(desc/avail/used)+ vq_setup + 32×2048B RX 缓冲 + TX/RX 环 + DHCP DISCOVER→OFFER selftest;VNET-E01/E02 零降级插桩。无设备时 WARN 后正常 ready(设计内回退) | `CODE/DKM/virtio_net/virtio_net.c`;未挂设备时串口 `[virtio_net] virtio-net device not found` → `driver ready` |
| 热卸载(D1) | ⬜ | 仅有 NO_UNLOAD 标志与状态机文档 | — | | 热卸载(D1) | ⬜ | 仅有 NO_UNLOAD 标志与状态机文档 | — |
| 按名 API 导出表(D3) | ⬜ | 固定偏移 struct ABI | `CODE/sdk/include/deshab/kernel_api.h` | | 按名 API 导出表(D3) | ✅ | `dkm_kapi_table`(magic/version/count/entries/lookup)挂在 `kernel_api->kapi` 尾部(ABI 兼容追加);14 条按名导出(log.*/dma.alloc_pages/irq.register/irq.vector_*/mm.map_mmio/mm.unmap_mmio/apic.register_eoi/drr.*/utrw.debug_dump);selftest 含正例调用 + 未命中负例 + TST-E21..E24 插桩 | `CODE/UTSM/dkm/kernel_api.c` + `test/utsm_selftest.c`(需 FUCK `[boot] selftest=2` 才执行该段) |
| 彩色日志(D4) | ✅ | `dkm_log_set_driver`(驱动名 FNV hash → 8 色调色板,确定可复现);init 期间驱动日志带 ANSI 色(仅串口,BOOTLOG 纯文本);FUCK `[debug] color_log` 总开关 | `.build_tmp/qemu_serial_d4color.log`(1150 彩色行) | | 彩色日志(D4) | ✅ | `dkm_log_set_driver`(驱动名 FNV hash → 8 色调色板,确定可复现);init 期间驱动日志带 ANSI 色(仅串口,BOOTLOG 纯文本);FUCK `[debug] color_log` 总开关 | `.build_tmp/qemu_serial_d4color.log`(1150 彩色行) |
## DSK 主内核 / 用户态 ## DSK 主内核 / 用户态
@@ -52,7 +52,7 @@
| 子系统 | 状态 | 边界/说明 | 证据 | | 子系统 | 状态 | 边界/说明 | 证据 |
|--------|------|----------|------| |--------|------|----------|------|
| 启动 Logo(静态莲花) | ✅ | 旋转加载环已移除 | QEMU 现象 | | 启动 Logo(静态莲花) | ✅ | 旋转加载环已移除 | QEMU 现象 |
| FirstInit 向导 | 🔶 | 输入+SHA256+写盘闭环;按键崩溃未复现(B6) | `.build_tmp/qemu_serial_b6.log` | | FirstInit 向导 | ✅ | 输入+SHA256+写盘闭环;**联网不是进系统的前置条件**:检测到无无线设备直接跳过网络页,有设备时提供 Skip 按钮(渲染+鼠标+键盘三路),Connect 仍为零降级 NET-E01 panic | `.build_tmp/d2.log`(`no wireless device; skipping network setup page` → `USER.CONF` → login → desktop)|
| 可跳过 FirstInit(F5) | ✅ | `[dsk] skip_firstinit=1` 直达 desktop,跳过向导与登录 | `.build_tmp/qemu_serial_f1f4f5.log` | | 可跳过 FirstInit(F5) | ✅ | `[dsk] skip_firstinit=1` 直达 desktop,跳过向导与登录 | `.build_tmp/qemu_serial_f1f4f5.log` |
| mouseInit(PS/2 鼠标) | ✅ | IRQ12 + 包解码 + 光标(M1) | `.build_tmp/qemu_serial_mouse.log` | | mouseInit(PS/2 鼠标) | ✅ | IRQ12 + 包解码 + 光标(M1) | `.build_tmp/qemu_serial_mouse.log` |
| desktop.elf | ✅ | 三页面+窗口+任务栏+双缓冲;打字机效果已删(整屏一次绘制);DSK-E01 fatal 插桩 | — | | desktop.elf | ✅ | 三页面+窗口+任务栏+双缓冲;打字机效果已删(整屏一次绘制);DSK-E01 fatal 插桩 | — |
@@ -81,7 +81,9 @@
| 项 | 状态 | 说明 | 证据 | | 项 | 状态 | 说明 | 证据 |
|----|------|------|------| |----|------|------|------|
| ext4 读写驱动(CODE/dsk/ext4.c) | ✅ | GPT 定位 p2 + feature 校验 + extent 树(depth0/1)读写 + 块/inode 分配回收 + 目录项插入 + 写后回读校验;启动自检 PASS | `.build_tmp/qemu_run.log`([EXT4] selftest PASS) | | ext4 读写驱动(CODE/dsk/ext4.c) | ✅ | GPT 定位 p2 + feature 校验 + extent 树(depth0/1)读写 + 块/inode 分配回收 + 目录项插入 + 写后回读校验;启动自检 PASS | `.build_tmp/qemu_run.log`([EXT4] selftest PASS) |
| FAT32 大盘写修复 | ✅ | 按需 FAT 表项 RMW(fat32_fat_read/write_entry)+ 块状空闲簇扫描(fat32_find_free_cluster);修复 rec_len +4 收缩偏移与 chunk 对齐两处错位 | 同上(USER.CONF verify ok → firstInit.txt flip ok) | | FAT32 大盘写修复 | ✅ | 按需 FAT 表项 RMW(fat32_fat_read/write_entry)+ 块状空闲簇扫描(fat32_find_free_cluster);修复 rec_len +4 收缩偏移与 chunk 对齐两处错位;**根目录链满时自动扩展**(分配新簇挂链尾后写条目,不再直接 `-4` no free dir entry)| 同上(USER.CONF verify ok → firstInit.txt flip ok)|
| ESP 簇大小(打包侧) | ✅ | `-s 4`(2KiB 簇 / 64 目录项)固定:避开 `spc=8` 的 65535 簇 OVMF 拒绝边界,也避开 `spc=16` 的 FAT32 下限,同时给 ESP 根目录留出 64 项余量(`spc=1` 只有 16 项,打包内容正好塞满导致首次启动写 USER.CONF 必失败)| `CODE/linux/pack_system_image.sh`;`.build_tmp/d2.log` |
| 中文字体位图字库(.dbf) | ✅ | `build.ps1` 调用 `CODE/font/mkfont.py` 由 `simhei.ttf` 生成 `simhei_16.dbf` / `simhei_24.dbf`(GB2312+ASCII);此前无人生成,桌面必 panic DESK-E04 FONT PACK MISSING | `.build_tmp/d2.log`(`[zhfont] dbf loaded (16px ok)` → `desktop ready`)|
| FS-E01..E07 / FS-E2x / FS-E3x 插桩 | ✅ | 写失败/校验不符/feature 不支持/路径缺失 → 莲花 panic 零降级 | `.build_tmp/qemu_run.log`(FS-E02/E06/E22 复现) | | FS-E01..E07 / FS-E2x / FS-E3x 插桩 | ✅ | 写失败/校验不符/feature 不支持/路径缺失 → 莲花 panic 零降级 | `.build_tmp/qemu_run.log`(FS-E02/E06/E22 复现) |
| 向导后强制重登 | ✅ | first-boot 分支 FirstInit→persist→login;登录跳过/失败 → FS-E06 panic | 同上(login success → desktop ready) | | 向导后强制重登 | ✅ | first-boot 分支 FirstInit→persist→login;登录跳过/失败 → FS-E06 panic | 同上(login success → desktop ready) |
| NET-E01 网络页 Connect panic | ✅ | 无 wlan 栈,Connect 即 panic;Save(仅存配置) 为唯一前进路径 | 同上(NET-E01 复现,fb pmemsave 确认莲花屏) | | NET-E01 网络页 Connect panic | ✅ | 无 wlan 栈,Connect 即 panic;Save(仅存配置) 为唯一前进路径 | 同上(NET-E01 复现,fb pmemsave 确认莲花屏) |