tsutsuiの作業記録置き場

NetBSDとかPC-6001とかの作業記録のうち、Twitterの140字では収まらない内容や記事としてまとめるべき内容をとりあえず置いてみる予定

PC-6001 タイニーあぴミクさんデモができるまで (プログラム編)

前回のエントリの続きで、タイニーあぴミクさんデモのプログラム本体について書こうと思います。
無駄に長いので閲覧注意

プログラムといっても、やっていることは「よっしゅさんの P6 PSGドライバで BGMを鳴らしながらグラフィックデータをテープからロードしている」だけで、技術的に高度なことをしているわけではありません。

と前回書いていますが、そもそも最初からデモを作ろうと思ったわけではなく、最初は単に

「減色して作った SCREEN3 画像を実機で表示してみるついでに、採譜した曲も鳴らそう」

という程度のノリでした。

Step 1: とりあえずテープからロード

まずは「VRAMデータのロード」「PSGドライバと曲データのロード」ということをしてやる必要がありますが、N60-BASIC には BLOAD といった機械語ロードの命令はありません。なので、テープからのデータロードについては自前で実装する必要があります。

このテープからのデータロードについては、安直に Tinyみずいろデモ で使われていた BASIC ROM内ルーチンを使う方法をそのまま流用させてもらいました。

Tinyみずいろデモでは「5枚のグラフィックデータをオンメモリに読み込んでおいて、BASICプログラムによるアドベンチャーパート(?)の画像表示コマンドで指定された画像をVRAMに転送する」という構成です。

テープからのロードルーチン自体は BASIC プログラムの DATA文で格納されていて、 READ文で読み出した上で POKE文で RAM上に書き込み、 EXEC文で実行、という流れです。
N60-BASICについて解説しているといくら書いても終わらないので「N6x BASIC リファレンス-コマンド一覧」等を参照してください

実際のテープからのロードとデータの構成については、まずは Z80のコードをそのまま貼ってみます。 Z80の命令そのものやアセンブラについてはいくら書いても(以下略

        org     9f28h

VRAMTOP equ 0e200h

OPENTAPE equ 1a61h
READTAPE equ 1a70h
CLOSETAPE equ 1aaah

loadbin:
call OPENTAPE
ld hl,datatop
ld bc,dataend-datatop
loadbin1:
call READTAPE
cp 0c9h
jr nz,loadbin1
loadbin2:
call READTAPE
cp 0c9h
jr z,loadbin2
loadbin3:
call READTAPE
ld (hl),a
inc hl
dec bc
ld a,b
or c
jr nz,loadbin3
call CLOSETAPE
ld hl,datatop
ld de,VRAMTOP
ld bc,6144
ldir
ret

exo_mapbasebits:
defs 156 ; exomizer tables for bits, baseL, baseH

org_a000: ds 0a000h - org_a000 ;
org 0a000h

datatop:

include "apimiku-screen3.asm"

org_d000: ds 0d000h - org_d000 ;
org 0d000h
include "psgdrv.asm"

org_d740: ds 0d740h - org_d740 ;
org 0d740h
;deexo
;input: hl=compressed data start
; de=uncompressed destination start

include "deexo.asm"

org_d800: ds 0d800h - org_d800 ;
org 0d800h
OBJSAD:
include "TellYourWorld.asm"

dataend:
end

Tinyみずいろのプログラムのメモリマップは 以前のエントリ に書いていますが、

  • 9F00h〜9F27h までが画像表示ルーチン
  • 9F28h からがテープロードルーチン
  • A000h 以降にロードしたデータを格納

という構成です。

テープのアクセス自体は BASIC ROM内ルーチンの「テープ入力開始」「テープ1バイト入力」「テープ入力終了」を呼んでいるだけです。ちなみに「テープ入力開始」と「テープ入力終了」のルーチンではデータレコーダーの REMOTE端子のリレーON/OFFも行われます。

テープ上に記録されているデータの形式については、 CSAVE で記録される N-60 BASIC のプログラムについては

  • D3h が 10バイト連続
  • 続く 6バイトがファイル名

という形式になっています。一方、カスタムの機械語データのロードにおいても先頭に同期用のヘッダデータを付加するのが作法なのか、先頭に C9h の連続データ + 00h のデータが付加されていました。

このヘッダの処理について、前述のプログラム上では

  • loadbin1 のところが ヘッダの C9h のデータが現れるまで待つループ
  • loadbin2 のところが「C9h 以外のデータが1バイト来る」のを待つループ
  • loadbin3 のところが「BCレジスタで指定された長さ分だけテープからロードする」というループ

という構成で処理されています。

すべてのデータが読み込まれたら、 LDIR 命令を使ってロードされたグラフィックデータを VRAMに転送し、 C9h の RET 命令で BASIC にリターンします。P6 PSGドライバの演奏開始の呼び出しは BASIC 側の EXEC文で実行しています。

それ以降にある org 0a000h からのアドレスのデータがテープからロードされるデータで、以下の順で配置されています。

  • あぴミクさん画像の VRAM データ
  • P6 PSGドライバ
  • Exomizer 展開ルーチン(この時点では未使用)
  • 採譜した Tell Your World の曲の P6 PSGドライバ用コンパイル済み演奏データ

これらを書いて遊んでいたのがこのあたりのツイート。

Step 2: ストリーミングロード

「ストリーミングロード」とは、 Tiny野郎さんのYS2OPデモ で使用されている、「画面スクロール描画とBGM演奏をしながらテープからデータをロードする」という技です。
この動画を見てP6いじりにハマったといっても過言ではありません

P6におけるテープストリーミングロードの実装詳細については Tiny野郎さんのブログ記事「P6イース2オープニングの作り方(6)」でくわしく解説されていますが、ポイントとしては以下の2点です。

  1. テープに記録するデータのストップビットをデフォルトより多く挿入し、テープロードにおけるバイトデータ間の待ち時間を増やすことで、2ms割り込みで処理されるPSGドライバの実行時間を確保する
  2. 画面描画のような時間のかかる処理を行う場合は、その処理時間に対応する量のダミーデータをテープ上に配置して、処理の間テープが読み飛ばされても問題ないようにする

Step 1 のテストプログラムが動いた後、

  • 画像データは最初からVRAMに直接ロードすれば上から順番に表示されるはず
  • そうすると画面に動きが出てロード待ち時間の退屈が緩和されるんじゃないか
  • さらに、PSGドライバと曲データを先にロードするようにすれば BGM演奏を始めた状態で画像データロードができるはず?

ということで書いてみたのが以下のプログラムです。

        org     9f00h

VRAMTOP equ 0e200h

OPENTAPE equ 1a61h
READTAPE equ 1a70h
CLOSETAPE equ 1aaah

loadbin:
call OPENTAPE
ld hl,datatop
ld bc,dataend-datatop
loadbin1:
call READTAPE
cp 0c3h
jr nz,loadbin1
loadbin2:
call READTAPE
cp 0c3h
jr z,loadbin2
loadbin3:
call READTAPE
ld (hl),a
inc hl
dec bc
ld a,b
or c
jr nz,loadbin3

call START

ld hl,VRAMTOP
ld bc,vdataend-vdatatop
loadbin11:
call READTAPE
cp 0c3h
jr nz,loadbin11
loadbin12:
call READTAPE
cp 0c3h
jr z,loadbin12
loadbin13:
call READTAPE
ld (hl),a
inc hl
dec bc
ld a,b
or c
jr nz,loadbin13
call CLOSETAPE
ret

exo_mapbasebits:
defs 156 ; exomizer tables for bits, baseL, baseH

org_a000: ds 0a000h - org_a000 ;
org 0a000h

datatop:

include "psgdrv.asm"

org_a740: ds 0a740h - org_a740 ;
org 0a740h
;deexo
;input: hl=compressed data start
; de=uncompressed destination start

include "deexo.asm"

org_a800: ds 0a800h - org_a800 ;
org 0a800h
OBJSAD:
include "TellYourWorld.asm"

dataend:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

vdatatop:
include "apimiku-screen3.asm"
vdataend:

end

まず、ロードされるデータの配置を以下のように入れ替えます。

  • A000h (datatop ラベル) の先頭に P6 PSGドライバを置く
  • A740h から exomizer の展開ルーチンを置く (この時点では未使用)
  • A800h から Tell Your World の P6 PSGドライバ用コンパイル済み曲データを置く
  • その後ろの vdatatopvdataend のラベルの間に VRAMデータを置く

その上で、曲データと VRAMデータの間に、処理時間待ちのダミーデータとロード再開時の同期用ヘッダデータを配置します。

ロードプログラムの動作は以下のようになっています。

  • loadbin1, loadbin2, loadbin3 の各ラベルで PSGドライバと曲データを A000h からの領域にロード
    ヘッダのデータを C9h から C3h に変えた理由が思い出せない……
  • CALL START で PSGドライバ演奏開始ルーチンを呼んで BGM開始
  • loadbin11, loadbin12, loadbin13 の各ラベルであぴミクさん画像データを VRAMの E200h 以降に直接ロード
  • ロードが終わった時点で BGM演奏も画面表示も完了しているのでそのまま BASIC にリターン

ストリーミングにおいて必要な「テープに記録するデータのストップビットをデフォルトより多く挿入する」については、 Tiny野郎さん作の P6形式データ→実機テープロード用wav変換ツールである「なんでもピーガー」がストップビット設定に対応しているので、そこでパラメータとしてストップビットの数を設定するだけです。

YS2OPデモのP6実機実行 でいろいろ試行錯誤したときの経験からすると、 P6 PSGドライバを安定動作させるためには「ストップビット=7ビット」にすればほぼ問題ないようです。

なお、ストップビットが短すぎる場合は、 BGMがおかしくなるのではなく、ロードされるデータの取りこぼしが発生します。その結果として画面化けやロードデータ数のズレ等により暴走等が発生することになります。

ストリーミングにおいて必要な 2点目の「処理時間に応じたダミーデータの挿入」については、この時点ではシビアなタイミングは不要であったので、適当に多めにデータを挿入して、再ロード時は最初のロードと同様のヘッダを入れるようにしました。

ストリーミングロードにおけるエミュレータと実機動作の違い

Tiny野郎さんの「P6イース2オープニングの作り方(6)」の記事でも書かれていますが、ストリーミングロードについてはエミュレータ上の動作と実機での動作とでかなりの差があります。

デモプログラム作成においては実機での調整が必要で、この調整をするには 再アセンブル〜wav作成〜実機ロード というサイクルが必要になり、トライアンドエラー的な意味で結構難儀します。

テープデータ中のストップビット

P6 の BASIC の標準では UART のシリアル通信としてのビット構成は
スタートビット 1ビット + データ 8ビット + ストップビット 3ビット
で、合計すると「1バイトあたり 12ビット」で構成されています。この 12ビットが 1200bps で記録されるので、1バイトのデータが 10ms 毎にロードされることになります。

ここから先は想像ですが、各P6エミュレータ上では
「ロードデータの受信を完了してから10ms以内に読み出しが行われれば取りこぼさない」
という構成になっているのではないかと思います。

一方、 P6実機(正確にはデータロードを担当しているサブCPU)では、UARTとしてのストップビットは1ビットで設定されているのではないかと思います。その場合、
「ストップビットの1ビット目で受信が完了して、次のバイトのスタートビットが始まるまでにデータを取り出す必要がある」
ということになるのではないかと思います。

標準のストップビット3ビットの設定だと残りの猶予時間は 2ビット分、つまり (1/1200 × 2ビット) の 約1.67ms の間にデータを取り出す必要があるということになりますが、 P6 PSGドライバの割り込み処理時間は 2ms以上かかる場合もあるようなので、これでは全然間に合わないということになります。

ストップビットを7ビットにした場合は猶予時間は6ビット、つまり 5ms あることになります。PSGドライバ以外の VDG転送時間がどれくらいなのかは確認していませんが、PSGドライバだけでも 3ms以上消費する場合もあるようなので、設定としてはわりと妥当なのではないかと思っています。

ストップビット設定によるロード時間の差異

標準のストップビット 3ビットの設定の場合は 1バイトデータのフレームが 12ビットで 1200bps の場合 10ms/バイト という速度になります。一方、ストップビットを 7ビットにした場合は 1バイトデータのフレームが 16ビットになり、 1200bps では標準の 12ビットの場合と比べ 16/12 倍の 13.3ms/バイト かかるということになります。

PC6001VPC6001VX の場合は 1バイトあたりのロード時間は標準のビット構成で固定されていて 10ms になっており、「ストップビットを7ビット」に変更した前提のプログラムだと「テープロード時間と BGM演奏のタイミング」や「プログラム処理実行中のテープ読み飛ばしのバイト数」がかなりズレてきます。

PC6001VW の場合は「オプション2の設定項目」の中に「テープボーレート」という項目があり、これがデフォルトでは (1200bps ではなく) 1000bps となっています。これが何を意味するかと言うと、「標準の 12ビットフレームを受信するのに 12ms 必要」つまり、2ms分 (=2.4ビット分)だけストップビットが多いのと等価です。

PC6001VW においてこの「テープボーレート」を 900bps に設定すると、 1バイトフレームの受信時間が 1200/900倍の 13.3ms となり、実質ストップビットを 7ビットにした実機と同じタイミングでロードされることになります。この設定の場合は「テープロード時間とBGM演奏のタイミング」のズレはかなり少なくなるようです。

ただし、 PSGドライバの 2ms 割り込みやサブCPUからのデータの取り出し等々で実機とまったく同じタイミングとはいかず、全体としては「実機ではエミュレータでの設定よりダミーデータを増やす必要がある」という傾向がありました。このため、最終的には実機でタイミングを合わせる必要があります。

Step 3: 画像データ圧縮

BGMを鳴らしながらのストリーミングロードが動いたところで、

「このまま画像を増やして BGMにあわせてあぴミクさんを次々と表示すれば、元動画に対するタイニー版と呼べるものが作れるんじゃないか」

などという考えが頭に浮かびました。
マシンガンPによるかわいいあぴミクさん元動画はこちら

そんな思いつきで作業を始めたのがこのツイート。

動画投稿を見据えてデモの構成を考えると、1つのポイントとして
「テープロードの待ち時間(=画面が何も動いていない時間)をなるべく短くする」
ということを考えました。

データロード時間を短くするには「データを圧縮する」という案があり、圧縮アルゴリズム自体は Tinyみずいろデモ での Exomizer の実績はあったので、それを適用するだけという案はありました。

一方、画像データを圧縮すると
「圧縮したデータ全体を読み込んだ後でないと展開(=表示)できない」
という問題がある一方、VRAM直接ロードの場合は
「左上から順に走査線のように描画されていく様子が実質アニメーションになる」
というメリット(?)もあります。

……といったことをあわせて、ざっと考えて以下の構成にすることにしました。

  • 1枚目は(何も表示されていないところからスタートなので)VRAM直接ロードで描画
  • 2枚目以降は以下のように2分割で描画
    • 上半分は(前の絵を愛でる時間として) Exomizer 圧縮データをロードした後に展開
    • 下半分は(飽きてきた頃にロード中を主張する意味で) VRAMに直接ロードで描画

ランレングス圧縮

「VRAMに直接ロード」の場合でも多少なりともロード時間を短くできないか、ということで、Tiny野郎さんのブログ記事「P6イース2オープニングの作り方(3)」でも挙げられていたランレングス圧縮を入れてみることにしました。

Tiny野郎さんの記事にもあるとおり、ランレングス圧縮は「同じデータが連続する場合に、データと個数を並べる」という方式です。

より具体的には、0〜FFh までの 256通りの1バイトデータの1つを「キーデータ」として、同じデータが3つ以上連続するパターンが現れた場合(もしくはキーデータそのものが現れた場合)には
(キーデータ) (個数) (連続するデータ値)
と記録します。

展開時は、「キーデータが現れた場合はフォーマットに従って指定されたデータを個数分連続して書き込む」という動作になります。

ここで、ランレングス圧縮のもう一つのメリットとして
「圧縮後のデータ全体をバッファに格納する必要がなく、1バイトずつ読んだそばから展開ができる」
という点が生きてきます。「キーデータが来たら次のデータを個数として記憶」「さらに次のデータを個数分連続して書く」だけなので、ロード時の動作としては「データが連続している箇所は一気に描画される」という具合になります。

実際には、P6の SCREEN3 モードでは「1バイト当たり4ドット」なので、圧縮が効くのは「4ドットの特定パターンが3回以上繰り返される」という場合だけになり、意図してそういうパターンを描かない限りは実質圧縮されるのは背景のベタ塗り部分だけになります。

しかし、キーデータを「ほぼありえないデータ」にしておけば元データのサイズより増えることはほぼ無く、実際に使ったデータも背景ベタ部分がそれなりにあったので、結構圧縮の効果はありました。

特に、1枚目のこのあぴミクさんだと元の 6144バイトに対してランレングス圧縮後は 3589バイトと、約58%のサイズになりました。

ということで、前項のストリーミングロードのプログラムに対してランレングス圧縮展開を追加したのがこちらのプログラム。

        org     9f00h

VRAMTOP equ 0e200h

OPENTAPE equ 1a61h
READTAPE equ 1a70h
CLOSETAPE equ 1aaah

MAGIC equ 09ch

loadbin:
call OPENTAPE
ld hl,datatop
ld bc,dataend-datatop
loadbin1:
call READTAPE
cp 0c3h
jr nz,loadbin1
loadbin2:
call READTAPE
cp 0c3h
jr z,loadbin2
loadbin3:
call READTAPE
ld (hl),a
inc hl
dec bc
ld a,b
or c
jr nz,loadbin3

call START

ld hl,VRAMTOP
ld de,vdataend-vdatatop
loadbin11:
call READTAPE
cp 0c3h
jr nz,loadbin11
loadbin12:
call READTAPE
cp 0c3h
jr z,loadbin12
loadbin13:
call READTAPE
dec de
cp MAGIC
jr z,loadbin14
ld (hl),a
inc hl
jr loadbin16
loadbin14:
call READTAPE
dec de
ld b,a
call READTAPE
dec de
loadbin15:
ld (hl),a
inc hl
djnz loadbin15
loadbin16:
ld a,d
or e
jr nz,loadbin13
call CLOSETAPE
ret

exo_mapbasebits:
defs 156 ; exomizer tables for bits, baseL, baseH

org_a000: ds 0a000h - org_a000 ;
org 0a000h

datatop:

include "psgdrv.asm"

org_a740: ds 0a740h - org_a740 ;
org 0a740h
;deexo
;input: hl=compressed data start
; de=uncompressed destination start

include "deexo.asm"

org_a800: ds 0a800h - org_a800 ;
org 0a800h
OBJSAD:
include "TellYourWorld.asm"

dataend:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

vdatatop:
include "api1-vram-rlcomp.asm"
vdataend:

end

MAGIC で書いている 9Ch がキーデータで、意図としては「10011100b」という「1バイト中の4ドットがすべて異なる色になっているパターン」はそんなに無いだろう、というくらいでしたが、実際出てくることはなかったようです。

実装としては、ラベル loadbin13 から loadbin15 の部分で「個数を Bレジスタに入れて DJNZ を使って連続書き込み」としているだけです。DJNZ のループ分だけ 1バイト受信後の処理時間が増えることになりますが、これくらいだと問題にはならないようでした。

ちなみに圧縮データを作成するためのプログラムはその場で適当に書いたのですが、これも gist に投棄しておきました。検証用展開プログラムはこちらいずれも「圧縮して展開した結果が一致すればOK」という超テキトー実装なのでツッコミはご勘弁

蛇足ですが、ランレングス圧縮の場合「256個以上のデータが連続する場合でも、フォーマット上圧縮できるのは255個まで」というところに注意が必要です。この罠に見事にハマりました

複数画像展開

ランレングス圧縮展開が動けば、もう一つの Exomizer 展開ルーチンはすでに動作実績があるので、後は以下のように順にロード・展開の実装を書くだけです。

  • 1枚目をランレングス圧縮展開で VRAM直接展開
  • 2枚目の上半分の Exomizer 圧縮データをバッファに読み出し
  • 読み出した圧縮データをVRAMに展開
  • 2枚目の下半分をランレングス圧縮展開で VRAM直接展開
  • 3枚目以降も同様

ただ、4分弱の Tell Your World の曲データに対して何枚の画像を表示できるかというところは実際に圧縮データを作ってロード時間を見ないとわかりません。ということで、最初はエイヤで以下の4枚で作成しました。







が、デバッグに悪戦苦闘しつつエミュレータ上でテストすると結構丈が余ってしまったので、元動画の「だめー?」というミクさんの絵を作って2枚目と3枚目の間に足して見ることにしました。

さらに、「せっかくだから1枚くらいはアニメーションっぽくできないかな〜」と、この絵だけは全画面を Exomizer 圧縮して一気に表示することにしました。結果として、 Exomizer の展開処理による描画速度が逆にいい感じで、さらに、たまたま書き換えのタイミングが BGMの2周目のループ部分に来たことで思ったよりいい感じになりました。自己満足炸裂モード

このあたりツイートがこのへんをいじって遊んでいた時分ですね……

このときにコミットした PC6001VW でデバッグ完了したソースコードはこちら。

        org     9f00h

VRAMTOP equ 0e200h

OPENTAPE equ 1a61h
READTAPE equ 1a70h
CLOSETAPE equ 1aaah

MAGIC equ 09ch

loadbin:
call OPENTAPE

ld hl,datatop
ld bc,dataend-datatop
call loadtape

call START

ld hl,VRAMTOP
ld de,api1end-api1top
call loadrl

ld hl,loadbuf
ld bc,api21end-api21top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api22end-api22top
call loadrl

ld hl,loadbuf
ld bc,api2aend-api2atop
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,loadbuf
ld bc,api31end-api31top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api32end-api32top
call loadrl

ld hl,loadbuf
ld bc,api41end-api41top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api42end-api42top
call loadrl

; call CLOSETAPE
; ret
jp CLOSETAPE

skipmark:
call READTAPE
cp 0c3h
jr nz,skipmark
skipmark1:
call READTAPE
cp 0c3h
jr z,skipmark1
ret

loadtape:
call skipmark
loadtape1:
call READTAPE
ld (hl),a
inc hl
dec bc
ld a,b
or c
jr nz,loadtape1
ret

loadrl:
call skipmark
loadrl1:
call READTAPE
dec de
cp MAGIC
jr z,loadrl2
ld (hl),a
inc hl
jr loadrl4
loadrl2:
call READTAPE
dec de
ld b,a
call READTAPE
dec de
loadrl3:
ld (hl),a
inc hl
djnz loadrl3
loadrl4:
ld a,d
or e
jr nz,loadrl1
ret

org_a000: ds 0a000h - org_a000 ;
org 0a000h

datatop:

include "psgdrv.asm"

org_a740: ds 0a740h - org_a740 ;
org 0a740h
;deexo
;input: hl=compressed data start
; de=uncompressed destination start

include "deexo.asm"

org_a800: ds 0a800h - org_a800 ;
org 0a800h
OBJSAD:
include "TellYourWorld2.asm"

dataend:

exo_mapbasebits:
defs 156 ; exomizer tables for bits, baseL, baseH

loadbuf:

db 0,0,0,0
db 0c3h,0c3h,0c3h,000h

api1top:
include "api1-vram-rlcomp.asm"
api1end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api21top:
include "api2-vram_1_exo.asm"
api21end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api22top:
include "api2-vram_2_rlcomp.asm"
api22end:

db 0,0,0,0,
db 0c3h,0c3h,0c3h,000h

api2atop:
include "api2a-vram_exo.asm"
api2aend:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api31top:
include "api3-vram_1_exo.asm"
api31end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api32top:
include "api3-vram_2_rlcomp.asm"
api32end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api41top:
include "api4-vram_1_exo.asm"
api41end:

db 0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api42top:
include "api4-vram_2_rlcomp.asm"
api42end:

end

自分で見返しても頭悪いという感じですが、アセンブラなので無理して構造化を考えず頭からベタで「ランレングス圧縮データのVRAM直接読み込み」「Exomizer圧縮データ読み込み」「Exomizer圧縮データ展開」をひたすら繰り返しているだけです。

これをアセンブルしてできたバイナリのうち、datatop ラベルの A000h 以降のデータに C3h のヘッダマークを付けたものをテープからロードする P6形式ベタデータとして保管する、ということになります。

Step 4: 実機調整

ストリーミングロードの項で書いたとおり、テープロードおよび PSGドライバの動作についてはエミュレータと実機とでは微妙にタイミングが異なるので、P6ファイル作成→wavファイル変換→P6実機CLOAD というサイクルで主に処理待ち時間のダミーデータの数を調整することになります。

なんだかんだで実機での調整に1時間くらいかかってますね……。

ここまでは「Windows Media Player で wav 再生と同時に CLOAD」でテストしていたので、次はデータレコーダー実機でのテストです。

理屈の上では wav ファイル直接ロードで動いたのであればデータレコーダー実機で動かない理由はないのですが、このときはまだ複数 wav ファイルの結合(BASIC分とデータ分で2つのP6ファイルがあるので 2つの wav を結合していた)の手順が確立していなかったのと、データレコーダー自体も修理が完全ではなかったのが敗因だと思います。

Step 5 : PSGドライバロード時間短縮

「Tell Your World のオルゴール曲を鳴らしながらあぴミクさん絵をロードする」というデモ自体は当初の目論見通りに完成したわけですが、いざ動画撮影の準備として何度かデモを流してみると、最初に PSGドライバと曲データをロードしている間の「Now Loading...」の時間を短縮したくなってきました。

ざっとサイズを計算してみるとこんな感じです。

ストップビットを 7で作ったのか 8で作ったのかいまいち覚えていない

20秒というは動画視聴的にはかなり大きな時間なので、以下のように構成を変更してテキトーに実装してみました。

  • PSGドライバを A000h から配置
  • PSG曲データを A800h から配置
    (ドライバと曲データとの間にすき間ができるが圧縮されるので気にしない)
  • その後ろから Exomizer の展開ルーチンを配置して、そこからロード用P6データ領域にする
  • Exomizer展開ルーチンのあとに圧縮した PSGドライバと曲データを置く
  • それ以降は従来通りの画像データを配置

実行時は以下のようになります。

  • テープ先頭の Exomizer 展開ルーチンと圧縮された PSGドライバ+曲データを読む
  • そこまで読み終わったら PSGドライバ+曲データを展開して即BGM再生開始
  • それ以降は従来通り順番にロードもしくは展開

「圧縮された PSGドライバ+曲データ」を準備するのにアセンブラを 2パスで回す必要がありやや面倒ですが、それを除けばわりとあっさり動きました。20秒の短縮効果はかなり大きく、かつてのドアドアもこんな気持ちで作ったのかな〜、などと勝手な自己満足に浸っていたのですが、この後で再度実機でのタイミング調整をするはめになりました……。ズサンな計画ともいう

gist か github に置けよという感じですが、この動画撮影時点の最終ソースコードは以下:

        org     9f00h

VRAMTOP equ 0e200h

OPENTAPE equ 1a61h
READTAPE equ 1a70h
CLOSETAPE equ 1aaah

MAGIC equ 09ch

loadbin:
call OPENTAPE

ld hl,loadstart
ld bc,psgend-loadstart
call loadtape

ld hl,psgtop
ld de,datatop
call deexo

call START

ld hl,VRAMTOP
ld de,api1end-api1top
call loadrl

ld hl,loadbuf
ld bc,api21end-api21top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api22end-api22top
call loadrl

ld hl,loadbuf
ld bc,api2aend-api2atop
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,loadbuf
ld bc,api31end-api31top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api32end-api32top
call loadrl

ld hl,loadbuf
ld bc,api41end-api41top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api42end-api42top
call loadrl

; call CLOSETAPE
; ret
jp CLOSETAPE

skipmark:
call READTAPE
cp 0c3h
jr nz,skipmark
skipmark1:
call READTAPE
cp 0c3h
jr z,skipmark1
ret

loadtape:
call skipmark
loadtape1:
call READTAPE
ld (hl),a
inc hl
dec bc
ld a,b
or c
jr nz,loadtape1
ret

loadrl:
call skipmark
loadrl1:
call READTAPE
dec de
cp MAGIC
jr z,loadrl2
ld (hl),a
inc hl
jr loadrl4
loadrl2:
call READTAPE
dec de
ld b,a
call READTAPE
dec de
loadrl3:
ld (hl),a
inc hl
djnz loadrl3
loadrl4:
ld a,d
or e
jr nz,loadrl1
ret

org_a000: ds 0a000h - org_a000 ;
org 0a000h

datatop:

include "psgdrv.asm"

org_a800: ds 0a800h - org_a800 ;
org 0a800h
OBJSAD:
include "TellYourWorld2.asm"

dataend:

exo_mapbasebits:
defs 156 ; exomizer tables for bits, baseL, baseH

loadstart:
;deexo
;input: hl=compressed data start
; de=uncompressed destination start

include "deexo.asm"

loadbuf:

psgtop:
include "psgdrvdata.asm"
psgend:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api1top:
include "api1-vram-rlcomp.asm"
api1end:

db 0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api21top:
include "api2-vram_1_exo.asm"
api21end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api22top:
include "api2-vram_2_rlcomp.asm"
api22end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api2atop:
include "api2a-vram_exo.asm"
api2aend:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api31top:
include "api3-vram_1_exo.asm"
api31end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api32top:
include "api3-vram_2_rlcomp.asm"
api32end:

db 0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api41top:
include "api4-vram_1_exo.asm"
api41end:

db 0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api42top:
include "api4-vram_2_rlcomp.asm"
api42end:

end

さらに動画撮影に向けて再度テープを準備:

結局最後はストップビット 7ビットで作ったみたいですね

こうしてようやくできあがったデモを動画として撮影して、ニコ動に投稿したのが前回のエントリでも書いた以下のツイート。

PC-6001 を展示した OSC京都会場でも以下のようにいい感じでした。

Step 6: 16KB 対応

Tell Your World オルゴール曲の採譜から始まって、本題だったはずの OSC京都の準備をそっちのけにして作ってしまったタイニーあぴミクさん動画ですが、撮影後も1つ気になっていました。

もともとあぴミクさんでもは Tinyみずいろデモの構成をベースにしていたので 32KB 前提の 9F00h からバイナリを置いていましたが、これには特に意図があったわけではありません。むしろ、5枚の画像をオンメモリに置いていた Tinyみずいろとは違い、あぴミクさんデモはPSGドライバと曲データだけがオンメモリで、あとは Exomizerの展開前の圧縮データを読むバッファ領域さえ確保できればいいということになります。

ということで、「ガッツはなかった」などと言いつつもやはり気になってしまったので調べてみると、以下のような感じです。

従来 9F00h から配置していた各バイナリとバッファについて、 16KB対応のため BASICプログラムのサイズも考慮して CA00h から配置してみたのですが、「アニメっぽくしよう!」と安直な考えで全画面 Exomizer 圧縮としたこのあぴミクさん

のデータが圧縮後で 2076バイトあり、これを読み出すためのバッファがギリギリ E000h からの VRAM領域にはみ出してしまう、という状態でした。いろいろと無駄っぽいすき間を削ってみたのですが、100バイト近くを空けるのは難しい状況でした。

が、よくよく考えると Exomizer の展開用バッファの 156バイトはどこにとっても問題ない、ということで次のように解決しました。

微妙にくどい感じですが、この 16KB対応のコードはこちら:

        org     0CA00h

VRAMTOP equ 0e200h

OPENTAPE equ 1a61h
READTAPE equ 1a70h
CLOSETAPE equ 1aaah

MAGIC equ 09ch

loadbin:
call OPENTAPE

ld hl,loadstart
ld bc,psgend-loadstart
call loadtape

ld hl,psgtop
ld de,datatop
call deexo

call START

ld hl,VRAMTOP
ld de,api1end-api1top
call loadrl

ld hl,loadbuf
ld bc,api21end-api21top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api22end-api22top
call loadrl

ld hl,loadbuf
ld bc,api2aend-api2atop
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,loadbuf
ld bc,api31end-api31top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api32end-api32top
call loadrl

ld hl,loadbuf
ld bc,api41end-api41top
call loadtape

ld hl,loadbuf
ld de,VRAMTOP
call deexo

ld hl,VRAMTOP+3072
ld de,api42end-api42top
call loadrl

; call CLOSETAPE
; ret
jp CLOSETAPE

skipmark:
call READTAPE
cp 0c3h
jr nz,skipmark
skipmark1:
call READTAPE
cp 0c3h
jr z,skipmark1
ret

loadtape:
call skipmark
loadtape1:
call READTAPE
ld (hl),a
inc hl
dec bc
ld a,b
or c
jr nz,loadtape1
ret

loadrl:
call skipmark
loadrl1:
call READTAPE
dec de
cp MAGIC
jr z,loadrl2
ld (hl),a
inc hl
jr loadrl4
loadrl2:
call READTAPE
dec de
ld b,a
call READTAPE
dec de
loadrl3:
ld (hl),a
inc hl
djnz loadrl3
loadrl4:
ld a,d
or e
jr nz,loadrl1
ret

org_cb00: ds 0cb00h - org_cb00 ;
org 0cb00h
datatop:

include "psgdrv.asm"

;org_a800: ds 0a800h - org_a800 ;
; org 0a800h
OBJSAD:
include "TellYourWorld2.asm"

dataend:

exo_mapbasebits: equ 0C200H
; defs 156 ; exomizer tables for bits, baseL, baseH

loadstart:
;deexo
;input: hl=compressed data start
; de=uncompressed destination start

include "deexo.asm"

loadbuf:

psgtop:
include "psgdrvdata16.asm"
psgend:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api1top:
include "api1-vram-rlcomp.asm"
api1end:

db 0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api21top:
include "api2-vram_1_exo.asm"
api21end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api22top:
include "api2-vram_2_rlcomp.asm"
api22end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api2atop:
include "api2a-vram_exo.asm"
api2aend:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api31top:
include "api3-vram_1_exo.asm"
api31end:

db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api32top:
include "api3-vram_2_rlcomp.asm"
api32end:

db 0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api41top:
include "api4-vram_1_exo.asm"
api41end:

db 0,0,0,0,0,0,0,0
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h
db 0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,0c3h,000h

api42top:
include "api4-vram_2_rlcomp.asm"
api42end:

end

変更内容としては以下だけですね。

  • ORG 文のアドレスを修正
  • PSG曲データを PSGドライバ直後に配置
  • Exomizer 展開用バッファについて EQU 文で C200h の VRAM アドレスを指定

実機確認は OSC京都の荷解きの関係でしばらくサボっていたのですが、以下のように問題ありませんでした。

増設メモリ工作で 16KBモード切替スイッチをつけたのが役に立った瞬間

Step 7: PSGドライバ+曲データ ストップビット短縮

このネタ自体は、16KB対応の作業する直前に以下のようにつぶやいていたものです。

このときは「原理上は難しくないはず」と思っていたものの、作った wav ファイルを切り貼りするツールを探して作業手順を試行錯誤する時間のほうが大変であろうという判断で見送っていました。

前述の 16KB対応の実機確認をしたさらに次の週に重い腰を上げて試行錯誤してみたところ、以下のようにあっさり動きました。

N60-BASIC の標準ではストップビット 3ビットということらしいのですが、少なくとも P6初代実機ではストップビット 2ビットでも CLOAD および ROM内ルーチン1A70h のテープ1バイト読み出しとも問題はないようです。

作業手順についてはちゃんとツイートをしていませんでしたが、ここでの wav ファイル操作は Windows 用の Sound Engine Free というツールを使いました。作成手順としてはざっと以下のような感じです。

  • ローダープログラムで読み込む P6データについて、 「Exomizer展開ルーチン+PSGドライバと曲の圧縮データ」までの部分とそれ以降の 2つに分割する
  • PSG演奏をしない前半のデータについては、「なんでもピーガー」で ストップビット 2ビット の設定で wavファイルを作成する
  • PSG演奏をしながらのストリーミングロードになる後半のデータについてはストップビット 7ビット、かつ、「空送り」「ヘッダマーク」をそれぞれ 0秒にして wavファイルを作成する
  • Sound Engine Free で 2つの wavファイルの不要な無音部分を削除してから結合する

「無音部分の削除」というのがやや乱暴な操作で、矩形波のどの部分でぶった切られるのかが心配でしたが、特に問題なくロードできているようでした。なんでもピーガーとしては「矩形波を出し切った後無音部分はレベル0のセンターに戻している」ということだと思います。

で、ストップビットを削ることでどれだけロードが速くなるかというと、

というわけで、動画撮影前にここまで頑張っておけばよかったかも? というくらいには短縮される結果になりました。

まとめ

読む側のことは二の次にして自己満足最優先で書いたので予想通り無駄にくどく長くなりましたが、前回のエントリ と合わせ、一通りまとめたいと思っていたことは書けたと思います。

むしろ、実際にデモプログラム作成にかかった時間(約1日半)よりこのエントリを書く時間のほうがかかっているという本末転倒なところもありますが、「これをやったら面白そうだ」と思うことが何よりのモチベーションである、といったところでしょうか。

一方で、1年以上経過した後でもこれだけの内容のまとめが書けたのは git のログと Twitter のつぶやきのおかげで、そういう意味では当初から狙い通りに記録を残せていた、とも言えます。

あぴミクさん以外にもまだまとめられていない作業がいろいろありますが、そのあたりも機会を見てぼちぼち書けたらいいなと思います。