Wednesday, February 3, 2021

[PC88] C framework for the NEC PC8801 - Part 2, Simple graphics

The following project (which is very similar to what is found in the repository) demonstrates the basics of drawing on the PC-88. 

#include "pc88-c.h"
#include "img_b.h"
#include "img_r.h"
#include "img_g.h"

PlanarBitmap layeredImage = { 
    img_r, img_g, img_b, 248/8, 100

void main()


    // Test yellow ALU draw (V2)

    vu8* vp = (vu8*)0xc100;
    *vp = 0xff;


    // Toggle, then test blue ALU draw (v2)
    vp += 0x100;
    *vp = 0xff;
    // GVRAM copy mode on
    SetIOReg(EXPANDED_GVRAM_CTRL, (u8)(bit(7)|bit(4)) );
      ld a,(0xc200)
      ld (0xc201),a
    // (copies blue byte)

    // Planar bitmap (V1) draw and individual pixels
    DisableALU(); // ALU off
    PlanarBitmap* pb = &layeredImage;    
    DrawPlanarBitmap(pb, 20, 10);
    SetPixel(360, 180, CLR_BLUE);
    SetPixel(361, 181, CLR_CYAN);
    SetPixel(362, 182, CLR_GREEN);
    SETBANK_MAINRAM() // must reset after draw!



        if(GetKeyDown(KB_RETURN)) print("HI!\x00");

(Updated 2/5/2021)

At the top, img_b, img_g, and img_r are the resultant const char arrays produced from (using ishino.png). 

These are referenced in the definition of layeredImage, along with its width (divided by 8, for byte width) and height in pixels. This is drawn in V1 mode, described below.

LINE_POINTER and SCREEN_POINTER are initialized to SCREEN_TXT_BASE and 0, respectively. This is done in __init(), within pc88-c.h. 

When drawing, we want interrupts disabled, so we call IRQ_OFF 

First, we'll try drawing a line of 8 pixels (in VRAM, this equals 0xff) in V2 mode, using the ALU.

Before calling EnableALU(), you must first call ExpandedGVRAM_On(). If you enable ALU expanded mode without swapping to expanded GVRAM, the system will think you want to write to Main RAM instead - because expanded mode is off, it defaults to independent mode. 

Once that's done, you load your pen color into EXPANDED_ALU_CTRL*. Then any write to 0xC000+ will write that color to the screen, in a linear order of 1bpp pixels - for instance, 0xFF is a string of 8 pixels, 0b10101010 is every other pixel, etc. 0xC000 is the top left single pixel of the screen. (Remember that PC-88 pixels are double-high!)
*See the "deeper explanation" below

When done writing to GVRAM, you need to disable it with ExpandedGVRAM_Off() or SETBANK_MAINRAM(). Expanded mode (aka ALU) writes require the former, independent mode writes require the latter. You won't be able to access the Main RAM page, and overall program performance will decrease, until Main RAM is banked back in.

The "simple" explanation of Expanded mode/ALU writing:

1. Disable IRQ / wait for VBlank
2. Enable Expanded GVRAM access mode (set the high bit of I/O register 0x35)
3. Enable the ALU (set bit 6 of I/O register 0x32, leaving the rest unaffected)
4. Set I/O register 0x34 with the palette number to write
5. Write pixel data to VRAM (0xC000~).
6. Disable Expanded GVRAM access mode
7. Re-enable IRQ

In PC88-C this looks like:

vu8* vp = (vu8*)0xc100;   // initialize vram pointer somewhere
*vp = 0b10101010;         // write every other pixel

EXPANDED_ALU_CTRL has a deeper purpose than simply setting the active palette, however. The method used when writing to GVRAM through the ALU depends on the value in I/O register 0x35 (so named EXPANDED_GVRAM_CTRL). 

The actual bit definitions are:
bit   7    6    5    4    3    2    1    0
          GV2H GV1H GV0H     GV2L  GV1L GV0L

Where GV0, GV1 and GV2 represent each respective graphic VRAM plane, and H and L represent the high and low bits of the following functions for each plane:
 H L
  0 0 - Bit reset 
  0 1 - Bit set (OR)
  1 0 - Bit invert (XOR)
  1 1 - Ignore / noop

Take a moment to understand what this means in practice. If all three of the lower bits are set, that means any bit written to VRAM through the ALU will set the bits on all 3 graphics planes. A white pixel will be written. Hence, loading EXPANDED_ALU_CTRL's lower bits with the palette value has the same effect as changing the active pen color. 

Loading only the upper bits of the register with a palette value will flip the bits on that plane. SetIOReg(EXPANDED_ALU_CTRL, CLR_YELLOW << 4), for instance, will change black pixels to yellow, and yellow pixels to black (values 000 ^ 110). To determine what other colors would change to, you would have to XOR the present color on that plane. This has limited application - one, to erase data you know is already there, and two, to perform quick palette swaps. 

Loading both the upper and lower bits prevents the ALU from writing to that plane. If writing to GVRAM is not working when ALU is enabled, ensure I/O register 0x34 is set to the proper value.

bit   7    6   5-4   3   2   1   0
     GAM   -   GDM   -   GC  RC  BC

GAM : 0 - enable Main RAM, 1 - enable GVRAM
GDM: Graphic Data Multiplexer control
   0 0 - ALU write via port 34h
   0 1 - Write all planes via VRAM copy
   1 0 - Transfer plane R to plane B*
   1 1 - Transfer plane B to plane R*
GC, RC, BC - Bit comp data for each of the 3 GVRAM planes**

*Used in hi-res B&W mode
**Used in multiplexer modes 00 and 01

Generally, this register will be at 0x80 when writing to GVRAM and 0 during normal program execution. When bit 7 is set, data that is loaded into the accumulator from GVRAM is not actual data, but instead as I understand it, a VRAM buffer. What is written is also not actual data, but it instead moves that buffer to the desired location. (On this point it is difficult to find a detailed explanation).

The bottom 3 bits of this register act as a mask when loading ALU data from VRAM. Mind that the value loaded into the accumulator is _not_ what is written to VRAM, regardless of the mask. This is for arithmetic operations only, i.e. determining the color that needs to be written to the ALU to change the pixel to a specific color. (The mechanism to do this at the moment is beyond me)

V1 Mode - Pixel and PlanarBitmap draw:
You can mix V1 and V2/ALU draw modes, as shown in the main.c example. To write V1 mode graphics (independent GVRAM plane access):

Turn off the ALU, if it is on.

    PlanarBitmap* pb = &layeredImage;    
    DrawPlanarBitmap(pb, 20, 10);
If you are drawing a bitmap, initialize a pointer to the defined struct. 
This is a macro that (over)writes the bitmap on all 3 VRAM planes.

    SetPixel(360, 180, CLR_BLUE);
Paints an individual pixel.

DrawPlanarBitmap() and SetPixel() both change the active page of memory bank 0xC000~0xFFFF to the corresponding RGB plane. After calling them, you must call SETBANK_MAINRAM() before returning to normal program execution.

And that's all there is to it! V1 mode is simple, but it is ineffective and slow - V2 is more complex, depending on your needs, but can cut down render time by over half.

The code above in the example paints a bitmap in V1 mode and several methods of plotting pixels in V2 mode. 

Part 3 will detail V1 mode's layered palette technique, software sprites, and more.

[PC-88] C framework for the NEC PC8801 - Beginnings

So, you wanna make a game for an old Japanese computer virtually nobody in the west cares about? 


I built this from my own knowledge of Python, C, and z80 assembly. (Thanks to the SDCC core!) I had to start from scratch, but a very large portion of the knowledge I gained came from these two Japanese-only sites on PC88 assembly:

Bookworm's Library

Maroon Youkan

I will attempt to translate and explain what I've learned as best I can. If you have questions and are serious about PC-88 C development, consider joining the @RetroDevDiscord and helping the few of us there are. 

I use the Windows 10/x64 distribution of SDCC, version 4.0.0. Details on the build process further down.

PC88-C on Github

PC88-C base file list:


(*makepc88.bat is the primary build script.)

As with most C projects, main.c contains the project code. SDCC generally only likes one primary C file at a time, so try to put all your code in main.c or in files included by main.c.

Due to my unfamiliarity with the inner workings of SDCC, void main() must be the first actual code entry in the built file. The crt0-equivalent, IPL.BIN, points directly to code start at $1000, which is where the autoloader targets (further information below).

Most of the command documentation, explanation on registers, etc. is in this file. There is still a lot that needs to be documented, but a lot of information is here. Overview:

These are fairly self-explanatory. String should allow you to define character arrays as you are accustomed. bit() allows you to get bit values without doing the hex in your head.

PlanarBitmap is a bitmap definition required for DrawPlanarBitmap(). 'r', 'g' and 'b' are pointers to raw image data for each of the color planes, and 'w' and 'h' are width (in tiles, or in pixels divided by 8) and and height (in pixels). 

The PC-88's video memory is divided into three, one-bit RGB color planes. The original PC-88 models, referred to as 'V1' mode, had to access the RGB planes independently, one at a time. The resulting 3-bit color is displayed on the screen. By default, plane 0 is blue, plane 1 is red, and plane 2 is green. So, to get a "cyan" color, you write a 1 to the blue and green planes, and a 0 to the red plane.

You can change the default palette values for certain effects, such as simulated foreground- and background-layers by limiting overall palette color. This technique is explained in part 3, and functions well with 1-bit software sprites.

However, given the 4MHz CPU speed of the V1 models and the relatively slow bus speed, this is not effective for high fidelity games. V2 mode came not much later, and the ALU expansion module.

The ALU (Arithmetic Logic Unit, thanks rikkles) is the key to improving game performance. With the control of three extra registers, you can write to all 3 VRAM planes simultaneously. The explanation given of utilizing the ALU was extremely complex and took me several days to fully understand, so don't feel bad if it doesn't click right away.

Before getting into that, here are the function outlines:

static inline void putchr(u8 c) (and putchr40)
Puts a character on the screen at location global SCREEN_POINTER. SCREEN_POINTER must be initialized in main() at SCREEN_TXT_OFFSET (or your desired location) before using this function or print().

void print(String str) (and print40)
Prints a string to SCREEN_POINTER

u8 ReadIOReg(u8 r)
Returns the value in a given I/O register, if that register can be read. See the H file for detailed definitions.

void SetIOReg(u8 r, u8 v)
Sets the value of I/O register 'r' to single byte value 'v'.

void SetTextAttribute(u8 x, u8 y, u8 attr)
Adds an attribute byte-pair to row 'y', beginning at position 'x'. The text attribute macros are explained below.

void ClearAttributeRam()
Resets the entire screen's text attributes to their default (80/Color mode) defaults.

void SetCursorPos(u8 x, u8 y) (and SetCursorPos40)
Moves SCREEN_POINTER to 'x', 'y' on-screen, where x=(0, 79) and y=(0, 25)

void Wait_VBLANK()
Proper Vblank ASM routine. Waits for VBL signal from the CRTC, then waits until its clear before returning to ensure we are *inside* vertical blank. 

void DrawPlaneBMP(const u8* img, u8 plane, u16 x, u16 y, u8 w, u8 h)
Draws raw, single color plane image data 'img', to color plane 'plane', of width/8 and height 'w' and 'h', at pixel offset 'x', 'y'. Warning, not pixel perfect - x offset is tied to 8-pixel boundary. This is macroed three times with DrawPlanarBMP, defined below. This will toggle the GVRAM planes for you, but SETBANK_MAINRAM() must be called afterwards to re-enable the Main RAM page.

void SetPixel(u16 x, u8 y, u8 c)
Pixel-perfect plot a single pixel at 'x', 'y' of color 'c', where bits 0-2 of 'c' represent the VRAM color planes. Default colors are macroed with the prefix CLR_.
SetPixel() will toggle GVRAM dependant on the color, but SETBANK_MAINRAM() must be called afterwards to re-enable the Main RAM page. Note that to access individual GVRAM pages, expanded GVRAM must be off.  

bool GetKeyDown(u8 SCANCODE)
Returns true if the macro 'SCANCODE', prefixed by KB_, is presently down; else returns false.

static inline void EnableALU()
static inline void DisableALU()
Sets I/O register 0x32 to 0xC9 if enabled and 0x89 (IPL defaults) if disabled. Must be called after calling ExpandedGVRAM_On(). (V2 only)

static inline void ExpandedGVRAM_On()
static inline void ExpandedGVRAM_Off()

Sets I/O register 0x35 to 0x80 if enabled and 0 if disabled. This is required before calling EnableALU(), otherwise writing through V2 mode (via the ALU) will not work. V2 only. Note that on boot, Expanded GVRAM is off.

void DiskLoad(u8* dest, u8 srcTrack, u8 srcSector, u8 numSecs, u8 drive) __naked 
Same assembly routine as is in IPL.BIN. e.g.
DiskLoad((u8*)0x4000, 1, 7, 40, 0);
Loads 40*256 bytes from track 1, sector 7, to RAM at 0x4000. 

void __init()
Sets up the screen pointer and calls main(). Should generally be left alone. :)

Macro definitions:

#define SetBGColor(c) SetIOReg(0x52, c << 4);
#define SetBorderColor(c) SetIOReg(0x52, c); // PC88mk2 and prior

Sets the background color for color text mode. Border color function was removed from most later models.

#define SETBANK_BLUE() SetIOReg(0x5c, 0xff);
#define SETBANK_RED() SetIOReg(0x5d, 0xff);
#define SETBANK_GREEN() SetIOReg(0x5e, 0xff);
#define SETBANK_MAINRAM() SetIOReg(0x5f, 0xff);
Toggles GVRAM banks over 0xC000 ~ 0xFFFF. SETBANK_MAINRAM() must be active during normal program execution - having any VRAM bank active can slow programs down.

#define DrawPlanarBitmap(pb, x, y) 
Macro for drawing a PlanarBitmap struct (V1 mode).

#define COLORMODE_SET(color, semigraphic) 
Defines a SET type attribute in Color Text mode, where 'color' is 0-7 and 'semigraphic' is 0 or 1.

#define COLORMODE_ATTR(underline, upperline, reverse, blink, hidden) 
Defines an ATTR type attribute in Color Text mode, where all parameters are either 0 or 1.

#define BWMODE_ATTR(underline, upperline, reverse, blink, hidden) 
#define ATTR_BW_SEMIGRAPHIC 0b10011000
Defines an attribute for B&W Text mode. For B&W mode, use the ATTR_BW_SEMIGRAPHIC macro to enable semigraphic mode.

#define IRQ_OFF __asm di __endasm;
#define IRQ_ON __asm ei __endasm;
#define HALT __asm halt __endasm;
Convenience macros. When swapping RAM/GVRAM pages and reading/writing IO ports, remember to disable IRQs. HALT and BREAKPOINT are simply easy ways to aid in debugging without a full debugger.

Takes the place of hex2bin.exe. This is taken from Intel's official open source library. Requires Python 3 and python module "intelhex", obtainable via 'pip install intelhex'. This is already integrated in makepc88.bat.

Simple tool of convenience - overwrites 1 byte in the given file with the given value, e.g.
 python3 tools/ ipl.bin 0x2f 0x50
Will change the number of sectors loaded by the autoloader in IPL.BIN (the byte located at 0x2f) to 0x50. This tool is not utilized in the chain, but is there for ease of use.

Replaces the eponymous D88SAVER.EXE. D88SAVER was taken from the above websites and was useful in generating a blank d88 file, and injecting files into it (including IPL.BIN). This serves the same purpose, and is used in the same way - with the added feature that it will create the disk file passed as argument if it does not already exist.
(Note this only supports 2D, or 375kB disks for now.)
This is integrated into makepc88.bat. 

Converts a standard indexed PNG to its corresponding R, G and B bitplanes, then writes them to C-style header files in const char array format. The header file byte data can then be drawn directly by DrawPlaneBMP(). Requires the modules 'Pillow' and 'numpy'.

This file is assembled using ASW assembler from ipl.z80 and disk.z80. It contains a short routine that sets up the screen and stack pointer, and has a minimal disk access routine for loading from floppy. 

Boot process on the PC-88 is roughly:
- Is the 'boot from floppy' dipswitch on?
-- If YES, copy the 256 bytes from cylinder/head/record 0/0/1 from the inserted disk into RAM at 0xC000. Then, jp $C000.
--If NO, check the TERMINAL and BASIC dipswitches and boot to ROM.

The 256 bytes within IPL.BIN are therefore called from within RAM at org $C000. This is clearly not enough to run an entire game, so the routine then copies N bytes, the value of which is located at offset [0x2F] in the IPL, to the location at offset [0x38-0x39].
By default, [0x2F] = 0x4F and [0x38-0x39] = 0x00 0x10, meaning 79 sectors (2f = 79 * 256 bytes, or ~20kB) is copied from disk to RAM starting at little-endian address $1000. Feel free to change these using if you don't have ASW to re-assemble.

Performs the following:
- Deletes app.d88
- Creates a blank 2D d88 with
- sdcc -mz80 --code-loc 0x1000 --stack-loc 0x0080 --data-loc 0x0100 --fomit-frame-pointer --no-std-crt0 src/main.c
- Converts the resulting IHX to BIN format using
- Inserts IPL.BIN and the resultant MAIN.BIN into app.d88 at sectors 0 0 1 and 0 0 2
- Prints a (rough) outline of the memory map
- If "main.bin" (or the resultant filename) is passed as an argument, it will inform you if the file is larger than the default number of sectors copied in by the autoloader. (change line 6 in the bat, set usedsec=nn if you change this value in the IPL). 
- Launches the emulator (you must change this to your own emulator path)

This finalizes the explanation of the files included in the repository.

Part 2 will cover basic text, pixel and bitmap drawing in both V1 and V2 modes!