<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>移植 on CloudLake</title><link>https://cloudlake.eu.org/tags/%E7%A7%BB%E6%A4%8D/</link><description>Recent content in 移植 on CloudLake</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Sat, 24 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://cloudlake.eu.org/tags/%E7%A7%BB%E6%A4%8D/index.xml" rel="self" type="application/rss+xml"/><item><title>在浏览器里养一台计算器：TI-Nspire 在线模拟器从 0 到「长期使用」</title><link>https://cloudlake.eu.org/p/nspire-web-emulator/</link><pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/nspire-web-emulator/</guid><description>&lt;img src="https://cloudlake.eu.org/p/nspire-web-emulator/cover.jpg" alt="Featured image of post 在浏览器里养一台计算器：TI-Nspire 在线模拟器从 0 到「长期使用」" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;一个 GPL 的 ARM 计算器模拟器、一个从 2016 年捞出来的 Palm 游戏、一台 1995 年的 Windows 95 机器，最后都被塞进了同一个网页里。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;现在打开 &lt;a class="link" href="https://cloudlake.eu.org/nspire/" target="_blank" rel="noopener"&#10; &gt;cloudlake.eu.org/nspire&lt;/a&gt;，你会看到四张卡片：经典灰度 Touchpad、彩色 CX II CAS、一个彩蛋 Windows 95，以及长期使用的登录入口。下面这篇文章把这个平台从架构到踩坑完整讲一遍。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="入口页：四张卡片" class="gallery-image" data-flex-basis="305px" data-flex-grow="127" height="1100" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry_hu_623494c3318391c8.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一最后做出来的东西"&gt;&lt;a href="#%e4%b8%80%e6%9c%80%e5%90%8e%e5%81%9a%e5%87%ba%e6%9d%a5%e7%9a%84%e4%b8%9c%e8%a5%bf" class="header-anchor"&gt;&lt;/a&gt;一、最后做出来的东西&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;三种「机器」共用一个页面&lt;/strong&gt;：经典 Nspire Touchpad（灰度 320×240）、CX II CAS（彩色 16 位色）、以及通过 WiNspire 跑的 Windows 95&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;完整键盘 + 触摸板 + 文件传输&lt;/strong&gt;：70 个键的屏幕键盘、触摸板滑动/点击、拖拽/整文件夹上传、目录浏览与下载&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;游戏一键启动&lt;/strong&gt;：点一下就从服务器把 nPal（仙剑奇侠传）传进计算器并自动运行&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;长期使用模式&lt;/strong&gt;：用户名登录，状态存服务器，每 2 分钟自动保存，下次登录直接接着用&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="CX II 在线模拟器整页：左边屏幕、右边键盘、下面是文件传输和快速启动游戏" class="gallery-image" data-flex-basis="258px" data-flex-grow="107" height="1301" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2_hu_6547d1e51d011008.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;p&gt;经典灰度机的页面是同一个文件，只是换了镜像：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="经典 Touchpad 页面" class="gallery-image" data-flex-basis="252px" data-flex-grow="105" height="1331" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic_hu_872dbeeb73d70df4.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二地基把-firebird-编译成-webassembly"&gt;&lt;a href="#%e4%ba%8c%e5%9c%b0%e5%9f%ba%e6%8a%8a-firebird-%e7%bc%96%e8%af%91%e6%88%90-webassembly" class="header-anchor"&gt;&lt;/a&gt;二、地基：把 Firebird 编译成 WebAssembly&#10;&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/nspire-emus/firebird" target="_blank" rel="noopener"&#10; &gt;Firebird&lt;/a&gt; 是一个开源（GPL-3.0）的 TI-Nspire 模拟器，桌面版基于 Qt。它模拟的是 ARM926 级别的 SoC、LCD 控制器、键盘矩阵、NAND flash 和 USB 控制器。&lt;/p&gt;&#10;&lt;p&gt;网页版做的事情是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;用 emscripten 把 Firebird core（约 33 个源文件）编译成 &lt;code&gt;firebird.js&lt;/code&gt;（约 88 KB）+ &lt;code&gt;firebird.wasm&lt;/code&gt;（约 230 KB）&lt;/li&gt;&#10;&lt;li&gt;页面把 &lt;strong&gt;flash 镜像&lt;/strong&gt;（整块 NAND 转储，经典机 33 MB，CX II 132 MB）写进 Emscripten 的内存文件系统&lt;/li&gt;&#10;&lt;li&gt;可选加载 &lt;strong&gt;snapshot&lt;/strong&gt;（gzip 压缩的 RAM + CPU 状态），实现「秒开」&lt;/li&gt;&#10;&lt;li&gt;用 &lt;code&gt;paintLCD()&lt;/code&gt; 把 320×240 的帧缓冲画到 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; 上&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;几个设计决定后来被证明是对的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;一个 &lt;code&gt;emu.html&lt;/code&gt; 支持所有机型&lt;/strong&gt;：机型差异全部收敛到 URL 参数 &lt;code&gt;?flash=...&amp;amp;boot=...&amp;amp;snap=...&lt;/code&gt;，页面代码是同一份。经典机、CX II、Windows 95、长期使用模式都复用这套加载逻辑。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;镜像放服务器、状态放浏览器/账号&lt;/strong&gt;：静态镜像走 Caddy 的 precompressed（brotli/gzip），经典 flash 压缩后约 12 MB，CX II 约 17 MB，浏览器缓存 30 天。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;快照优先&lt;/strong&gt;：冷启动 60 秒 vs 快照恢复几秒，体验差距巨大，所以后来能存快照的场景都尽量存。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="三让模拟器能用键盘触摸板文件传输"&gt;&lt;a href="#%e4%b8%89%e8%ae%a9%e6%a8%a1%e6%8b%9f%e5%99%a8%e8%83%bd%e7%94%a8%e9%94%ae%e7%9b%98%e8%a7%a6%e6%91%b8%e6%9d%bf%e6%96%87%e4%bb%b6%e4%bc%a0%e8%be%93" class="header-anchor"&gt;&lt;/a&gt;三、让模拟器「能用」：键盘、触摸板、文件传输&#10;&lt;/h2&gt;&lt;p&gt;只有画面没有输入，模拟器就是个屏保。这部分是平台真正的骨架。&lt;/p&gt;&#10;&lt;h3 id="31-键盘70-键矩阵"&gt;&lt;a href="#31-%e9%94%ae%e7%9b%9870-%e9%94%ae%e7%9f%a9%e9%98%b5" class="header-anchor"&gt;&lt;/a&gt;3.1 键盘：70 键矩阵&#10;&lt;/h3&gt;&lt;p&gt;Firebird 的键盘是 8×11 矩阵，网页端只暴露一个 &lt;code&gt;setKey(row, col, state)&lt;/code&gt;。页面上用 DOM 画了 70 个键，按下时转成矩阵坐标注入。&lt;/p&gt;&#10;&lt;p&gt;唯一的小坑：OS 的键盘扫描有间隔，太快松开会漏键，所以前端给每次按键加了 &lt;strong&gt;最少 80 ms 的保持时间&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h3 id="32-触摸板从只认方向键到真正能推鼠标"&gt;&lt;a href="#32-%e8%a7%a6%e6%91%b8%e6%9d%bf%e4%bb%8e%e5%8f%aa%e8%ae%a4%e6%96%b9%e5%90%91%e9%94%ae%e5%88%b0%e7%9c%9f%e6%ad%a3%e8%83%bd%e6%8e%a8%e9%bc%a0%e6%a0%87" class="header-anchor"&gt;&lt;/a&gt;3.2 触摸板：从「只认方向键」到真正能推鼠标&#10;&lt;/h3&gt;&lt;p&gt;这是整个项目最值得写的一段。TI-Nspire 的触摸板在 OS 里既当 &lt;strong&gt;5 向键&lt;/strong&gt;用（菜单导航），又当 &lt;strong&gt;鼠标&lt;/strong&gt;用（图形、几何、Windows 95）。&lt;/p&gt;&#10;&lt;p&gt;原来的实现只会模拟「按触摸板边缘 → 方向键」，菜单能走，但 &lt;strong&gt;GUI 指针纹丝不动&lt;/strong&gt;。原因是：OS 的指针用的是触摸板驱动算出来的&lt;strong&gt;相对位移&lt;/strong&gt;（&lt;code&gt;touchpad_rel_x/y&lt;/code&gt;），而页面从来没把连续滑动坐标送进去。&lt;/p&gt;&#10;&lt;p&gt;修复思路：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;拖动时按 ~38 Hz 连续调用 &lt;code&gt;setTouchpadNorm(x, y, contact=1, down=0)&lt;/code&gt;，让 Firebird 的 &lt;code&gt;touchpad_set_state()&lt;/code&gt; 计算相对位移&lt;/li&gt;&#10;&lt;li&gt;轻点 = 按下脉冲（中心 = 左键，边缘 = 方向键；&lt;code&gt;Ctrl&lt;/code&gt; + 轻点 = 右键）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;混合回退&lt;/strong&gt;：如果滑动了 220 ms、画面完全没变化（TI-OS 菜单只认方向事件），自动退化成方向键手势，这样拖动导航不丢&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;修复后的效果（拖动前 / 后，Win95 的鼠标从画面中心移动到右下）：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="触摸板指针修复前后对比" class="gallery-image" data-flex-basis="593px" data-flex-grow="247" height="530" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad_hu_68942d18c4994de5.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad.png 1310w" width="1310"&gt;&lt;/p&gt;&#10;&lt;h3 id="33-文件传输把虚拟-usb-线变成拖拽上传"&gt;&lt;a href="#33-%e6%96%87%e4%bb%b6%e4%bc%a0%e8%be%93%e6%8a%8a%e8%99%9a%e6%8b%9f-usb-%e7%ba%bf%e5%8f%98%e6%88%90%e6%8b%96%e6%8b%bd%e4%b8%8a%e4%bc%a0" class="header-anchor"&gt;&lt;/a&gt;3.3 文件传输：把虚拟 USB 线变成拖拽上传&#10;&lt;/h3&gt;&lt;p&gt;Firebird 的 USB Link 层允许宿主机和模拟器里的 OS 互传文件。我们在 WASM 里加了这些导出：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuPutFile(local, remote) MEMFS → 计算器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuGetFile(remote, dest) 计算器 → MEMFS&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuNewDir / emuDirList / emuDelete / emuMove&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuUsblinkProgress / Busy / Done / Abort&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;前端在此之上做了：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;上传队列&lt;/strong&gt;：串行传输、逐文件进度条、失败自动重试（OS 侧偶发返回负数错误码时，&lt;code&gt;emuUsblinkAbort()&lt;/code&gt; + 重连再试）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;整文件夹上传&lt;/strong&gt;：&lt;code&gt;&amp;lt;input webkitdirectory&amp;gt;&lt;/code&gt; + 拖拽目录时用 &lt;code&gt;webkitGetAsEntry()&lt;/code&gt; 递归，保留相对路径，按层级 &lt;code&gt;emuNewDir&lt;/code&gt; 自动建目录&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;只收 &lt;code&gt;.tns&lt;/code&gt; 的 OS 限制&lt;/strong&gt;：实测 OS 文档服务只接受 &lt;code&gt;.tns&lt;/code&gt;，其它扩展名（&lt;code&gt;.txt/.dat/.bin/.zip/.py/...&lt;/code&gt;）一律被拒；文件夹里的非 &lt;code&gt;.tns&lt;/code&gt; 会在界面上标「跳过」，并提示先改名&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="文件传输面板：文件夹上传、队列、只收 .tns 的说明" class="gallery-image" data-flex-basis="583px" data-flex-grow="243" height="497" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders_hu_aa2fe1ce3e9363eb.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders.png 1208w" width="1208"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="四踩过的最大的坑页面弹输入窗口然后冻结"&gt;&lt;a href="#%e5%9b%9b%e8%b8%a9%e8%bf%87%e7%9a%84%e6%9c%80%e5%a4%a7%e7%9a%84%e5%9d%91%e9%a1%b5%e9%9d%a2%e5%bc%b9%e8%be%93%e5%85%a5%e7%aa%97%e5%8f%a3%e7%84%b6%e5%90%8e%e5%86%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;四、踩过的最大的坑：页面「弹输入窗口」然后冻结&#10;&lt;/h2&gt;&lt;p&gt;有一段时间用户反馈：&lt;strong&gt;装 Ndless 的时候浏览器弹出一个输入窗口，然后整个页面卡死&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;追下去发现链路非常长：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Ndless 安装器在模拟器里触发了非法指令（&lt;code&gt;Prefetch abort&lt;/code&gt; / &lt;code&gt;Cannot set T bit&lt;/code&gt;）&lt;/li&gt;&#10;&lt;li&gt;Firebird 的异常处理会进入内置调试器&lt;/li&gt;&#10;&lt;li&gt;WASM 版的调试器输入函数还在读 &lt;code&gt;stdin&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Emscripten 的 stdin 兜底实现是 &lt;strong&gt;&lt;code&gt;window.prompt(&amp;quot;Input: &amp;quot;)&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;window.prompt&lt;/code&gt; 会&lt;strong&gt;阻塞页面主线程&lt;/strong&gt; → 整个模拟器看起来死了&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;修复是在 &lt;code&gt;firebird/emscripten/main.cpp&lt;/code&gt; 里给 &lt;code&gt;gui_debugger_request_input()&lt;/code&gt; 换成直接返回一条 &lt;code&gt;c&lt;/code&gt;（continue），让异常路径走 &lt;code&gt;EVENT_RESET&lt;/code&gt; 重启 OS，而不是等用户输入。&lt;/p&gt;&#10;&lt;p&gt;顺带修掉的还有一个自己引入的回归：&lt;strong&gt;点击画面曾经等于点触摸板中心&lt;/strong&gt;，会误开文档/对话框。现在画面单击只负责取键盘焦点，点击请用触摸板区域。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="五npal-移植把-2016-年的-palm-游戏跑进浏览器"&gt;&lt;a href="#%e4%ba%94npal-%e7%a7%bb%e6%a4%8d%e6%8a%8a-2016-%e5%b9%b4%e7%9a%84-palm-%e6%b8%b8%e6%88%8f%e8%b7%91%e8%bf%9b%e6%b5%8f%e8%a7%88%e5%99%a8" class="header-anchor"&gt;&lt;/a&gt;五、nPal 移植：把 2016 年的 Palm 游戏跑进浏览器&#10;&lt;/h2&gt;&lt;p&gt;nPal 是网友 ntzyz 把 &lt;strong&gt;PalmOS 时代的《仙剑奇侠传》&lt;/strong&gt; 移植到 TI-Nspire 的项目。我们拿到的是 2016 年的 nPal 1.2 发布包，里面是一个 1.25 MB 的 &lt;code&gt;sdlpal.tns&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="51-旧包为什么会在-cx-ii-上弹兼容性模式"&gt;&lt;a href="#51-%e6%97%a7%e5%8c%85%e4%b8%ba%e4%bb%80%e4%b9%88%e4%bc%9a%e5%9c%a8-cx-ii-%e4%b8%8a%e5%bc%b9%e5%85%bc%e5%ae%b9%e6%80%a7%e6%a8%a1%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;5.1 旧包为什么会在 CX II 上弹「兼容性模式」&#10;&lt;/h3&gt;&lt;p&gt;旧的 &lt;code&gt;sdlpal.tns&lt;/code&gt; 是 &lt;strong&gt;PRG 容器&lt;/strong&gt;（文件头 &lt;code&gt;50 52 47 00&lt;/code&gt;），里面根本没有 &lt;code&gt;Zehn&lt;/code&gt; 签名。Ndless r2022 会用旧加载路径跑它，于是：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;读不到 Zehn 标志位表&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;supports_hww&lt;/code&gt; 保持 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Ndless 弹出「Activating compatibility mode. This application hasn&amp;rsquo;t been updated to work with your hardware.」&lt;/li&gt;&#10;&lt;li&gt;走 320×320 的 LCD 兼容层，CX II 的原生 240×320 旋转屏适配失效&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="52-重新编译"&gt;&lt;a href="#52-%e9%87%8d%e6%96%b0%e7%bc%96%e8%af%91" class="header-anchor"&gt;&lt;/a&gt;5.2 重新编译&#10;&lt;/h3&gt;&lt;p&gt;用 &lt;strong&gt;Ndless r2022 SDK&lt;/strong&gt; 重新编译 nPal 的 SDL 部分：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;SDK 自带的 nSDL 1.1.1 已经改用新的 &lt;code&gt;lcd_blit&lt;/code&gt; 屏幕 API&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;genzehn&lt;/code&gt; 打包时扫描 ELF 符号，发现 &lt;code&gt;_genzehn_new_lcd_api&lt;/code&gt; 标记，自动写入 &lt;code&gt;USES_LCD_BLIT = true&lt;/code&gt; 和 &lt;code&gt;RUNS_ON_HWW = true&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;产物 &lt;code&gt;sdlpal.tns&lt;/code&gt; 419,248 B（纯 Zehn，无 PRG 外壳），&lt;code&gt;genzehn --info&lt;/code&gt; 能直接验证：&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Application: sdlpal&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This executable uses the new lcd_blit API.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This executable supports the HW-W 240x320 LCD.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;另外两条工具链（官方 GCC 14.2 和 apt GCC 13）构建出的版本，在模拟器里&lt;strong&gt;逐帧逐像素完全一致&lt;/strong&gt;，最后分发用官方工具链版。&lt;/p&gt;&#10;&lt;h3 id="53-资源文件与启动"&gt;&lt;a href="#53-%e8%b5%84%e6%ba%90%e6%96%87%e4%bb%b6%e4%b8%8e%e5%90%af%e5%8a%a8" class="header-anchor"&gt;&lt;/a&gt;5.3 资源文件与启动&#10;&lt;/h3&gt;&lt;p&gt;nPal 的资源是 &lt;code&gt;resources.bin.tns&lt;/code&gt;（23.9 MB）和 &lt;code&gt;desc.dat.tns&lt;/code&gt;（16 KB）——它们本质上就是数据文件，只是 &lt;strong&gt;故意用 &lt;code&gt;.tns&lt;/code&gt; 扩展名&lt;/strong&gt;，这样才能通过 OS 的文档服务传进计算器。全部放进计算器的 &lt;code&gt;/npal&lt;/code&gt;，运行 &lt;code&gt;sdlpal.tns&lt;/code&gt; 即可。&lt;/p&gt;&#10;&lt;p&gt;在模拟器里看到的结果：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="nPal 启动后的仙剑标题画面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-npal.png" width="640"&gt;&lt;/p&gt;&#10;&lt;h3 id="54-一键游戏启动"&gt;&lt;a href="#54-%e4%b8%80%e9%94%ae%e6%b8%b8%e6%88%8f%e5%90%af%e5%8a%a8" class="header-anchor"&gt;&lt;/a&gt;5.4 一键游戏启动&#10;&lt;/h3&gt;&lt;p&gt;手动上传三个文件太麻烦，于是做了「快速启动游戏」：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;服务器放 &lt;code&gt;games.json&lt;/code&gt; 清单 + 每个游戏的文件（同时生成 &lt;code&gt;.br/.gz&lt;/code&gt; 供 Caddy 预压缩）&lt;/li&gt;&#10;&lt;li&gt;页面选择 nPal → 依次下载 3 个文件 → 自动创建 &lt;code&gt;/npal&lt;/code&gt; → 走 USB 上传 → 主程序最后上传，保证文件浏览器选中它 → 自动发回车运行&lt;/li&gt;&#10;&lt;li&gt;旁边还有「安装 Ndless」按钮：自动上传安装器+资源，再按固定节奏发 Enter 走完安装流程&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;实测（线上）：&lt;strong&gt;安装 Ndless 66 秒 + 启动游戏 59 秒&lt;/strong&gt;，然后就是标题画面。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="文件面板里的快速启动游戏行" class="gallery-image" data-flex-basis="458px" data-flex-grow="190" height="633" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel_hu_a319d50a3e91baf0.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel.png 1208w" width="1208"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Ndless 安装器在模拟器里的界面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-ndless.png" width="640"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="六彩蛋在计算器里跑-windows-95"&gt;&lt;a href="#%e5%85%ad%e5%bd%a9%e8%9b%8b%e5%9c%a8%e8%ae%a1%e7%ae%97%e5%99%a8%e9%87%8c%e8%b7%91-windows-95" class="header-anchor"&gt;&lt;/a&gt;六、彩蛋：在计算器里跑 Windows 95&#10;&lt;/h2&gt;&lt;p&gt;WiNspire 是 &lt;strong&gt;tiny386（x86 PC 模拟器）的 CX II 移植&lt;/strong&gt;，以 Ndless 程序运行，Native 模式给 guest 16 MB RAM / 256 KB 显存。于是流程变成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;浏览器 → Firebird(WASM，模拟 CX II) → TI-OS + Ndless → winspire.tns(tiny386) → Windows 95&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;磁盘镜像是从 PCjs 的 Windows 95 RTM 演示机用它的 “Save HD” 导出的 68 MB raw image&lt;/li&gt;&#10;&lt;li&gt;把镜像改名成 &lt;code&gt;disk.img.tns&lt;/code&gt;，和 &lt;code&gt;winspire.tns&lt;/code&gt;、bios、vgabios、ini 一起上传到 &lt;code&gt;/winspire&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;冷启动大约 4–5 分钟：SeaBIOS → Windows 95 开机画面 → 桌面&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="WiNspire 里的 SeaBIOS 在浏览器中启动" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-seabios.png" width="640"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Windows 95 启动画面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-win95-boot.png" width="640"&gt;&lt;/p&gt;&#10;&lt;p&gt;为了把「开机 4 分钟」变成「秒进桌面」，又做了预烘焙：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;在模拟器里跑到 Win95 桌面&lt;/li&gt;&#10;&lt;li&gt;调用 &lt;code&gt;saveSnapshot()&lt;/code&gt; 生成快照&lt;/li&gt;&#10;&lt;li&gt;关键：Firebird 的 NAND 修改默认只在内存里，必须调用新加的 &lt;code&gt;saveFlash()&lt;/code&gt;（内部就是 &lt;code&gt;flash_save_changes()&lt;/code&gt;）把修改块写回 &lt;code&gt;flash.img&lt;/code&gt;，否则导出的镜像还是原样&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;最终页面加载 &lt;code&gt;flash&lt;/code&gt;（brotli 后 17.2 MB）+ &lt;code&gt;snapshot&lt;/code&gt;（33 MB，基本不可压缩），点开始几秒后直接是 Windows 95 桌面：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Windows 95 桌面（浏览器内）" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-win95.png" width="640"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="七长期使用用户名登录--状态持久化"&gt;&lt;a href="#%e4%b8%83%e9%95%bf%e6%9c%9f%e4%bd%bf%e7%94%a8%e7%94%a8%e6%88%b7%e5%90%8d%e7%99%bb%e5%bd%95--%e7%8a%b6%e6%80%81%e6%8c%81%e4%b9%85%e5%8c%96" class="header-anchor"&gt;&lt;/a&gt;七、长期使用：用户名登录 + 状态持久化&#10;&lt;/h2&gt;&lt;p&gt;短期玩一玩，刷新页面就重来；但总有人想「养」一台自己的计算器。于是加了长期使用模式：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;登录页输入用户名（无密码，HMAC 签名 cookie 记身份）&lt;/li&gt;&#10;&lt;li&gt;一个用户名同一时间只能开一个窗口（心跳续锁，180 秒无心跳自动过期）&lt;/li&gt;&#10;&lt;li&gt;新用户从干净的 CX II 系统开始（默认镜像）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;保存 = flash 镜像 + 内存快照&lt;/strong&gt;：&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;saveFlash()&lt;/code&gt; 把 NAND 修改写回 MEMFS 的 &lt;code&gt;flash.img&lt;/code&gt;（138 MB）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;saveSnapshot()&lt;/code&gt; 生成 &lt;code&gt;snapshot.bin&lt;/code&gt;（gzip 流）&lt;/li&gt;&#10;&lt;li&gt;浏览器里用 &lt;code&gt;CompressionStream('gzip')&lt;/code&gt; 把 flash 压到约 19 MB，和快照（约 10–33 MB）一起 POST 到服务器&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;恢复 = 直接加载 flash + snapshot&lt;/strong&gt;，从快照恢复现场&lt;/li&gt;&#10;&lt;li&gt;每 2 分钟自动保存一次；「保存」按钮手动存；「退出」先保存再注销&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;后端是一个 &lt;strong&gt;Python 标准库写的单文件服务&lt;/strong&gt;（&lt;code&gt;nspire-lt&lt;/code&gt;，systemd 托管），Caddy 把 &lt;code&gt;/nspire/api/&lt;/code&gt; 反代到它，数据落在 &lt;code&gt;/var/lib/nspire-lt/users/&amp;lt;user&amp;gt;/&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;实测完整流程（登录 → 启动 → 打开图形应用 → 保存 → 退出 → 再登录）：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="err"&gt;已保存&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;restore&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="mi"&gt;9236&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="err"&gt;恢复后画面与保存时逐像素一致&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;&lt;img alt="长期使用登录页" class="gallery-image" data-flex-basis="305px" data-flex-grow="127" height="1100" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt_hu_27e2ee6570edcb8b.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="八数据与性能"&gt;&lt;a href="#%e5%85%ab%e6%95%b0%e6%8d%ae%e4%b8%8e%e6%80%a7%e8%83%bd" class="header-anchor"&gt;&lt;/a&gt;八、数据与性能&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;机型&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;flash 原始&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;首屏（br 压缩）&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;启动&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;备注&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;经典 Touchpad&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;33 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;~12 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;快照恢复极快&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;灰度 320×240&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CX II CAS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;132 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;17.2 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;冷启动 ~60 s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;彩色 16 位色&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Windows 95&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;132 MB + 68 MB 磁盘&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;~50 MB（含快照）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;冷启动 4–5 分钟，快照秒开&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;双重模拟，慢&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;长期使用&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;每用户 flash + 快照&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;登录时下载&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;快照恢复 ~9 s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;服务器存档&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;一些数字：模拟 USB 传输约 &lt;strong&gt;550 KB/s&lt;/strong&gt;（23 MB 的 nPal 资源约 40–60 秒）；一次长期存档上行约 &lt;strong&gt;20–50 MB&lt;/strong&gt;；长期模式每人磁盘占用约 &lt;strong&gt;30–50 MB&lt;/strong&gt;（gzip 后）。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="九还欠着的"&gt;&lt;a href="#%e4%b9%9d%e8%bf%98%e6%ac%a0%e7%9d%80%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;九、还欠着的&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Windows 95 冷启动版&lt;/strong&gt;：预烘焙时遇到 NAND 写回/flash 保存的边界问题，目前只保留快照直启入口&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Ndless 冷启动激活状态&lt;/strong&gt;：干净镜像冷启动后 Ndless 不是激活状态，需要在游戏面板点一次「安装 Ndless」（约 1 分钟）；长期使用模式里装好保存后，快照会保留激活状态&lt;/li&gt;&#10;&lt;li&gt;移动端触控（目前触摸板/鼠标事件主要按桌面调优）&lt;/li&gt;&#10;&lt;li&gt;音频：Firebird 的 PC 喇叭/声音还没接&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="结语"&gt;&lt;a href="#%e7%bb%93%e8%af%ad" class="header-anchor"&gt;&lt;/a&gt;结语&#10;&lt;/h2&gt;&lt;p&gt;这个项目最花时间的部分，其实不是「把模拟器编成 WASM」——那是几小时的活。真正难的是&lt;strong&gt;层与层之间的接缝&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;p&gt;浏览器输入 → ARM 模拟器 → TI-OS 驱动 → Ndless → 游戏/Windows，每一层都有自己的假设。触摸板的「方向 vs 指针」、文件系统的「只收 .tns」、调试器的「读 stdin」、NAND 的「写缓存」，每一个都足以让整个平台看起来「坏了」。&lt;/p&gt;&#10;&lt;p&gt;把这些接缝一个个对上之后，才有现在这个能在浏览器里养计算器、打仙剑、开 Windows 95、还能下次登录接着用的东西。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;入口：&lt;a class="link" href="https://cloudlake.eu.org/nspire/" target="_blank" rel="noopener"&#10; &gt;https://cloudlake.eu.org/nspire/&lt;/a&gt;&lt;br&gt;&#10;长期使用：&lt;a class="link" href="https://cloudlake.eu.org/nspire/lt/index.html" target="_blank" rel="noopener"&#10; &gt;https://cloudlake.eu.org/nspire/lt/index.html&lt;/a&gt;&lt;br&gt;&#10;技术栈：Firebird（GPL-3.0）+ emscripten/WASM + Hugo + Caddy + 一个 Python 小服务&lt;/p&gt;&#10;</description></item></channel></rss>