続・過去の遺物

いまさらPC-9801
  小手先技巧講座[後編]

フッ……86なんて… 杉之原名人

 さて、後編です。

第5話:アナログパレットの利用

 PC-9801Vシリーズのあまり使われない機能の中に4096色のアナログパレットがあります。私も今までグラフィックツールソフトの他にこの機能を利用したソフトを見たことがありません。が、これも利用しない手はないというもの。ビジネスソフトに、ゲームソフトに、発想次第ではいくらでも応用できることと思います。例えばモノトーンの画面でも、郷愁を誘う微妙なセピア色のワークシートとか目に優しく気持ちの落ちつくグローブグリーンのテキストエディタなど、ちょっとした色使いでかなり面白い効果を出せる場合も多いと思います。また、ゲームなどの分野であっても、重厚なメタリック感覚のキャラクタやリアルな爆発シーンなど大きな効果を期待できます。

 それでは早速プログラミングに入りましょう。Vシリーズにはハードウェア的に2つのカラーモードが存在します。それぞれ8色グラフィックモード、16色グラフィックモードと呼ばれ、前者が従来機とのコンパチモード(8色パレット)で後者が今回利用する4096色パレットが使用できるモードです。「16色」というのは、Vシリーズ以降の機種はRGB3画面の他にI(インテンシティー)(=明度)の画面が追加され、同時16色表示が可能になったことに由来します。  PC-9801のパレットポートはかなり異常でプログラマ泣かせでありまして、I/Oアドレスが連続しているわけでもなければパレットコードの順に並んでいるわけでもありません。おまけにポートが4バイトしかなく、従来コンパチの8色パレットの場合は上下2つのニブル(4bit)に分割して使用しなければなりません。はっきり言って完璧に人を馬鹿にしています。  幸い4096色パレットの設定の場合は8色のときほど苦労はなさそうです。I/Oポートを【表1】に示します。

PORT DATA XPLANATION
 A8H
3210
0000irgb
パレットアドレス設定ポート(0~15)
 AAH
3210
0000GGGG
緑パレットの色の濃度(0~15)
 ACH
3210
0000RRRR
赤パレットの色の濃度(0~15)
 AEH
3210
0000BBBB
青パレットの色の濃度(0~15)
 A8H~AEHまでのI/Oアドレスは常に【表1】のようになっているのではなく、I/Oポート6AHに01Hを出力して16色グラフィックモードを選択することによって初めてこのようになります。
【表1】

 さて、パレットの設定の仕方ですが、わずか4バイトのI/Oポートを用いて4096色を表現するわけですから、従来のように3bitのパレットコードを出力するだけではとても済みません。まずA8Hにカラーコード(0=黒~15=明るい白)を出力し、次いでそのコードに割り当てる色をGRBそれぞれ16段階(4bit)でAAH,ACH,AEHに出力、設定します。ここで注意することは、VM2/VF2のように実際にはインテンシティーVRAMが載っておらず(オプション)16色モードに設定しても8色しか使用できない機種の場合は、出力するカラーコードが0及び9~15になることです。ちなみにカラーコード8はグレー(灰色)になります。

 何はともあれサンプルプログラムを見てみましょう。一目瞭然だと思います。このプログラムを走らせると画面をフェードイン・フェードアウトします。原理は簡単で、グラフィックVRAMの黒のカラーコードに暗色の白から明色の白へ15段階でパレットコードを割り当てるわけです。ま、リストを見てください。ただ、このプログラムはSLR社のオプティマイズアッセンブラで開発してありますので、マイクロソフト社のMASMだとエラーを出すかもしれません。そのときは適宜修正してください。

;------------------------------------------------------------
CODE    SEGMENT
        ASSUME  CS:CODE,DS:CODE
;
START:
        MOV     AL,8                  ;グラフィック画面表示。
        OUT     68H,AL                ;
        MOV     AL,0DH                ;
        OUT     0A2H,AL               ;
        MOV     AL,1                  ;16色モード選択。
        OUT     06AH,AL               ;
        MOV     CX,15                 ;パレットコード初期値。
FADE01: MOV     AL,8                  ;カラーコード8(黒)を設定。
        OUT     0A8H,AL               ;
        MOV     AL,15                 ;
        SUB     AL,CL                 ;
        OUT     0AEH,AL               ;BRGの順に出力。
        OUT     0ACH,AL               ;
        OUT     0AAH,AL               ;
        MOV     AL,5                  ;タイム・ウエイト。
        CALL    WAIT1                 ;
        LOOP    FADE01                ;
;----------------------------------------
        MOV     CX,15                 ;
FADE05: MOV     AL,8                  ;
        OUT     0A8H,AL               ;
        MOV     AL,CL                 ;
        OUT     0AEH,AL               ;
        OUT     0ACH,AL               ;
        OUT     0AAH,AL               ;
        MOV     AL,5                  ;
        CALL    WAIT1                 ;
        LOOP    FADE05                ;
;----------------------------------------
        MOV     AH,04CH               ;プログラム終了。
        INT     21H                   ;
;------------------------------------------------------------
WAIT1   PROC    NEAR                  ;タイムディレイ・サブルーチン。
        PUSH    AX
        PUSH    CX
        CMP     AL,0
        JE      WAIT1_99 WAIT1_00:
        MOV     CX,5000 WAIT1_01:
        NOP
        NOP
        NOP
        LOOP    WAIT1_01
        SUB     AL,1
        JNE     WAIT1_00 WAIT1_99:
        POP     CX
        POP     AX
        RET
WAIT1   ENDP
;-----------------------------------------------------------
CODE    ENDS
        END     START

第6話:ちょっとひと休み(SLR社製OPTASMについて)

 MS-DOS用のアッセンブラと言えばマイクロソフト社製のマクロアッセンブラである“MASM”が有名かつ主流のようです。このMASM、アメリカではすでにバージョン5.0が発売されており、その機能にも磨きがかかってきました。しかし、この独占を許すまじとばかりに、Z80ASMで有名な米SLR社がMS-DOSアッセンブラ界に殴り込みをかけてまいりました。その名もオプティマイズアッセンブラ“OPTASM”。そのスペックはというと、MASM V5.0完全上位コンパチブル。(実際には『完全』ではないようで、私もMASM用のとあるマクロライブラリを使用した際にエラーを出しまくられまして少々がっかりしました。おそらくバグでしょう。)本当に終わったのか?と思ってしまうような超高速性。(MASM V3.0と比べると、なんと15倍程度。)それでいながら条件ジャンプの自動最適化処理を行なうため、NEARジャンプとSHORTジャンプを使い分ける必要がありません。他にもローカルラベル機能やクロスリファレンサ内臓など、うれしい機能が満載されています。 私は一人で一気にプログラムを組む関係上分割アッセンブルは避けたいわけで、先日も依頼ソフトを完成させたところソースサイズが80Kバイトを越えてしまいました。これをMASM V3.0にかけますと、RAMディスクを使用しているにもかかわらずアッセンブルに2分以上もかかってしまいました。これにしびれを切らした私は大枚3万9800円(3万5000円にまけてもらいましたが)をはたいてOPTASMを購入したのです。結果は驚き、10秒足らずでアッセンブルが終了しました。その日からアッセンブルが楽しくなったのは言うまでもありません。現在もOPTASMを愛用していますが、ついこの前、ふとMASMの遅さをもう一度実感してみようと思い立ち、開発中の30K程度のソースをMASMにかけてみたのです。OPTASMだと3秒足らずで帰ってきますから、MASM(V3.0)だと40秒以上かかる計算になります。ところがそのとき私の見たものは、出るわ出るわのエラーの山でした。OPTASMでは“0 errors”と表示されるのに…………そう、オプティマイズの影響とV3.0のバグのために発生するものでした。これでおわかりでしょう。私はもうMASMを使えない身体になってしまったのです。  また、オブジェクトサイズにも違いが現れます。OPTASMの作成したOBJファイルはMASMのものよりも小さくなるのです。これはJMP命令が多く使用されていればいるほどこの差は大きくなります。OPTASMは不用なNOP命令を全く生成しないためです。MASMはNEARジャンプ(+127/-128)が2バイト命令、SHORTジャンプ(+32767/-32768)が3バイト命令であることからパス1実行時にとにかく3バイト確保し、パス2でNEARジャンプだと判断されると余分となった3バイト目にNOPを挿入するのです。  現在MASMのV5.0は日本国内では販売されていません。しかしこれの上位コンパチのOPTASM(V0.96)は国内代理店を通して現在販売されております。これは何と言ってもおすすめ品です。私個人にしてみれば腹が立つのですが、現在は定価33,800円と6000円安で販売されております。(私のように発売前から注文をしていると6000円損したことになってしまいました。ちなみに私の購入したフロッピーのシリアルNo.は「6」でした。おまけにタイトルはボールペンの直筆ですし。)

第7話:V30の使いこなし

 以前、零壱症候群第一号で「V30の話」をいたしました。ここではその続きを兼ねまして8086との差やアッセンブラプログラミングの高速テクニックなどを紹介します。

回数指定SHIFT命令

 8086にはCLレジスタにより回数を指定するSHIFT命令があることは周知の通りです。1ステップの命令で最大15回までの任意シフトができるわけですから、定数乗除算やビット編集にはもってこいの命令のように思われます。
ex.1
     MOV     CL,6
     SHL     DX,CL

 確かにソースレベルではまことに便利なのですが、オブジェクトレベルになると話は別です。実行速度がかなり遅くなるのです。8086CPUの場合、レジスタの1回シフトに要するクロックは最低の2クロックですが、回数指定シフトの場合は1回のシフトでも12クロックもかかってしまうのです。【表.1】を見てください。
【表.1】 (8086の場合)
  SHL  DX,1 ・・・・・ 2 Clock
  SHL  DX,CL ・・・・・ CL*4+8 Clock

 CLによる回数指定シフトの場合は、基本8クロックにシフト回数×4クロックを加えた分のクロックを消費します。ですから、ex.1の場合を計算すると 6×4+8=32 となります。これに対して1回シフト命令を任意回数繰り返した場合は 6×2=12 となり、6回シフトする場合は約2.7倍も高速になります。ちなみに2回シフトの場合は4倍、15回シフトの場合は約2.3倍と、共に回数指定をしない方がずっと高速です。8086の場合は回数指定SHIFT命令の使用は、高速性が必要なく、ビット操作の多用が必要なとき以外は控えるべきでしょう。
 以上の話はあくまで8086のときのことで、これがV30になるとかなり話は変わってきます。表.2を見ればわかるように、回数指定シフトのクロック数が減っています。
【表.2】 (V30の場合)
  SHL  DX,1 ・・・・・ 2 Clock
  SHL  DX,CL ・・・・・ CL+7 Clock
  SHL  DX,imm ・・・・ imm+7 Clock

 これから計算しますと、7回以上シフトするときは回数指定SHIFT命令を使用した方が良いことになります。ちょうど7回シフトした場合は共に14クロックですが、当然サイズの小さい回数指定シフトの方がよいでしょう。これはV30が定数指定によるシフトが可能なためです。8086のようにCLレジスタによる指定しかできないと、CLレジスタへ回数をロードする手間がかかってしまうので11回以上シフトしないと元がとれないことになってしまいます。7回以上一度にシフトすることはめずらしいとは思いますが、いちおう憶えておくことをおすすめします。

DEC,INC命令の不条理

 DEC,INC命令はZ80や6809などの8bitCPUにも存在するオーソドックスな命令です。もちろん8086にもあり、任意の汎用レジスタやメモリの内容を±1することができます。
 ところが8086の場合、この命令の実行サイクルが不条理なのです。Z80などでは当然8bitレジスタに対するINC,DEC命令の方が16bitのペアレジスタの場合よりも速いのですが、8086ではこれが逆になります。つまり、16bitレジスタのINC,DEC命令が2クロック、8bitレジスタのINC,DEC命令が3クロックと16bitレジスタの方が高速になっているのです。ゆえに、より高速にDLレジスタ(8bit)の値を±1したいときは、DHレジスタの破壊を考慮しながらDXレジスタをINC,DECすべきです。ただし、これは8086のときだけで、V30の場合は8bit/16bitどちらでも2クロックです。

ブロック転送 8086 V.S. V30

 インテル系CPUの特徴のひとつであるブロック転送専用命令は、8086ではストリング命令として用意されています。MOVS命令がそれで、バイト転送/ワード転送どちらを選択することも自由です。この命令の簡便性、高速性は使用経験ある方は実感済みのことと思います。 ではここで、8086のMOVS命令の高速性を証明するために、通常のMOV命令を使用した等価ルーチンとの所要クロックの差を見ていくことにしましょう。
MOVSを使わない場合

TLOOP:MOV    AX,[SI]   13(11)Clock
      MOV    [DI],AX   14(9)Clock
      INC    SI         2(2)Clock
      INC    DI         2(2)Clock
  +)  LOOP   TLOOP     17(13)Clock
  ---------------------------------
                       48(37)Clock
MOVSを使った場合

  +)  REP    MOVSW     20(10)Clock
  ---------------------------------
                       20(10)Clock
注) 括弧内の数字はV30の場合。
【表.3】
 まず断わっておくことにしますが、【表.3】のクロック数は2回目以降、最終回の直前までの場合を指しています。ちなみにMOVSを使った場合の第1回目は26(11)Clockとなり、MOVSを使わない場合の最終回は36(29)Clockとなります。
 さて、【表.3】をご覧になればおわかりのように、8086のストリング命令を使用した場合としなかった場合では実に約2.5倍もの実行速度の差が生じます。このことは脳裏にしっかりととどめておいてください。たかだか3ワード程度のデータ転送だからといって1ワードずつレジスタを介して転送するよりも、多少の初期設定をしてでもストリング命令を使用した方が高速な場合だって多々あるのです。V30ならばなおさらです。
 ここでV30のケースがでましたが、【表.3】の括弧内を見れば一目瞭然なように、ストリング命令を使用しない場合でも25%程度、使用した場合はちょうど2倍も8086より高速なのです。このストリング命令の高速化はV30の特徴のひとつでして、PC-9802がVシリーズになってからグラフィックを多用したゲームなどの速度がグ~ンとアップした理由のひとつはここにあるといってよいでしょう(VRAMアクセスのウエイトが下がったのも大きいですが)。V30プログラマの人はここんとこをうまく利用しましょう。
(※以上はメモリアクセスに際してウエイトがかからないという前提で計算したものです。)

ビットフィールド操作命令

 V30特有の命令にビットフィールド操作命令があります。これは他の16bitCPUにも類を見ないものです。
 通常、メモリにデータを格納する際やメモリからレジスタにデータをロードする際などは、バイト単位、あるいはワード単位で行なわれます。3bitや14bitといった中途半端なビット長でのロード/ストアはできないのが普通です。こういった場合は、AND,ORといった論理演算やSHL,RORなどのシフト命令を使用して実現しなければなりません。
 V30のビットフィールド操作命令を使うと、1~16bitの任意ビット長のデータをメモリ空間上の任意のビット位置に格納したり、またレジスタにロードしたりすることができます。言うなれば「ビット単位のブロック転送命令」です。これはグラフィックVRAMを操作する際にはこの上なく高速かつ便利であります。例えばPC-9801のN-88BASICのPUT@文,GET@文を考えてみてください。任意の大きさのグラフィックデータをドット単位の任意の画面位置に表示、あるいは画面から取り込みを行なうわけですから、結構複雑なビット操作が必要になることはご想像いただけることと思います。

 右の【図.1】は青VRAMに対してビットフィールド操作命令のINS命令を実行したときの様子です。この例ではAXレジスタの下位5bitを0A8024H番地の下位2bitから0A8025H番地の上位3bitにかけて転送します。INS命令を使用すると、
【図.1】
 (MOV  AX,10101B    ;AX=WRITEデータ)
  MOV  DX,0A800H    ;
  MOV  ES,DX        ;ES=転送先セグメント
  MOV  DI,24H       ;DI=BYTEオフセット
  MOV  DL,6         ;DL=bitオフセット
  INS  DL,5         ;5ビット転送
となります。これを8086のコードで書くと、
 (MOV  AX,10101B    ;AX=WRITEデータ)
  MOV  DX,0A800H    ;
  MOV  DS,DX        ;DS=転送先セグメント
  MOV  DI,24H       ;DI=BYTEオフセット
  ROR  AX,1         ;shift right 3bit
  ROR  AX,1         ;& copy LSB to MSB
  ROR  AX,1         ;& repeat 3times.
  AND  AL,00000011B ;mask high 6bit.
  OR   [DI],AL      ;DATA(2bit) WRITE.
  AND  AH,11100000B ;mask low 5bit.
  OR   [DI+1],AH    ;DATA(3bit) WRITE.
このようになってしまいます。ここでは2バイトにまたがる例でしたが、これが3バイトにまたがる場合だとこんなもんでは済みません。INS命令を使用すれば第2オペランド(アンダーライン部分)を例えば14にするだけで済みますが、8086コードだとさらに複雑になってしまいます。速度は言うに及ばず、平均でも3~5倍高速化されます。
 余談ですが、PC-8801VAのCPUであるμPD9002はV30同様、NECオリジナルの8086コード上位コンパチの機能を持っていますが、V30ほどの機能は有していません。言い方を変えれば、V30からいくつかの機能を削減し、8080エミュレート機能をZ80エミュレート機能に変えたCPUであるということができます。では、どのような機能が削減されたかというと、このビットフィールド命令やBCDストリング演算命令なのです。

STOS命令の高速性

 STOS命令も知らないと損をする命令のひとつでしょう。どのくらい速いかというと、
共通部分

      XOR    AX,AX      3(2)Clock
      MOV    DX,2       4(4)Clock
      MOV    DI,AX      2(2)Clock
      MOV    CX,4000H   4(4)Clock
STOSを使わない場合

LB1:  MOV    [DI],AX   14(9)Clock
      ADD    DI,DX      3(2)Clock
  +)  LOOP   LB1       17(13)Clock
  ---------------------------------
                       48(37)Clock
STOSを使った場合

  +)  REP    STOSW     13(6)Clock
  ---------------------------------
                       13(6)Clock
 これくらい速くなります(【表.4】)。例によってクロック計算は初回と最終回を除くループ内部の合計、括弧内はV30での値です。
 この場合、メモリを32Kバイト0でクリアするルーチンになります。ご覧のようにSTOS命令をREPプリフィクスと併用して使用すると8086で2.6倍、V30で4倍も高速化します。
 STOS命令はメモリを一定のデータで埋める用途以外にはあまり使い道はないように定説化されていますが、数列や演算テーブルの作成など応用はまだまだあると思われます(リピートプリフィクスは使用できませんけど)。特にV30の場合はそれによってかなり高速化が図れます。
(※これもメモリアクセスに際してウエイトがかからないという前提で計算したものです。)
【表.4】

ビット操作命令

 Z80のプログラミングをされたことのある方はビット操作命令のありがたみがおわかりになると思います。任意1ビットのセット,クリア,テストが1ラインで実現できるのですから。8080でこれをやろうとすると大変。シフトやAND,ORの連発になります。実は8086も8080と同様で、任意のビットを操作するには論理演算の連発にならざるを得ません。
 ところがV30にはZ80以上に完備したビット操作命令が用意されているのです。セット,クリア,テストの他に反転(NOT)が追加され、アドレッシングも豊富になりました。8086の未定義コード“0FH”で命令を拡張しています。
 ビット操作命令はBASICのPSET文を実現するのに便利そうですが、他にもテキストのアトリビュートをいじったり、不揮発性メモリスイッチの設定を変えたり、また効率の良いデータ形式に、と応用は限りないと思います。また、実行速度も申し分なく高速です(レジスタの場合3~5クロック)

ブロックPUSH/POP

 V30特有命令の中でも光っているもののひとつです。V30ニモニックでは、
  push R
  pop  R

と表現します。わずか1ラインで8ライン分のPUSH/POPを行なうのですから嬉しさいっぱい夢いっぱいです。8本の汎用レジスタをもれなく一気にスタックに放り込んだり戻したりしてくれます。さて問題の速度ですが、【表.5】を見てみましょう。

 PUSHもPOPも8回行なえば64クロックを消費しますから、PUSH_Rで1.8倍、POP_Rでも1.5倍は高速になります。また、これだけ速度に余裕があると、なにも全レジスタをPUSH/POPする場合のみに使用する必要もありません。例えば、あるサブルーチンで5本の汎用レジスタをPUSHしてPOPする必要があるとき、合計10回のPUSH/POP=80クロックのクロック消費があるわけです。しかし、PUSH_R+POP_R=78クロックと、16回のPUSH/POPがあるにもかかわらずより高速です。つまり、5本以上のレジスタの退避復元が必要なときはブロックPUSH/POP命令を使用した方が、高速かつ省メモリということです。決して忘れてはいけません。
 
 PUSH   R ・・・・・・ 35Clock
 POP    R ・・・・・・ 43Clock

 PUSH   AX ・・・・・・ 8Clock
 POP    AX ・・・・・・ 8Clock
 

(AXに限らず他のレジスタも同様)

【表.5】
(※これもメモリアクセスに際してウエイトがかからないという前提で計算したものです。)

最終話:特に脈絡ありません

 先日、NECからV30の改良型と呼ぶべきV33が発表になりました。16MHzで2.8MIPS、10MHzのV30に比べ4倍の処理能力といいますからなかなかのものです。物理アドレス空間も16Mバイトになり、内部回路も完全ワイヤードロジックになりました。これによってインテルとの訴訟問題も解決するとのことです。今秋発表のNECのPC-9801シリーズの新型にもおそらくV33が載ってくることでしょう。今から楽しみですね。(「心から……」ではないけど)

※この原稿は1990年頃に書かれたものです。