HN 日本語サマリー

← 一覧へ戻る
プログラミング

ZX Spectrumのためのグラフィカルデスクトップ

A graphical desktop for the ZX Spectrum (github.com)

119 pointsby graemep89 コメント

要約

Z80アセンブリで書かれたZX Spectrum 48K用のグラフィカルデスクトップ環境「ZX Desk」が開発されました。このプロジェクトは、1980年代に高価で手に入れられなかったAtari STのGEMデスクトップに触発されており、オーバーラッピングウィンドウ、プルダウンメニュー、イベントキュー、ストレージレイヤーなどを48KBのメモリ内に収めることに成功しています。

全文翻訳

ZX Spectrum Next

ZX Desk

ZX Spectrum 48K 用のグラフィカルデスクトップ。Z80アセンブリで記述されています。

Zオーダーとフォーカスを持つオーバーラッピングウィンドウ、プルダウンメニュー、ヒープ、イベントキュー、スワップ可能なバックエンドを持つストレージレイヤー、ダイアログ、コントロール、メモ帳、時計、カレンダー、2ペインのファイルマネージャー、そして実際に設定を変更できる設定パネル。これらすべてが1982年のマシンで48KBに収まり、1フレーム(69,888 Tステート)以内にウィンドウをドラッグできます。

エミュレーターだけでなく、実機でも動作します。

[画像: デスクトップ、コマンダー、時計、ロードされたアプリが表示されている様子]


なぜ作ったか

80年代、私はアタリSTが欲しかったのですが、買えませんでした。本当に欲しかったのはGEMでした。デスクトップ、ウィンドウ、常にあるメニューバー、プロンプトではなく「場所」であるというマシンの感覚。代わりにスペクトラムを持っていましたが、それにどれだけ近づけるかを長い間考えていました。その一部を書き始めましたが、完成しませんでした。

だから、これはその完成版です。GEMのポートではなく、そうであると主張するものでもありません。3.5MHzのZ80、48KBのRAM、アトリビュートクラッシュのある1ビットディスプレイ、そして描画中にCPUからサイクルを盗むビデオチップに押し付けたときに、そのアイデアがどうなるかということです。多くの答えは質問よりも興味深いものになり、ほとんどすべてが推論ではなく、マシンを測定することから得られました。

これは楽しいプロジェクトであり、愛情のこもった労働の成果です。そして、これほど長く書かれている理由は、測定値が有用な部分だからです。このハードウェアで何かを構築しているなら、以下の数値は私に多くの夜を費やさせました。それらはあなたのものです。


今日の機能

ウィンドウオーバーラップ、Zオーダー、移動可能、リサイズ可能、タイトルバー、閉じるボタン、グリップ付き。フォーカスはZオーダーの最前面なので、前面に出すこととフォーカスすることは一つのアクションです。すべてのウィンドウは閉じられ、最後のものも閉じられます。デスクトップのショートカットで再度開くことができます。
メニュープルダウン、セーブアンダー、ヒットテストを備えた常設メニューバー。
入力Kempstonマウス、Kempstonジョイスティック、および3つのテーブルにデコードされたフルキーボードマトリックス(リピート付き)。すべてイベントとして到着します。
イベント16スロットのリング。メインループにはウィンドウ固有のコードは含まれていません。
ストレージ6つのベクトルでバックエンドを登録。RAM、テープ(実際のROMローダー経由)、および128KBのスペアバンクをRAMディスクとして使用。コマンダーはテープからファイルをいずれかにインポートします。esxDOSには予約済みのIDがあります。
メモリオーナーバイト付きの実際のヒープ、8,111バイト。ウィンドウサイズに合わせてウィンドウバッファを割り当てます。
アプリケーションinit、event、paintのディスクリプタ、およびインスタンスごとの状態をスワップイン/アウトします。メモ帳、時計、カレンダー、コマンダー、アバウト。
アプリケーションインターフェース$8000のジャンプテーブル、45スロット。スロットnは$8000 + 3nにあります。時計、カレンダー、メモ帳、コマンダーはすべてこれを介して動作し、デスクトップのグローバル変数を参照しません。
ロード可能なアプリアプリはファイルです。ヘッダー、オリジンゼロでアセンブルされたイメージ、およびリロケーションテーブル。デスクトップはそれをRAM、128KBバンク、またはテープからヒープにロードし、ウィンドウで実行します。一度に3つまで。時間を保持するアプリにはティックがあります。アプリの書き方を参照してください。
永続性設定はマジックバイトとバージョンとともにストレージに書き込まれ、起動時に読み戻されます。
壁紙128KBでは、デスクトップは画像にすることができます。スペアバンクに保持され、各ウィンドウ移動後にコピーされる16文字行の帯です。設定パネルのDESKTOPはPLAIN、DOTS、IMAGEを循環させます。mkwall.pyは任意の画像を帯に変換します。

スクリーンショット:

[画像: 2つのウィンドウ][画像: メモ帳]
2つのウィンドウ、Zオーダーメモ帳、シンクレアスタイルのシフトレポートカーソル付き
[画像: コマンダー][画像: 時計とカレンダー]
2ペインのコマンダー、ディレクトリではなくデバイスを表示時計とカレンダー
[画像: 3つのロードされたアプリ][画像: FILEメニュー]
ファイルからロードされた3つのサンプルアプリFILEメニュー、アプリファイル用のLOAD付き

実行方法

ツールチェーンはローカルで小さく、pasmo 0.5.5 をソースから tools/ にビルドします。

./build.sh                  src/zxdesk.asm をアセンブルして build/zxdesk.tap をビルド
./run.sh                    ビルドしてマシンにロード
MACHINE=128 ./run.sh        128KBで同じことを行い、設定を元に戻します

esxDOS もここで動作します。エミュレートされた DivMMC と 64MB のカードイメージを使用します。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 でウィンドウをドラッグするには、マウスボタンではなくスペースバーが必要です。Fuse for macOS, 1.9.2 は、ボタンが押されている間 Kempston マウスの移動を停止するため、ドラッグが開始された瞬間にポインターがフリーズし、ウィンドウは決して追従しません。タイトルバーを指し、SPACE を押したまま移動し、離します。マウスは他のすべての操作で問題なく、ボタン自体も正しく登録されます。停止するのは移動だけです。

これはデスクトップではなくエミュレーターであり、MOUSETEST=1 は私がそれを証明した方法です。これはマウスポートを一度ずつ読み取り、ポートと画面の間に何も挟まず、ボタンが下がっている間カウンターは静止したままです。ReadInput の RiBtn は SPACE を機能させるもので、マウスのないマシンでもデスクトップが使用できるようにするためです。実際の Kempston マウスは正常にドラッグします。

SCRIPT=1 は知っておく価値のあるものです。これはマウスの代わりに合成入力イベントのリストからデスクトップを駆動するため、インタラクション(メニューを開く、項目を選択する、ウィンドウを別のウィンドウの上にドラッグする、フィールドに入力する、保存する)は毎回同じように実行され、監視するのではなく前回の実行と比較できます。


構築方法

ボトムアップで。

デバイスレイヤー。 DevFillRect, DevFillDesk, DeskFillCol, AddrAt, BlitRect, RectGrab。これらすべてはバイト列とピクセル行で動作し、画面の3番目のレイアウトやインターリーブレイアウトに直接触れることはありません。これはポートがスワップアウトする境界であり、その理由のために初日に描画しました。

フレーム規律。 メインループは割り込みで停止し、トップボーダーですべてのポインター作業を行い、ウィンドウが移動した場合はビームを待ち、再描画してから、フレームの最後に読み取りとディスパッチを行います。入力は最後に行われるため、ビーム待機前のコストは一定になり、スケジューラが正確になります。

イベントキュー。 4バイトイベントの16スロットリング。EvPoll は生の入力をポインター移動、ボタンプレス、キーに変換します。EvDispatch はハンドラテーブルを通じてそれをドレインします。

ヒットテスト。 コントロールごとに5バイトの行、Zオーダーで前から後ろへ、$FF で終了。検索前にモデルから再充填されるため、ウィンドウ位置の2番目のコピーが古くなることはありません。閉じられたメニューの高さはゼロなので、HitTest には特別なケースはありません。

一時的なサーフェス。 4つの深さのアリーナ、各サーフェスは最大16x96で、プッシュおよびポップされます。スタックが1つではなく2つあるのは、メニューの下のピクセルはパネルレコードをまったく持たないものによってプッシュされるためです。

ウィンドウ。 パネルレコードと同じトリックで、ライブコピーにスワップされるレコード。ウィンドウ位置は6つのファイルにわたって93回参照されるためです。Zオーダーはペイントオーダーの逆でもあり、ヒットテストオーダーでもあります。

ストレージ。 バックエンドのレジストリ、それぞれ14バイトのID、機能ビット、6つのエントリポイント。6つの操作は手作業で配置されたJP命令で、オペランドは選択時にパッチされるため、ディスパッチコストは10 Tステートで、レジスタを破壊しません。

コントロール。 パネルは行のスタックであり、各コントロールは行です。そのため、行のインデックスは検索ではなく3つのシフトです。4つのタイプがあり、有用なのはサイクルです。チェックボックスは制限が2のサイクルであり、ラジオグループは制限がNのサイクルです。設定パネル、ファイルリスト、保存ボックスはすべて同じコードで、異なるテーブルを使用します。

メモリマップ
$6000-$7EC4   遅い領域: ストレージ、パネル、カレンダー、
              ファイルパネル、デスクトップセットアップ、コマンダー
$7EC5-$7FFF   空き、315バイト、競合あり
$8000-$B6E3   速い領域: その他すべて、アプリケーションインターフェースのジャンプテーブルから始まる
$B6E4-$BCFF   空き、1,564バイト
$BD00         スタックトップ
$BDBD         割り込みハンドラ
$BE00-$BEFF   割り込みベクタテーブル
$C000-$C63F   RAMディスク、そのディレクトリ、およびテープバッファ
$C640-$C750   ライブメモ帳状態
$C751-$DF50   4つの一時的なサーフェス
$DF51-$FEFF   ヒープ、8,111バイト: ウィンドウバッファ、ウィンドウ
              状態、およびロードされたアプリ
$FF00-$FFFF   意図的に未使用

マップ内の2つの点は説明する価値があります。

遅い領域が存在するのは、ULAが画面描画中に$8000より下のサイクルを盗むため、そこにあるコードは約3分の1遅く実行されるからです。そこでのルールは1行です。フレーム内で実行されるものはありません。ドラッグパス、ポインターパス、または割り込みには何もありません。タイミングは移動全体でTステートまで変更されませんでした。システム変数の上ではなく$6000から始まるのは、テープローダーがIYを介してシステム変数領域をインデックス付けし、BASICローダー自体がそのすぐ上にあり、ロード中のコードを上書きするCODEブロックは失敗の斬新な方法になるからです。

ヒープはメモリのトップから1ページ手前で停止し、$FFFFではなく停止します。すべてのウォークは、次のブロックをアドレス+ヘッダー+サイズとして計算します。$10000で終わるブロックはゼロにラップしてヒープのベースより下にあると比較されるためです。$FF00で停止すると8,367バイトのうち256バイトが失われますが、そのクラスの失敗はすべて削除されます。


測定されたもの

これは、私が他人のリポジトリを読んでいたら欲しい部分です。以下のすべての数値は、コードを実行し、マシンのクロックに対してタイミングを取ることで測定されました。これらは命令数を数えたものではなく、導出された主張はそうであると明記されています。

[画像: マシン上でのタイミング]

信頼できる時計

48Kの割り込み周期は正確に69,888 Tステートであり、プログラムがそれを動かすことはできません。そのため、マシン上で唯一使用可能な時計です。したがって、タイミング実行はHALTで同期し、オプションで既知のTステート数だけ遅延させてルーチンをフレームの選択されたポイントに配置し、それを呼び出し、次に割り込みが発生するまで16 Tループのターンを数えます。空のキャリブレーション実行と比較することで、すべての固定オーバーヘッドがキャンセルされます。

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

118 Tは、命令ごとに数えられた復帰割り込みパスの正確なコストです。ハンドラ、その変数、カウントループ、スタックはすべて$8000の上にあり、ULAはサイクルを盗まないため、これは推定ではなく正確です。タイミングを取られているルーチンのみが競合メモリに触れます。

既知の長さの3つの遅延に対してブラインドで測定:

公称測定値エラー
2,599 T2,592 T−7 T
51,999 T52,000 T+1 T
103,999 T103,994 T−5 T

最悪のエラーは104,000 T中の7 T、つまり0.007%です。3番目の測定はフレーム境界を越えます。これが118 Tの数値を裏付けています。

競合コストは14.7%、50%ではない

プロジェクトのすべての予算は、伝承から取られた悲観的な50%の画面書き込みペナルティから始まりました。フレーム全体に1,024バイトのフィルをスイープ:

開始コスト
トップボーダー11,744 T
10,399 T12,913 T
20,799 T13,441 T
31,199 T13,473 T
41,599 T13,441 T
51,999 T12,369 T
ボトムボーダー11,745 T

2つのボーダーの数値は1 Tの差で一致します。これは非競合コストです。ディスプレイ内の最悪ケースは13,473 Tです。これは14.7%、つまり競合バイト書き込みあたり約1.7 Tです。50%の数値に基づいた予算は、約3分の1保守的すぎました。私のものもそうでした。

ウィンドウ描画コストの半分は3つの短い文字列

WinDrawを4つのフェーズに分割し、それぞれを個別にタイミング測定しました。フェーズの合計は全体より330 T以内です。これは4回の呼び出しと戻りのペアに、16 Tのカウント粒度を加えたものです。

フェーズフレームのトップディスプレイの中間割合
テキスト35,872 T35,969 T50%
エッジ19,552 T19,841 T27%
フィル14,688 T16,641 T21%
閉じるボックス832 T849 T1%
全体71,274 T73,099 T

タイトルと本文のテキストは27文字で、それぞれ約1,330 Tです。問題だと思っていたフィルは、その5分の1です。フィルを最適化しても、ドラッグフレームの数パーセントしか得られず、1週間費やすことになったでしょう。

間違ったものを測定したベンチマーク

以前のメモには、セルアウェアプッシュフィルがバイトあたり7.4 Tであると記録されていました。実際のルーチンはバイトあたり11.5 T、つまり54%多くかかります。ベンチマークはテクニックを測定していましたが、ルーチンはベンチマークが支払わなかった行ごとのアドレス演算を処理します。1行あたり183 Tのコストのうち、88 Tは実際にピクセルを書き込むプッシュチェーン、95 Tはレジスタ交換、アドレスステップ、セル境界テスト、およびループです。

マシン上で最も速いフィルのコストの半分以上は、ピクセルを書き込むことではありません。これは一般化できます。このプロセッサでは、バイトあたりのスループットではなく、行あたりのオーバーヘッドが攻撃すべき対象です。

割り込みは失われるのであり、延期されるのではない

これは、このハードウェアで他の人に最も知ってほしいことです。なぜなら、それは原因以外の何にでも見えるような誤動作を引き起こすからです。

フィルは実行中ずっとDIを保持していました。なぜなら、SPは画面メモリを歩き回り、スタックではなくなるからです。受け入れられている考えは、これが割り込みを遅延させるということです。それは違います。スペクトラムはINTを32 Tステートの間だけアサートし、その後撤回します。したがって、それらの32 TをカバーするDIウィンドウは、割り込みを延期するのではなく破壊します。

理解される前に観測されました。フレームの62,399 Tに配置されたフィルは、フレーム境界を明らかに越えたにもかかわらず、フレーム境界を越えなかったと報告しました。それを失われた割り込みとして再採点すると、トップボーダーでの同じフィルに対して11,745 T対11,744 Tとなり、1 Tの差で診断が確認されました。

その後、適切に測定されました。8x24フィルの各命令の後に割り込みを注入すると、1つのフィルルーチンで500の命令境界のうち458、もう一方のフィルルーチンで752のうち710で割り込みが拒否されました。

明白な修正は機能しません。 行の間に割り込みを再有効化することは正しく聞こえますが、失敗します。なぜなら、割り込みは保留中ではなく、消滅しているからです。数T幅のEIウィンドウは、236 Tごとに1回、約30行に1回割り込みをキャッチします。DI領域を短くすることは、それをなくすことと同じではありません。なくすことだけが修正です。

機能するのは、最後のプッシュを負うことです。 フィルは現在、割り込みを有効にしたまま実行されます。DIが保護していたのはSPでした。したがって、修正は、リターンアドレスの2バイトが常に、すでに上書きされようとしている場所に配置されることを保証することです。SPは2種類の値を取ります。矩形内では、プッシュはチェーンの次のプッシュが書き込むものとまったく同じものを書き込みます。そして、行の最後のプッシュ後の低点であり、チェーンは決して戻りません。したがって、チェーンは行よりも1プッシュ短くされ、左端の2バイトは負われます。つまり、SPが次の行に移動し、それらに到達できなくなった後に、1回のイテレーションで支払われます。

その後: 607回の命令境界のうち0回、904回の命令境界のうち0回が割り込みを破壊し、矩形はバイトごとに同一で、その外側は何も触れられず、画面チェックサムは変更前後の両方で変更されません。

前後
16×96矩形フィル17,414 T22,068 T+48 T/行
16×96デスクトップフィル25,351 T30,125 T+50 T/行

両方のフィルは奇数幅にも対応できるようになりました。以前は3列目がペイントされないままでした。偶数幅は、1回の未実行ジャンプを支払います(呼び出しあたり7 T)。この数値は導出されたものであり、測定されたものではありません。ジャンプは$8000の上にあり、画面に触れる前に実行されます。このセクションと競合スイープのフィルタイミングは変更前に取得されたため、呼び出しあたり7 T低くなっています。

これは実際のコストであり、それだけの価値があります。ドラッグパスは主に列フィルであり、HLを介して書き込み、SPには決して触れず、リスクにさらされることはありませんでした。

キャッチするために構築されたものの、それ自体にキャッチされたウォッチドッグ

フレームウォッチドッグは、ハンドラ内の割り込みとメインループ内のフレームをカウントします。その差はドロップしたフレーム数です。ハンドラは厄介な半分です。なぜなら、スタックがスタックではないフィル内で発生するからです。

最初のバージョンはIXを借用し、INC (IX+0)でカウントしていましたが、割り込みスイープは最初の実行で失敗しました。その命令はフラグを設定し、フィルはアドレスステップの間にライブキャリーを持ち、行の終わりと境界を越えたかどうかを決定する分岐を見つけます。そのギャップでの割り込みはキャリーを盗み、行は間違ったアドレスにステップしました。

INC IXは16ビットインクリメントであり、16ビットインクリメントはフラグを変更しません。したがって、カウンターはワードであり、ハンドラは透過的であり、フラグやレジスタを変更せずに50回/秒で104 Tかかります。ロードマップは、ウォッチドッグが割り込みバグを早期に検出すると予測していました。実際に起こったことは、割り込みテストがウォッチドッグを検出したことです。

ハンドラにプッシュさせることも行き止まりです。 AFをプッシュするハンドラは、SPより下4バイトを使用します(2バイトではなく)。したがって、負われた領域は2倍になり、各行はさらに22 Tを支払います。

1フレーム内にドラッグを取得する

開始位置は絶望的でした。ウィンドウの完全な消去と再描画は96,010 Tかかり、フレームは69,888 Tでした。これはフレームの137%であり、ドラッグが25 Hzで実行された理由です。4つの変更を順に、それぞれ測定しました。

変更効果
オフスクリーンバッファ。 一度バッファにコンポーズし、フレームごとにブリット。高コストな作業はコンポーズであり、転送ではありません。再描画 71,274 T → 26,048 T
高速テキスト。 ピクセルテキストレンダラーを書き直しました。文字あたり1,307 T → 622 T、2.10倍
ダメージ矩形。 ウィンドウが空にしたストリップのみを消去し、ウィンドウ全体ではありません。消去 19,664 T → 1,456 T
狭いデスクトップフィル。 HLを直接2バイトで、レジスタに保持されたパターンを使用。アンロールして4方向にしたため、行ごとにフェーズを計算する必要がなくなりました。単一列ストリップ 14,256 T → 5,712 T
ビームを追いかけるのではなく、ウィンドウ全体がクリアされるのを待つ。さらに16,128 T

ドラッグフレームは現在、フレーム内の59,858 Tで終了し、ドラッグは50 Hzで実行されます。

ビームスケジューラは、Tステートではなくスキャンラインをカウントします。なぜなら、1ラインは正確に224 Tであり、トップボーダーは正確に64ラインなので、ターゲットは除算ではなく8ビット加算だからです。手動でカウントされた36,353 Tに対して、163ラインで36,576 Tを測定しました。

テキストの書き直しは、私の両方の推測が間違っていたことを証明したため、注目に値します。アドレスルーチンはピクセル行ごとではなく、文字ごとに1回呼び出され、セル境界を越えるのにコストはかかりませんでした。実際のコストは、反転フラグを各ピクセル行で再テストするのに232 T、行ステップルーチンへの8回の呼び出しに344 T、前のバイトから1バイト右にあるアドレスを再計算するのに126 Tでした。置き換えは、反転を文字列ごとに1回設定された自己変更XORオペランドにし、文字列全体にわたって1つのアドレスを歩き、8ピクセル行を、横断できる単一のセル境界の両側に分割します。

ダメージ認識ブリッティングは従いません。 ウィンドウが垂直に移動すると、ピクセルが下のオフセットで同じウィンドウであるため、すべての行を書き込む必要があります。ダメージは消去のみを縮小します。グラブ時に構築された行ごとのシグネチャは、均一な内部をスキップすることを可能にし、このウィンドウで約9,700 Tの価値がありますが、大きな平坦な領域を持つウィンドウにのみ役立ちます。拒否されたのではなく、保留中です。

スタックベースのブリットも従いません。 バッファから8つのレジスタペアをポップして画面にプッシュするのは、LDIの256 Tに対して約212 Tですが、ポインターブックキーピングには行ごとにLD (nn),SPとリロードが必要で、これは現在の327 Tに対して約304 Tに戻ります。また、DIが必要で、失われた割り込みハザードを再導入し、負われたプッシュ技術を介して割り込みをオンにして実行すると、行あたり48 Tになり、計算が悪化します。

割り込みは50 Hzではない

48Kフレームは3.5 MHzで正確に69,888 Tなので、レートは50.0801 Hzです。1秒を50回の割り込みごとにカウントすると、0.16%増加し、1日あたり2分18秒になります。代わりに時計は50回の割り込みで1秒を作り、時々51回にします。これは、毎秒100分の1を蓄積し、キャリーが発生したときに余分な割り込みを要求することで決定されます。1時間駆動すると、実際の180,288回に対して180,287回の割り込みがかかり、1日あたり半秒です。

128KBでは、フレームは3.5469 MHzで70,908 Tなので、レートは50.0211 Hzであり、加算子は異なります。時計が初期化されるまでに、マシン検出はすでに実行されています。

時計がカウントしているのは、ウォッチドッグがドロップしたフレームを検出するために構築した割り込みカウンターです。最後の確認からの差として読み取られるため、メインループがスキップしたフレームでも時間が進みます。なぜなら、何かを聞いていたかどうかに関わらず、割り込みは発生したからです。

除算のないカレンダー

曜日を1980年1月1日(火曜日)から順にカウントします。ZellerやSakamotoの方法ではなく、これらはすべて4、100、400で除算する必要があり、このマシンには除算命令がありません。1年は7で1モジュロ、または閏年は2なので、1世紀を歩くのは最大でも100回の加算であり、これは再描画ごとに発生します。

世紀ルールは決して発火しません。これは範囲のプロパティであり、コーナーカットではありません。1980年から2079年までには1つの世紀年が含まれ、2000年は閏年です。したがって、その範囲内では、閏年は正確に4で割り切れる年です。表示できるすべての1,200ヶ月は、Pythonのdatetimeと比較して、最初の曜日と月の長さの両方を確認しています。


書き留める価値のあるバグ

このプロジェクトでのすべての実際の発見は、コードを読むことからではなく、コードを実行することから得られました。これらは一般化できるものです。

目の前で自己修復したバグ。 ヒープのフリールーチンにはDEC HLが1つ多すぎたため、使用済みフラグをクリアしてからブロックサイズの高バイトにゼロを書き込みました。最初のフリーでヒープは破損したはずです。そうならなかったのは、結合処理が短いフリーブロックを見つけ、それを越えてペイロード(偶然すべてゼロだった)を見て、それらのゼロを空のフリーブロックとして読み取り、正確な合計に戻るまで4バイトずつ吸収したからです。すべての数値が一致しました。解放されたブロックがゼロ以外のもの(ウィンドウピクセルを保持できた最初のブロック)を持っていたときにのみ表面化し、ウォークはピクセルに入り、ヒープは2,619バイト不足して戻ってきました。

教訓は、アロケータではなくテストに関するものです。書き込みを行わなかったブロックを解放するヒープテストは、ゼロフィルをテストしています。現在、解放する前にペイロードの上に$A5を書き込み、合計ではなくヘッダーでアサートします。合計は回復できます。

答えを運ぶレジスタは、引数を運ぶレジスタであってはならない。 このファミリーは5つの別々のバグを占めていました。検索ルーチンは、何かを見つけたかどうかを決定する比較全体でインデックスをAFに保持していたため、POP AFは比較前のフラグを復元し、一致しませんでした。数字プリンターはBを10の位のカウントに使用し、Bに別の数字が入った状態で2回呼び出されたため、カレンダーの年は2050になりました。LDIRで終わる2つのルーチンは、Aが破壊されBCがゼロの状態で返され、最初のウィンドウの正確なコピーである2番目のウィンドウと、行数がゼロのパネルが生成されました。DJNZカウンターをロードしてからBを行カウンターとして使用するルーチンを呼び出すと、ループは256回実行されます。これは現在2回発生しています。

Z80では、フラグもレジスタです。値が呼び出しを生き残る必要がある場合、それはメモリまたはスタックに格納されます。そのルールは「Bに注意」として学習できるものではありません。

Eからのみ減算する行ステップ。 ブリットは、書かれた日からこれを行っていました。LDIはDEを16ビット進めます。そのため、矩形の行開始に幅を加えたものが256を超えると、Dはすでにインクリメントされており、再度インクリメントすると次の行が1ページ高くなります。システム内の唯一のウィンドウは列8にあり、クロスするのに8バイト不足していたため、2番目のウィンドウが列16に移動するまで表示されませんでした。行ごとに矩形を歩くものはすべて、借り(borrow)を処理する必要があります。

逆さまに読む数字。 タイミングは、次の割り込みまで16 Tループをカウントすることで取得されるため、遅いルーチンは少ないターンを残します。これは1回のセッションで2回誤読されました。1回は16 Tの節約を1 Tの損失として報告し、もう1回は1,216 Tのコストを76 Tの節約として報告しました。これは、実際の退行が改善として表示されたために見過ごされたことを意味します。導出された数量を報告するものは、人々が推論する単位で報告する必要があります。

破損したブロックサイズは、失敗するのではなくマシンをフリーズさせる。 すべてのヒープウォークは、次のブロックをアドレス+ヘッダー+サイズとして計算します。ガベージサイズは$FFFFをラップしてベースの下に到達し、ヒープ内にあると見なされるため、ウォークは無限ループになります。現在、何もガベージサイズを生成できないため、ガードは追加されませんでしたが、それは次の失敗の形状であり、フリーズとして提示されることを知っておく価値があります。

以前、そして同じ精神で: LFSRが間違った方向にシフトしたため、周期が65,535ではなく71になりました。レジスタの破壊により、このプロジェクトの検証場であったペーシェンスゲームで間違ったカードが表示されました。ラスタータイミングのバグにより、実機ではディスプレイの上半分でポインターが見えなくなりましたが、エミュレーターでは見えました。そして、符号ビットのアンダーフローにより、Yが127を超えた瞬間にウィンドウが画面の上部にテレポートしました。


作業方法

すべての作業は、同じ3つのパスを1回のセッションで通過しました。

  1. 調査。 何かを書く前に先行技術を見つけます。Spectrumデモシーン、ULAとフローティングバスのドキュメント、esxDOS APIノート、Nextレジスタリスト。これらの問題の半分は1987年に誰かが解決しており、デモシーンの答えは通常、教科書のものよりも高速です。
  2. ビルド。 測定できる最小のもの、および測定方法をルーチンと一緒にコミットします。
  3. 批評。 結果を受け入れる前に、それに対して議論します。破壊されたレジスタ、画面端の境界ケース、および前のセッションから現在では通用しない数値を検索します。

セッションごとに3つの役割を演じようとしました。変更が間違っていると主張する批評家、1987年の答えを探す研究者、タイミングなしでパフォーマンスの主張を受け入れない測定者、または画面チェックサムなしで正当性を主張しない測定者です。

その価値があった3つのルール:

  • 画面上で正しく見えるからといって完了ではありません。 ディスプレイの上半分で見えなくなったポインターは、画面上では正しく見えました。
  • 直接観察できなかったものはすべて、測定済みではなく導出済みとラベル付けされます。 特にラスター動作。このリポジトリのすべてのラスター導出された主張がそう述べている理由です。その区別が、最終的に見えないポインターを説明したものです。
  • 検証済みの作業片ごとに1つのコミット。コミットメッセージに受け入れられた数値を記載します。 このドキュメントの数値は、それらのコミットからのものです。

アプリのロード

アプリはファイルであり、デスクトップはファイルが読める場所ならどこからでも実行できます。RAMデバイス、128KBのバンクデバイス、またはテープです。3つの方法があります。

コマンダーから。 FILE、OPEN、またはFILESアイコンを開くと、2ペインのコマンダーが開きます。ファイルに移動してENTERを押します。アプリであれば独自のウィンドウで実行されます。それ以外の場合は、常にそうであったようにメモ帳で開かれます。デスクトップは、ファイルの最初の3バイトでこれらを区別するため、拡張子を間違えることはありません。

名前で。 FILE、LOADは名前と検索場所を尋ねます。FROMは現在のデバイスのDISKまたはTAPEです。LOADはそれを実行し、EXITはパネルを閉じます。テープに何が入っているかを知っている場合は、これが迅速な方法です。

テープのインポート。 テープにはディレクトリがありません。テープに何が入っているかを知る唯一の方法は、再生することです。そのため、コマンダーはテープをリストせず、テープから読み取ります。テープペインが表示されている状態で:

キー
Rテープの次のファイルを反対側のペインのデバイスに読み込みます。
Aすべてのファイルを次々と読み込みます。

その後、それらは反対側のペインの通常のファイルになり、ENTERで実行されます。Aは、ループするテープが一度聞かれたことを知る方法である、最初の名前が再び現れたときに停止します。単に終了するテープでは、SPACEを押すと停止します。

テープについて知っておくと良い3つのことがあります。

テープは順方向にしか再生しません。すでに通過したファイルのリストを名前で要求すると、来ないものを待つことになり、これはベアスペクトラムのLOAD ""と同じです。テープの読み取り中は、ステータス行にREADING TAPE. SPACE TO STOP.と表示され、SPACEを押すと停止してデスクトップに戻ります。

名前付きロードは、途中で他のファイルを通過します。テープの3番目のファイルを要求すると、最初の2つを読み飛ばし、最大約30ブロックを読み飛ばしてから諦めます。

最後に読み取られたファイルはテープバッファに残っており、テープペインにリストされます。そこでのENTERは、テープに触れることなくバッファから直接実行します。これは48Kでは重要です。RAMデバイスのファイルは256バイトです。それより大きいアプリはインポートできず、Aはそれを全体として拒否してそのように言いますが、バッファにはまだあり、テープペインから実行できます。

何がどこに収まるか。

ファイルサイズ制限ファイル
RAMデバイス、48K256バイト4
バンクデバイス、128K8,192バイト8
テープ511バイトテープが保持する数

RAMとバンクデバイスはどちらもメモリなので、インポートされたものはマシンがオフになると失われます。テープは持続するコピーです。

同時に実行できるもの。 最大3つのロードされたアプリ、それぞれ独自のウィンドウで、組み込みのアプリに加えてデスクトップの最大4つのウィンドウまで。同じファイルを2回ロードすると、状態が分離された2つのコピーが作成されます。アプリのウィンドウを閉じても、メモリはすぐに解放されません。次のロードで解放されます。

ロードできない場合。 これらはそれぞれアラートを表示し、メモリを元の状態のままにします。

アラート意味
COULD NOT LOADその名前のファイルがないか、アプリではありません。
NEEDS NEW DESKアプリはこのデスクトップが持っているよりも新しいインターフェース用にビルドされました。
BAD APP FILEファイルが途中でカットされているか、リロケーションテーブルが外側を指しています。
NO ROOMヒープが不足しているか、ウィンドウが空いていません。
CLOSE AN APP3つがロードされており、すべて画面上にあります。
DOES NOT FITインポートがデバイスに大きすぎました。名前は2行目にあります。

アプリをテープに入れる。 mkapp.pyは、通常のヘッダーとデータブロックとしてテープイメージに1つを追記します。これはSAVE "name" CODEが書き込むものと同じなので、エミュレーターでも実カセットでも動作します。

./mkapp.py examples/counter/counter.asm build/counter.zxa \
    --tap build/zxdesk.tap COUNTER

デスクトップの後にアプリをテープに入れます。デスクトップが最初にロードされ、テープを停止した場所のままにして、アプリが次に続きます。


アプリの書き方

アプリは、1つのインクルードに対して個別にアセンブルされ、実行時にデスクトップによってロードされます。アプリ内の何も、ジャンプテーブルを除いてデスクトップ内のアドレスを知りません。そのため、テーブルは追加専用です。

./build.sh && ./mkapi.py
./mkapp.py examples/counter/counter.asm build/counter.zxa
./mkapp.py examples/counter/counter.asm build/counter.zxa \
    --tap build/zxdesk.tap COUNTER

最初の行はビルドからapi/zxdesk.incを書き込みます。45スロット、キーコード、ストレージ定数、ディスクリプタオフセットです。2番目の行はサンプルをアセンブルします。3番目の行はテープイメージにも追加します。デスクトップの後ろに追加するため、FILE、LOADでFROMをTAPEに設定すると見つかります。ユーザーがそれを実行する方法は、アプリのロードにあります。

3つの例があり、それぞれが動作するアプリ全体です。

examples/counter126 バイト描画し、キーを受け取り、1バイトの状態を保持します。
examples/stopwatch163 バイトティックを使用して秒をカウントします。
examples/tally223 バイトストレージレイヤーを介して保存およびロードし、クリア前に確認します。

形状は次のとおりです。

            include "zxdesk.inc"
            org     APPORG

            defw    1               ; 状態バイト数(ウィンドウごと)
            defw    Count           ; 使用中の場所
            defw    Init, Draw, Key ; これらはすべてゼロでもよい
            defw    Title
            defb    16, 24, 12, 36  ; 列、行、幅、高さ
            defw    0, 0, 0         ; 閉じる、スクロール、スクロール先
            defw    Tick            ; またはゼロ

ディスクリプタは最初に必要です。ローダーはイメージの最初の24バイトをコピーしてアプリと呼びます。Drawはウィンドウ内でApiWinPrintを使用して描画します。Bはタイトルバーの下のピクセル行、Cはウィンドウの左境界からの列なので、アプリはウィンドウの位置を尋ねることはありません。KeyはAにキーを受け取ります。ウィンドウが表示しているものが変更された場合、1回の呼び出しでDrawを介して再描画します。

            jp      ApiWinRefresh

ティック。 Tickは、ウィンドウが前面にあり、何もドラッグされていない場合に1フレームあたり1回呼び出されます。Aには、前回の呼び出しから経過した秒数が保持されます。ほとんどのフレームではゼロです。秒数は、割り込みが50 Hzでないことを補正した時計自体の秒数です。フレーム内で実行されるため、何もない場合はすぐに戻り、変更があった場合にのみ再描画する必要があります。メニューまたはパネルが表示されている間は遅延され、秒数は次の呼び出しのために保持されます。

ダイアログは待機しません。 ApiDlgConfirmはすぐに戻り、後でBCのルーチンを呼び出します。A=1ははいを意味します。examples/tallyがそれを示しています。

ファイル。 8バイトのヘッダー:ZXA、アプリが必要とするインターフェースバージョン、イメージ長、リロケーションカウント。次にイメージ、次にリロケーションごとの1つのワード(それぞれアドレスを含むイメージ内のワードのオフセット)。mkapp.pyは2つのオリジンでアセンブルして比較することでそれらを見つけ、次に3番目のオリジンでアセンブルしてリストが完了したことを証明します。これにより、リロケートできない唯一のもの(半分のアドレスを取る命令)もキャッチされます。バージョン3を要求するファイルは、22バイトのディスクリプタ1.1.0を使用し、ティックなしで、それでもロードされます。

どこに配置されるか。 ヒープブロック内で、ロード時にリロケートされます。48Kにはアプリを約束するスペアのアドレス範囲がなく、ヒープが唯一のメモリなので、アプリはヒープに空きがある場所に配置されます。128Kでは、スペアバンクは実行場所ではなくロード場所です。それらは$C000でページインされ、ヒープとすべてのウィンドウの状態がすでにそこにあります。

制限。 一度に3つのアプリをロードでき、同じファイルを2回ロードすると、それぞれが独自の状態で2つのコピーが作成されます。アプリはウィンドウが閉じられた後もメモリに残っており、次のロードで解放されます。サイズ制限はデバイスのものです。RAMデバイスからは256バイト、テープからは511バイト、バンクからは8,192バイトです。新しいインターフェースを要求するファイル、イメージの外側を指すリロケーション、または途中でカットされたファイルは、アラートとともに拒否され、ヒープは元のままです。

スロット

スロットnは$8000 + 3nのジャンプです。行と列はピクセル行とバイト列です。「ウィンドウ相対」とは、Bがタイトルバーの下のピクセル行、Cが前面のウィンドウの左境界からの列を意味します。行が特に指定しない限り、すべてのレジスタが変更されると想定してください。

スロット名前入力出力
1ApiVersionA = インターフェースバージョン、B = スロット数
2ApiWndOpenA = APP_ の種類ウィンドウが開けなかった場合はキャリー
3ApiWndClose前面のウィンドウを閉じます。アプリに最初に尋ねます
4ApiWndIsFrontライブウィンドウが前面にある場合はZフラグ
5ApiWndRepaintデスクトップとすべてのウィンドウを再描画します
6ApiWndBarライブウィンドウのスクロールバーを再描画します
7ApiWinGrabライブウィンドウを画面からバッファにコピーします
8ApiPrintHL = 文字列デスクトップ使用:行はデスクトップ変数から取得。29を使用
9ApiPrintClipHL = 文字列デスクトップ使用、8と同様。29を使用
10ApiFillRectデスクトップ使用:矩形はデスクトップ変数にあります。43を使用
11ApiAddrAtA = ピクセル行、C = 列HL = 画面アドレス
12ApiPtrRestoreポインターを画面から持ち上げます。描画前に呼び出してください
13ApiPtrSaveBgポインターの下にあるものを保存します。描画後に呼び出してください
14ApiPtrDrawポインターを描画します。13の後で呼び出してください
15ApiAllocBC = バイト数、A = オーナーHL = ブロック。空きがない場合はキャリー
16ApiFreeHL = ブロック、またはゼロ
17ApiFreeOwnerA = オーナーそのオーナーを持つすべてのブロックを解放します
18ApiEvPostA = イベントタイプ、B, C = 引数キューがいっぱいだった場合はキャリー
19ApiStSelectA = ST_ デバイスそのようなデバイスがない場合はキャリー
20ApiStOpenHL = 名前、B = FA_READ または FA_OVERWRITEA = ハンドル。できなかった場合はキャリー。テープでは、空の名前は次のファイルを読み取ります
21ApiStCloseA = ハンドル
22ApiStReadA = ハンドル、HL = バッファ、BC = バイト数BC = 読み取られたバイト数
23ApiStWriteA = ハンドル、HL = バッファ、BC = バイト数BC = 書き込まれたバイト数
24ApiStDirA = n、ゼロからHL = n番目のファイルの名前、DE = その長さ。ない場合はキャリー。テープでは、n=0は最後に読み取られたファイル
25ApiStDeleteHL = 名前削除できなかった場合はキャリー
26ApiStCapsA = 現在のデバイスの STCAP_ ビット
27ApiStIdentA = 現在の ST_ デバイス
28ApiWinRecHL = ライブウィンドウ:X、Y、幅、高さ、それぞれ1バイト
29ApiWinPrintウィンドウ相対、HL = 文字列ウィンドウの端でクリップされて印刷されます
30ApiWinPrintInv29と同様同じものを反転して印刷
31ApiWinClearウィンドウの内部を空白にします
32ApiWinAddrウィンドウ相対HL = 画面アドレス
33ApiWinRowsA = ウィンドウ内のピクセル行。他のレジスタを保持します
34ApiWinColsA = ウィンドウ内の列。他のレジスタを保持します
35ApiWinAppHL = ライブウィンドウのディスクリプタ
36ApiWndCloseNow前面のウィンドウを尋ねずに閉じます
37ApiKbdModsA: ビット0 CAPS SHIFT、ビット1 SYMBOL SHIFT
38ApiWinPrintIf29と同様、A = ゼロまたは非ゼロAがゼロの場合はプレーンに印刷、それ以外の場合は反転して印刷
39ApiDlgAlertHL = 行、DE = 2番目の行またはゼロアラート、1行あたり14文字
40ApiDlgConfirmHL, DE は39と同様、BC = ルーチンすぐに返ります。ルーチンは、はいの場合はA=1を受け取ります
41ApiDlgSave3HL, DE は39と同様、BC = ルーチンルーチンは、キャンセル=0、破棄=1、保存=2を受け取ります
42ApiOpenFileHL = 現在のデバイス上の名前アプリファイルを実行するか、メモ帳を開きます。キャリー:何も開かれなかった。A=1:アラートが表示されている
43ApiWinFillウィンドウ相対、D = 行数、E = 列数、HL = パターン塗りつぶし。呼び出し元はウィンドウ内に保持します
44ApiWinRefreshフロントウィンドウを Draw を介して再描画します

一度に1つのデバイスで1つのファイルが開かれます。文字列はゼロバイトで終了します。スロット0はデスクトップ自体のエントリポイントです。


未完成のもの

esxDOS は記述され、パスしました。 しばらくの間、それは不可能でした。なぜなら、DivMMCを持っておらず、実機で実行する前にコードをコミットしなかったからです。esxDOS が常駐するエミュレートされた DivMMC が、3回の行き止まりの後、2026年9月にそれを解除しました。

Next はポートにはなりませんでした。 それは独自のレポジトリで独自のシステムになりました。なぜなら、ウィンドウモデルが異なるからです。オーバーラップではなくタイリングするため、コンポジタ、セーブアンダーアリーナ、およびほとんどのダメージ機構はそこで役立ちません。ウィンドウモデル以下のすべてが引き継がれました。まだ公開されていません。

既知のギャップ。 他のウィンドウの後ろにある時計は、ウィンドウバッファが画面からではなくコンポーズから取得されるため、最後に見た時間を保持します。アプリケーションは、終了を拒否できません。なぜなら、いいえと言えるテイクダウンベクトルがなく、何かが必要になる前にそれを追加することは推測だったからです。すべてのメモ帳は、ドキュメントではなくアプリケーションからタイトルが付けられています。メモ帳には選択、クリップボード、元に戻す、または単語ラップがありません。マウスの存在検出はまだヒューリスティックであり、マウスのないマシンを持っていないためテストできません。

既知のライブバグはありません。 最後のアラートは割り込み安全フィルで閉じられ、それ以降何も開きませんでした。


48Kシステムは最初のコミットからポートを念頭に置いて構築され、デバイスレイヤーの境界が保持されるという賭けでした。ストレージ、esxDOS、アプリケーションモデル、設定レコードについては保持されました。ウィンドウモデルについては保持されませんでした。そのため、ZX Desk Next はポートではなく2番目のシステムになっています。28MHz Z80、レイヤー2、タイルマップ、ハードウェアスプライト、DMAを備えたマシンでは、適切なデスクトップはタイルであり、タイリングデスクトップにはオーバーラップをソートするために構築されたコンポジタは不要です。

それは独自のレポジトリにあり、別途公開されます。まだNextを持っておらず、48Kと128Kのマシンしか持っていません。エミュレーターでブラインドで構築するつもりはないので、到着するまで待っています。


リポジトリレイアウト

build.sh, run.sh            アセンブルし、マシンにロードします
tstates.py, taplant.py      タイミング計算、およびテープブロックの植え付け
mkapi.py, mkapp.py          アプリのインクルードを作成し、アプリをアセンブルします
mkwall.py, taploader.py     画像を壁紙帯に、および2ブロックローダー
api/zxdesk.inc              アプリがアセンブルされるもの
examples/                   デスクトップの外でビルドされた3つの動作するアプリ

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のスペアバンクをRAMディスクとして
src/heap.inc                ヒープとそのオーナーバイト
src/app.inc                 アプリケーションディスクリプタと状態スワップ
src/api.inc                 アプリケーションインターフェース、$8000のジャンプテーブル
src/resize.inc              グリップ、アウトラインドラッグ、再アロケーション
src/note.inc                メモ帳
src/clock.inc               時計
src/calendar.inc            カレンダー
src/commander.inc           2ペインファイルマネージャー
src/desktop.inc             デスクトップショートカット
src/filemgr.inc             ファイルパネル
src/loader.inc              アプリファイルをヒープにロードして実行します
src/settings.inc            設定レコード
src/wall.inc                壁紙フィル、バンク1から
src/setdata.inc             フレームが読み取るその部分
src/script.inc              スクリプト入力、エンドツーエンド検証用
images/                     上記のスクリーンショット

感謝

FuseとCésar Hernández BañoのZEsarUXに。これらは、実機で実行する前に開発が行われた方法です。Julián Alboのpasmoに。これは小さく、言われたことを正確に実行します。Spectrumコミュニティに。30年間の競合、フローティングバス、ROMエントリポイントに関するドキュメントのおかげで、これは数年ではなく数ヶ月で済みました。そして、私が繰り返し参照していたULAタイミングノートを書いた人に。あなたの数値は正しく、私の仮定は間違っていました。

InkboxのNES-OSに。これは、このようなことがやる価値があることの証明です。

そして、これを行うように決して設計されていなかったマシン、そしてそれを実行するマシンに。


ライセンス

MIT、LICENSEを参照してください。ここにあるものは、他人のコードから派生したものではありません。ツールチェーン、エミュレーター、およびそれらが使用するROMは、それと一緒に配布されません。