Hacker News 中文摘要

RSS订阅

ZX Spectrum 图形桌面系统 -- A graphical desktop for the ZX Spectrum

一个为 ZX Spectrum 48K(ZX Spectrum 48K)用 Z80 汇编语言编写的图形桌面,带有 z 序和焦点的重叠窗口、下拉菜单、堆、事件队列、可换后端的存储层、对话框、控件、记事本、时钟、日历、双栏文件管理器,以及真正改变设置的设置面板。全部容纳在 1982 年机器的 48K 中,且能在一个 69,888 T 状态帧内拖动窗口。它运行在真实硬件上,而非仅模拟器。

正文

ZX Desk

为 ZX Spectrum 48K 编写的图形桌面,用 Z80 汇编语言写成。

重叠窗口带 z 序和焦点、下拉菜单、堆、事件队列、可切换后端的存储层、对话框、控件、记事本、时钟、日历、双栏文件管理器,以及一个真正能改设置的设置面板。这一切都塞进了 1982 年机器的 48K 里,并且能在一个 69,888 T 状态的帧内拖动窗口。

它运行在真机上,不只模拟器。

桌面

为什么

八十年代我想要一台 Atari ST 却买不起。我真正想要的是 GEM:桌面、窗口、常驻的菜单栏,那种机器是个场所而非提示符的感觉。我有的却是 Spectrum,于是长时间琢磨能在上面实现多少。我开始写一些片段,没写完。

所以这个项目是补全。它不是 GEM 的移植,也不假装是。它是一个想法撞上 3.5 MHz Z80、48K RAM、带属性冲突的单位显示,以及绘画时从 CPU 偷周期的视频芯片后变成的东西。许多答案比问题更有趣,几乎都来自测量机器而非推理。

这是个有趣且倾注感情的项目,写这么长是因为测量数据有用。如果你在这硬件上开发,下面的数字耗了我无数夜晚,现在给你。

今日功能

| | | | --- | --- | | 窗口 | 重叠、z 序、可移动、可缩放,带标题栏、关闭框和抓手。焦点即 z 序最前,因此提升和聚焦是一个动作。 | | 菜单 | 常驻菜单栏带下拉、保存底层、命中测试。 | | 输入 | Kempston 鼠标、Kempston 摇杆,以及经三张表解码带重复的完整键盘矩阵。全部以事件形式到达。 | | 事件 | 十六槽环形队列。主循环不含任何窗口特定代码。 | | 存储 | 六个向量后的后端注册表。RAM、磁带(通过真机 ROM 加载器)、128K 多余 bank 作 RAM 盘。esxDOS 留有保留 id。 | | 内存 | 带所有者字节的真实堆,8,112 字节,分配按窗口大小调整的窗口缓冲。 | | 应用 | 带 init、event、paint 的描述符,加每实例状态换入换出。记事本、时钟、日历、指挥官、关于。 | | 持久化 | 设置带幻数和版本写入存储,启动时读回。 |

截图:

两个窗口,z 序排列

记事本,带 Sinclair 风格移位报告光标

双栏指挥官,基于设备而非目录

时钟与日历

运行

工具链本地且小:pasmo 0.5.5,从源码构建到 tools/。

执行 ./build.sh 将 src/zxdesk.asm 汇编为 build/zxdesk.tap;./run.sh 构建并加载到机器;MACHINE=128 ./run.sh 在 128K 上同样操作并恢复设置。

esxDOS 也能跑,在带 64MB 卡映像的模拟 DivMMC 上。tools/ 不在仓库内,因为 pasmo、模拟器和 esxDOS ROM 无权再分发,需自行从 esxDOS 发布和 DivMMC 卡映像构建。建好后启动 tools/esxdos/esxdos.szx,esxDOS 已驻留;Machine > NMI 打开其文件浏览器。

构建标志均通过 --equ 传递:DEMO=1 ./build.sh 自拖动构建用于可复现抓取;NOWAIT=1 配合 DEMO 无光束调度器同款拖动;SCRIPT=1 ./build.sh 用合成输入驱动桌面;MOUSETEST=1 ./build.sh 原始 Kempston 鼠标诊断。

build.sh 必须显式传 --name,因为 pasmo 从输出路径精确取磁带头名称,否则会把 build/zxde 写入头。它也拒绝在代码增长到缓冲区段时构建磁带,因为这种溢出是静默的:首次窗口抓取会覆盖程序,几秒后机器抛无关错误掉进 BASIC。

在 Fuse 下拖窗口需用空格键而非鼠标键。 macOS Fuse 1.9.2 在按住按钮时停止传递 Kempston 鼠标移动,指针在拖拽开始瞬间冻结,窗口不跟随。指向标题栏,按住 SPACE,移动,松开。鼠标其他正常,按钮自身也正确注册;仅移动停止。

这是模拟器问题而非桌面,用 MOUSETEST=1 证实:它一次性读鼠标端口,端口与屏幕间无间隔,按钮按下时计数器仍静止。ReadInput 中的 RiBtn 使 SPACE 可用,是为无鼠机器也能用。真 Kempston 鼠标正常拖。

SCRIPT=1 值得了解。它用合成输入事件表而非鼠标驱动桌面,使交互(开菜单、选项、拖窗过另一窗、输入字段、保存)每次相同,可与上次比对而非观看。

如何构建

自底向上。

设备层。 DevFillRect、DevFillDesk、DeskFillCol、AddrAt、BlitRect、RectGrab。其上一切以字节列和像素行工作,从不直接触屏交错布局。这是移植可替换的边界,第一天就画好。

帧纪律。 主循环在中断时 halt,在顶部边框做所有指针工作,窗口动则等光束,重绘,帧末读输入分发。输入放最后使光束等待前开销恒定,调度因此精确。

事件队列。 十六槽四字节事件环。EvPoll 将原始输入转为指针移动、按钮和按键;EvDispatch 经处理表排出。

命中测试。 每控件五字节行,z 序从前到后,$FF 终止,每次搜索前从模型重填,故窗口位置无第二副本过期。关闭菜单高度零,永不匹配,故 HitTest 无特例。

瞬态表面。 四深竞技场,每面最大 16x96,推入弹出。两栈非一栈,因菜单下像素由无面板记录者推。

窗口。 记录换入活副本,同面板记录技巧,因窗口位置在六文件被引 93 次。z 序即反向绘画序和命中序。

存储。 后端注册表,每后端十四字节行:id、能力位、六入口。六操作是手写 JP 指令,操作数在选择时打补丁,分派耗 10 T 状态且不毁寄存器。

控件。 面板是行栈,每控件是一行,故行索引是三次移位而非搜索。四类,有用的是循环:复选框是上限 2 的循环,单选组是上限 N 的循环。设置面板、文件列表、保存框皆同代码不同表。

内存映射

$6000-$727C 慢速区:面板、日历、文件面板、桌面设置、指挥官。 $727D-$7FFF 空闲 3,460 字节,争用。 $8000-$B1B7 快速区:其他一切。 $B1B8-$BCFF 空闲 2,889 字节。 $BD00 栈顶。 $BDBD 中断处理。 $BE00-$BEFF 中断向量表。 $C000-$C63F RAM 盘、目录、磁带缓冲。 $C640-$C74F 活记事本状态。 $C750-$DF4F 四个瞬态表面。 $DF50-$FEFF 堆 8,112 字节。 $FF00-$FFFF 故意未用。

图中两点需解释。

慢速区因 ULA 在 $8000 下绘画时偷周期,代码慢约三分之一。规则:其中不得跑在帧内。无拖拽路径、指针路径或中断在内,时序逐 T 不变。始于 $6000 而非系统变量顶,因磁带加载器经 IY 索引系统变量区,BASIC 加载器在其上,覆盖加载程序的 CODE 块是新颖失败法。

堆止于内存顶前一页而非 $FFFF。每次行走计算下块为地址加头加大小,结束于 $10000 的块会回卷到零并比成低于堆基。停在 $FF00 耗 256 字节换除整类失败。

测量结果

这是读别人仓库时我想要的部分。每数字都跑代码对机器自身时钟计时。无指令计数,推导数目标注。

机器上的计时

可信的时钟

48K 中断周期恰 69,888 T 状态,程序无法移动,故是唯一可用钟。计时运行同步 HALT,可选延迟已知 T 放置例程于帧中,调用,数十六 T 循环至下中断。对比空校准抵消固定开销:

cost = (K - K0) * (69888 - 118) - 16 * (C - C0) - delay

118 T 是返回中断路径精确成本,逐指令数出。因处理、变量、计数环、栈皆在 $8000 上 ULA 不偷处。仅被计时例程触争用内存。

对三已知延迟盲测:

| 标称 | 测得 | 误差 | | --- | --- | --- | | 2,599 T | 2,592 T | −7 T | | 51,999 T | 52,000 T | +1 T | | 103,999 T | 103,994 T | −5 T |

最差误 7 T / 104,000,0.007%。第三跨帧边界,证实 118 T。

争用成本 14.7% 非 50%

每预算悲观取屏幕写 50% 罚,来自传说。扫 1,024 字节填充跨帧:

| 起点 | 成本 | | --- | --- | | 顶边 | 11,744 T | | 10,399 T | 12,913 T | | 20,799 T | 13,441 T | | 31,199 T | 13,473 T | | 41,599 T | 13,441 T | | 51,999 T | 12,369 T | | 底边 | 11,745 T |

两对边框差 1 T,即无争用成本。显示内最差 13,473 T,即 14.7%,约每争用字节 1.7 T。50% 预算保守三分之一,我的也是。

画窗成本半数是三短串

WinDraw 分四阶段定时。阶段和差 330 T 内,即四调用返回对加十六 T 粒度。

| 阶段 | 帧顶 | 显示中 | 占比 | | --- | --- | --- | --- | | 文本 | 35,872 T | 35,969 T | 50% | | 边 | 19,552 T | 19,841 T | 27% | | 填充 | 14,688 T | 16,641 T | 21% | | 关闭框 | 832 T | 849 T | 1% | | 整体 | 71,274 T | 73,099 T | |

二十七字符标题正文,约每字符 1,330 T。我以为是问题的填充仅占五分之一。优化填充只换拖帧几个百分点,我会耗一周。

测错物的基准

早先记录细胞感知推填 7.4 T/字节。真例程 11.5 T/字节,多 54%。基准测了技术;例程带基准未付的每行地址算。一行 183 T 中 88 T 是推链写像素,95 T 是寄存器交换、地址步进、细胞边界测和循环。

最快填充超半成本非写像素。推广:此处理器上每行开销是要攻的,非每字节吞吐。

中断是丢失非延迟

这是最想让同硬件他人知的,因故障像任何除因之事。

填充全程 DI,因 SP 走屏幕内存非栈。常识说延迟中断。否。Spectrum 仅 32 T 断言 INT 后撤,故覆盖此 32 T 的 DI 窗销毁中断而非推迟。

先观测后理解。填充放帧内 62,399 T 报未跨帧边界却明显跨了。重判丢失中断得 11,745 T 对顶边同填 11,744 T,差 1 T 证诊断。

后精确测。每指令注中断于 8x24 填充,一例程 500 边界拒 458,另 752 拒 710。

明显修复无效。 行间重启中断看似对却败,因中断非挂起是没了。每 236 T 宽几 T 的 EI 窗约三十行捕一。短 DI 区非无 DI 区,仅无是修。

有效是欠最后推。 填充现全程中断开。护的是 SP,故修是保证返回地址两字节总落将覆处。SP 取两类值:矩形内,推写恰链下一推所写;行末最后推后低点,链不返。故链比行短一推,最左两字节欠——推后一迭代付,SP 入下行不能及。

后:607 中 0、904 中 0 边界毁中断,矩形逐字节同,外未触,屏校验和不变。

| | 前 | 后 | | | --- | --- | --- | --- | | 16×96 矩形填 | 17,414 T | 22,068 T | +48 T/行 | | 16×96 桌面填 | 25,351 T | 30,125 T | +50 T/行 |

真成本,值。拖路径多列填经 HL 不触 SP 无险。

被它要抓之物抓的看门狗

帧看门狗计处理中中断和主循环帧;差即丢帧。处理是尴尬半,因在栈非栈的填充中 firing。

首版借 IX 且 INC (IX+0) 计数,上述中断扫首跑败。该指令设标志,填充在地址步进和跨边界分支间活携进位。隙中中断偷进位致行错址。

INC IX 是十六位增不改标志。故计数器为字,处理透明,耗 104 T 五十次秒不改一标志寄存器。路线图预见看门狗早捕中断 bug。实是中断测试捕了看门狗。

让处理推也死路。 推 AF 的处理用 SP 下四字节非两,欠区倍,每行多 22 T。

拖入一帧内

起点无望:整窗擦重绘 96,010 T 对帧 69,888 T,即 137%,故拖 25 Hz。四改序测:

| 改动 | 效果 | | --- | --- | | 离屏缓冲。 组入缓冲每帧 blit。贵在组非传。 | 重绘 71,274 T → 26,048 T | | 更快文本。 重写像素文本渲染。 | 1,307 T → 622 T/字符,2.10× | | 损坏矩形。 仅擦窗弃条非全窗。 | 擦 19,664 T → 1,456 T | | 窄桌面填。 两字节直穿 HL 模式存寄存器,四向展免行算相。 | 单列条 14,256 T → 5,712 T | | 追光束 非等清窗。 | 再 16,128 T |

拖帧现止 59,858 T 帧内,拖 50 Hz。

光束调度计扫描线非 T,因一线恰 224 T 顶边恰 64 线,故目标是八位加非除。163 线测 36,576 T 对手数 36,353 T。

文本重写注两猜错。地址例程每字符非每像素行调,跨细胞边界零成本。真成本:232 T 每像素行重测反相标,344 T 八调行步进例程,126 T 重算右一字节址。替换使反相成自改 XOR 操作数每串设一次,走一址穿全串,分八像素行跨单边界。

损坏感知 blit 不跟。 窗竖移每行仍写,因下像素是同窗错偏。损坏仅缩擦。抓时建行签名可跳匀内,此窗值约 9,700 T,但仅大平区窗付。预留非弃。

基于栈 blit 也不跟。 弹八寄存器对推屏约 212 T/行对 LDI 256,但指针簿记需 LD (nn),SP 和重载每行,回约 304 T 对现 327。且需 DI 重引丢失中断险,用欠推技中断开跑成 48 T/行更差。

中断非 50 Hz

48K 帧恰 69,888 T @3.5 MHz,率 50.0801 Hz。每五十中断跳秒增 0.16%,即每日两分十八秒。时钟改五十中断一秒,偶五十一,由每秒累百分之一进则索额外。驱一小时取 180,287 中断对真 180,288,半秒日。

128K 帧 70,908 T @3.5469 MHz,率 50.0211 Hz,加数异。机检已在时钟初跑过。

时钟计的是看门狗建来察丢帧的中断计数器,作自上次差读,故主循环漏帧仍进时——因中断发生无论听否。

无除法的日历

星期从 1980-1-1 周二前数,非 Zeller 或 Sakamoto,因要除 4、100、400 此机无除。年模 7 为 1 或闰 2,故走世纪至多百加,每次重绘一次。

世纪规则未触发,因范围属性:1980-2079 含一世纪年 2000 闰,故范围内闰恰被四除年。全 1,200 月对 Python datetime 验首星期和长。

值得记的 bug

每真发现来自跑码非读码。通用者:

自修 bug。 堆自由例程多一 DEC HL,清用标后写零入块大小高字节。首释应毁堆。未,因合并者遇短闲块,看入恰零载荷,读零为空闲块,逐四字节吸至回真总。数皆合。仅当释块含非零——首块够大容窗像素——走像素堆短 2,619 字节方现。

训在测非分配。释未写块测零填。现写 $A5 于载前释且直断头非可愈总。

携答寄存器非携参寄存器。 此族五 bug。搜例程 AF 存索引跨决匹配比较,POP AF 复比较前标志永不匹。数打器 B 计十被呼两次另数在 B,日历年出 2050。两 LDIR 尾例程返 A 毁 BC 零,产次窗同首和行数零面板。载 DJNZ 前呼 B 作行计例程使环 256 次,已两回。

Z80 标志亦寄存器。值需存呼叫间则存内存或栈。非“小心 B”可学。

仅减 E 的行步进。 blit 自写日如此。LDI 进 DE 十六位,故行起加宽跨 256 时 D 已增再增放次行高一页。系统唯窗在列 8 宽八字节未跨,故次窗移列 16 前未现。任走矩形行须处理借。

反读的数。 计时数十六 T 环至下中断,故慢例程留少圈。一会话误读两回:报 16 T 省为 1 T 损;报 1,216 T 成本为 76 T 省——真回归显为改进未觉。报派生量应报人推理单位。

坏块大小挂机非败。 每堆行走下块为址加头加大小,垃大小卷过 $FFFF 落基下比成堆内,走永环。现无能产垃大小故不加卫,但知其形为冻。

更早同神: 错向 LFSR 周期 71 非 65,535;寄存器毁在耐心游戏错卡;栅时序 bug 真机上半屏指针隐模拟器不;符号位下溢 Y 超 127 窗瞬移屏顶。

工作如何做

每活过同三遍,一坐内。

  1. 研究。 写前找前艺。Spectrum demoscene、ULA 和浮总线文档、esxDOS API 注、Next 寄存器表。半问题 1987 某人解,demoscene 答常快于课本。
  2. 构建。 可测最小物,测法同例程交。
  3. 批判。 受前辩果错。找毁寄存器、屏边界、前会话不真数。

每会话演三角:辩错改的批判者、找 1987 答的研究者、无计时不接受性能无校验不接受正确的测量者。

三规则赢得位:

  • 未完成因屏上看对。 上半屏隐指针屏上看对。
  • 不能直接察者标派生非测。 尤栅行为,故仓每栅派生声称标之。此分终解隐指针。
  • 每验活一提交,受数在提交信。 文数是那些提交的数。

未完

esxDOS 写完通过。 久不能因无 DivMMC 不交未跑码。模拟 DivMMC 驻 esxDOS 于 2026 九解,三死路后。

Next 未成移植。 成自系统自仓,因窗模异:铺砖非重叠,故合成器、存底竞技场、损机多无关。窗模下皆过。未发。

已知隙。 钟被窗遮持旧时直至升,因窗缓从屏抓非组入。应用不能拒关,因无堪否拆向量,需前加是猜。每记事本从应用非文档题。记事本无选、剪、撤、折。鼠存在侦仍启发,无无鼠机不能测。

无已知活 bug。 末随中断安填充闭,后无开。

48K 系统首提交怀移植,赌设备层界持。持于存储、esxDOS、应用模、设置录。不持窗模,故 ZX Desk Next 是二系非移植:28 MHz Z80 带 Layer 2、砖图、硬精灵、DMA 上正桌铺砖,铺砖桌无用于解重叠合成器。

它在自仓将另发。我无 Next 仅有 48K/128K,不盲模拟建,故等至到。

仓库布局

build.sh, run.sh 汇编并加载机器。 tstates.py, taplant.py 计时算术和磁带块植。 src/zxdesk.asm 桌面。 src/damage.inc 损矩形和窄桌面填。 src/saveunder.inc 瞬态面。 src/events.inc 事件环和分。 src/kbd.inc 键盘矩阵和解。 src/hittest.inc 区域表、z 序、命中。 src/menus.inc 菜单栏和下拉。 src/panel.inc 面板和行。 src/dialog.inc 警报和确认。 src/storage.inc 存储层和 RAM 后端。 src/tape.inc 磁带后端经真 ROM 加载。 src/bank.inc 128K 闲 bank 作 RAM 盘。 src/heap.inc 堆和主字节。 src/app.inc 应用描述符和状态换。 src/resize.inc 抓手、轮廓拖、重分。 src/note.inc 记事本。 src/clock.inc 钟。 src/calendar.inc 日历。 src/commander.inc 双栏文件管。 src/desktop.inc 桌面快捷。 src/filemgr.inc 文件面板。 src/settings.inc 设置录。 src/script.inc 脚本输入端到端验。 images/ 上截屏。

许可 MIT,见 LICENSE。无派生他码。工具链、模拟器、ROM 不随分。

评论

社区对借助 AI 生成代码打造的 Z80 汇编图形桌面项目态度分裂,多数人质疑其“手工热爱”叙事与 AI 参与的矛盾及文案虚伪感,少数人则欣赏其怀旧趣味并反感对 AI 参与的盲目排斥。

认为 AI 生成文档与“手工热爱”叙事相矛盾且显得虚伪

  • 项目标榜是充满热爱、严谨验证的手工汇编调优,却用 AI 撰写说明文档,且实际提交记录与自称的“每次验证都提交”规则不符,被指自相矛盾(较多人)
  • AI 写作中那种将每次微调都视为伟大洞见的真诚语气,把本是瞎玩的项目包装成开创性研究,让人感到虚伪,文案应反映“只是烧 token 玩玩”的精神 > 这种用 AI 生成的项目需要 AI 表现出“只是随便折腾”的态度,而不是假装在进行突破性研究(@jameshart)

认为不应因 AI 参与否定项目,用 AI 探索旧硬件带来趣味与怀旧

  • 不少人反感讨论过度聚焦于“是否由 AI 生成”而忽略项目本身的乐趣,认为在老机器上用 AI 生成汇编只是图一乐,能唤起童年回忆并满足技术好奇(较多人) > 看到 ZX Spectrum 这种童年电脑被这样折腾让我心里充满喜悦和怀旧,谢谢分享(@alt227)

认为汇编写桌面系统过于繁重,或好奇真正手写的先例

  • 有读者觉得在 8 位机上纯汇编写图形桌面属于过度工程(少数人)
  • 有读者借机提问有无配套教科书的好文档类似项目,反映出对扎实工程参考的需求(少数人)

有价值的补充

  • 历史上 Z80 机器上的 SymbOS 操作系统与图形桌面完全由人工编写,2006 年首发;而 C64 的 GEOS 桌面早在 1986 年就已作为商业产品发布,可作为本项目“纯手工”性质的参照(@unnah)
  • 有用户回忆 12 岁便在 ZX Spectrum 上模仿 Amiga Workbench 界面,虽不懂拖拽等概念,但印证了在老旧硬件上折腾图形界面的民间传统由来已久(@vilaca)