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()

{   
  
  IRQ_OFF

    // Test yellow ALU draw (V2)
    ExpandedGVRAM_On();     
    EnableALU();       
    SetIOReg(EXPANDED_ALU_CTRL, CLR_YELLOW);

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

    ExpandedGVRAM_Off();

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

    // 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!

    IRQ_ON

    while(1)

    {
        Wait_VBLANK();
        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 png288.py (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.

!!! IMPORTANT !!!
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:

IRQ_OFF
ExpandedGVRAM_On();
EnableALU();
SetIOReg(EXPANDED_ALU_CTRL, CLR_YELLOW);
vu8* vp = (vu8*)0xc100;   // initialize vram pointer somewhere
*vp = 0b10101010;         // write every other pixel
ExpandedGVRAM_Off();
IRQ_ON


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:
EXPANDED_ALU_CTRL (34h)
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.

EXPANDED_GVRAM_CTRL (35h)
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):

    DisableALU();
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.

    SETBANK_MAINRAM()
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.


No comments:

Post a Comment