diff --git a/CODE/dsk/main.c b/CODE/dsk/main.c index ed5f1eb..4664ea3 100644 --- a/CODE/dsk/main.c +++ b/CODE/dsk/main.c @@ -950,7 +950,86 @@ static int fat32_write_root_file(const char *name11, const u8 *data, u32 size) { } } else { /* 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++) { 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; } diff --git a/CODE/firstInit/main.c b/CODE/firstInit/main.c index e713c9c..af881c9 100644 --- a/CODE/firstInit/main.c +++ b/CODE/firstInit/main.c @@ -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_SAVE 6 #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) { 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); 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) { @@ -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, "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); } @@ -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(); 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; 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); for (int i=0;i<30;i++) delay_frame(); 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); continue; @@ -1607,13 +1631,20 @@ void dsk_entry(const dsk_boot_context *ctx) { net.connected = 0; net.ssid[0] = 0; net.password[0] = 0; - /* 始终显示网络设置页面,即使无无线设备也展示页面让用户确认 */ - 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"); + /* 无无线设备则直接跳过网络页:联网从来不是进系统的前置条件, + * 让用户在一个没有任何可选网络的页面上只能按 Save(或误按 Connect 吃 + * NET-E01 panic)是设计缺陷。有设备时仍展示页面供确认/配置。 */ + if (g_no_wireless_device) { + logl("[FirstInit] no wireless device; skipping network setup page"); + net.connected = 0; + } 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); /* Pass config buffer to DSK via boot context reserved fields */ diff --git a/CODE/linux/pack_system_image.sh b/CODE/linux/pack_system_image.sh index 69f928d..bf0ef8e 100644 --- a/CODE/linux/pack_system_image.sh +++ b/CODE/linux/pack_system_image.sh @@ -167,17 +167,25 @@ if [[ -z "$p1" || -z "$p2" ]]; then exit 1 fi -# 簇大小交给 mkfs.vfat 自动选择。 +# ESP 的簇大小必须显式指定为 4 个扇区(2KiB),不能交给 mkfs 自动选。 # -# 历史坑一:这里曾经强制 -s 8(4KiB 簇)。256MiB 的 ESP 恰好被算成 65535 -# 个簇(FAT32 的 0xFFF5 边界),该几何下 OVMF(EDK2 FatPkg) 拒挂该卷 —— -# 现象是 BdsDxe "Not Found",Shell 里分区只显示 BLKn、没有对应 FS。 -# 对照实验:sidecar(CODE/linux/make_sata_sidecar.sh)用不带 -s 的 -# mkfs.vfat 就一切正常,所以这里也不再手工指定簇大小。 +# 历史坑一(OVMF 挂不上):曾经写死 -s 8(4KiB 簇)。256MiB 的 ESP 在该几何下 +# 恰好得到 65535 个簇 —— 正是 FAT32 的 0xFFF5 边界 —— OVMF(EDK2 FatPkg) 拒挂 +# 该卷,现象是 BdsDxe "Not Found",Shell 里分区只有 BLKn 没有 FS。 # -# 历史坑二:不要加 -f 1。UEFI 规范建议 ESP 用单 FAT 表,但实测 OVMF 对 -# nfat=1/2 都能挂,加 -f 1 并不能解决上面的问题,反而偏离 sidecar 的行为。 -mkfs.vfat -F 32 -n DESHABBOOT "$p1" >/dev/null +# 历史坑二(根目录写满):修坑一时改成不指定 -s 交给 mkfs 自动选,它给出 +# spc=1(512B/簇)。根目录每簇只能放 16 个目录项,而打包进 ESP 根目录的内容 +# (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 树, # 不支持 journal、metadata_csum、64bit(遇到即 panic FS-E28)—— # 因此这里必须显式关闭这三个 feature。 diff --git a/build.ps1 b/build.ps1 index 0baf947..2180d1a 100644 --- a/build.ps1 +++ b/build.ps1 @@ -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 { & $make -C "$DskDir" -f MAKEFILE "CC=$clang" "LD=$lld" if ($LASTEXITCODE -ne 0) { diff --git a/docs/STATUS.md b/docs/STATUS.md index 020756e..ae310af 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -44,7 +44,7 @@ | 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` | | 热卸载(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 彩色行) | ## DSK 主内核 / 用户态 @@ -52,7 +52,7 @@ | 子系统 | 状态 | 边界/说明 | 证据 | |--------|------|----------|------| | 启动 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` | | mouseInit(PS/2 鼠标) | ✅ | IRQ12 + 包解码 + 光标(M1) | `.build_tmp/qemu_serial_mouse.log` | | 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) | -| 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 复现) | | 向导后强制重登 | ✅ | first-boot 分支 FirstInit→persist→login;登录跳过/失败 → FS-E06 panic | 同上(login success → desktop ready) | | NET-E01 网络页 Connect panic | ✅ | 无 wlan 栈,Connect 即 panic;Save(仅存配置) 为唯一前进路径 | 同上(NET-E01 复现,fb pmemsave 确认莲花屏) |