blog:pc98_devcode

NEC PC-98 C Code Examples

So you've got your NP21Kai PC-98 emulator up and running, or you've resurrected an ancient NEC PC-9801 or PC-9821 and you've decided you want to do some C programming for it.

Where do you start? Well, first, get a working development environment up and running. You can get one here on the development tools page.

Next you will want to figure out how to do some interesting things with the hardware, as otherwise it's just a normal command line DOS machine….

After compiling your code, your application will need the following to run:

  • A VCPI memory mapper running (EMM386, VEMM486 or similar)
  • A DPMI server running
  • A copy of GO32-V2.exe in the same directory as your application

The VCPI memory mapper should be started at boot, but the DPMI server only needs to be run at the time your application starts.

A copy of DPMI.EXE and GO32-V2.EXE are including on the development tools page.

Determine If Running on PC-98/DOS or PC/DOS

The macros ISPCAT() and ISPC98() are introduced in the patched version of the DJGPP C library. They can be used to check if your exe is running on either a IBM PC, or NEC PC-98.

The macros take one parameter, a global; crt0_mtype, which is set at call time with the machine type.

Example use follows below:

#include <stdio.h>
#include <libc/pc9800.h>
 
int main(){
	int m;
	m = __crt0_mtype;
	if(ISPCAT(m)){
		printf("Hello, PC world\n");
	}
	if(ISPC98(m)){	
		printf("Hello, PC98 world\n");
	}
	return 0;
}

There are several generations of custom graphics hardware on the PC-98:

  • PC-9801, GDC: 640×400, 8 colour, 96KB VRAM (later 192KB)
  • PC-9801VF, GC: 640×400, 16 colour, 96KB VRAM (later 192KB)
  • PC-9801VX, EGC: 640×400, 16 colour, 256KB VRAM, added raster operation functions, bitblt and bit shift functions
  • PC-9821AP, PEGC: 640×400, 256 colour, 512KB VRAM, added packed-pixel and planar addressing modes

The systems are generally backwards compatible, so a PC-9821 should be able to run software designed for early PC-9801 machines… cpu speed dependent, of course.

My focus is on the later PEGC, 256 colour systems, since (a) this is the hardware I actually own, and (b), its most similar to DOS VGA or modern framebuffer hardware.

In order to access the PEGC linear framebuffer for packed-pixel mode, the PC-98 BIOS should have the following option set:

  • “16MB System Space” or “Use memory hole at 15-16MB” or “Use memory region at 16MB”

The reason for this is that the PEGC hardware locates its framebuffer in the region between 15MB and 16MB in the x86 address space. From C you can detect whether this has been set or not by checking port 0x43B.

int vramHasMemoryHole(){
        // Checks if the memory hole is set at 16MB to enable us to set the VRAM framebuffer
        int x;
        x = inp(0x043b);
 
        printf("x: %x\n", x);
 
        if (x & 0x04){
                // Memory hole not present
                return -1;
        } else {
                // Memory hole present
                return 0;
        }
}

If the memory space at 16MB is being used for RAM, then bit 2 will be set. If it is free to use to map the VRAM framebuffer, than it should be unset.

The difference in behaviour can be seen on an emulated PC-9821 system using NP21Kai, where printing the binary representation of the value of 0x43B can show the following:

Alternatively, on a machine on which the BIOS setting has been enabled, the value of 0x43B should be clear:

If the memory hole at 16MB is not present, or more than 16MB of RAM is present, then an alternative memory region needs to be used at 4095MB.

In conclusion:

  • VRAM can be accessed as a single linear framebuffer at 16MB, if the BIOS option “Memory hole at 16MB” is enabled.
  • VRAM can be accessed as a single linear framebuffer at 4095MB, if the BIOS option “Memory hole at 16MB” is disabled or >=16MB of RAM is fitted.
  • VRAM must be accessed in a series of 32KB banks (I am not going to cover this method) if neither of the above are true, or the linear option is not enabled in your code.
DPMI Server Differences

In order to map IO memory (for example, our linear framebuffer, above) to a region which is addressable by the running application, the DPMI call __dpmi_physical_address_mapping() needs to be made, and this requires a running DPMI server.

Here is the results of that call on various PC-98 DPMI servers:

Source Exe Size Reported DPMI Version Result
Epson DOS 5.00 DPMI32.EXE 64752 1.00 Call returns -1. Error.
MS-DOS 5.00 DPMI.EXE 405566 1.00 Call returns -1. Error.
MS-DOS 6.22 DPMI.EXE 63792 1.04 Call returns 0. Success.

Enabling 256 Colour Mode

This fragment enables 256 colour packed-pixel mode on compatible PC-9821 hardware. IO ports in both the normal DOS memory space and in far memory need to be written to.

This is accomplished with outportb() (included in dos.h) for ports in the low memory range, and with _farpokeb() (included in farptr.h) for ports in the extended memory range.

// 256 colour packed-pixel mode
        #include <dos.h>
        #include <sys/farptr.h>
 
        #define PEGC_MODE_ADDR	        0x6A    // Normal address space
        #define PEGC_PIXELMODE_ADDR	0xE0100 // Register in PEGC space in far memory
        #define PEGC_BPPMODE_16c	0x20	// Enable 16 colour mode
        #define PEGC_BPPMODE_256c	0x21	// Enable 256 colour mode
        #define PEGC_PIXELMODE_PACKED	0x00	// Set for packed pixel mode
        #define PEGC_PIXELMODE_PLANAR	0x01	// Set for planar pixel mode
        #define PEGC_BPPMODE_DUAL_PAGE	  0x68	// Enable single graphics page (in 640x400)
        #define PEGC_BPPMODE_SINGLE_PAGE  0x69	// Enable single graphics page (in 640x400 or 640x480)
 
	outportb(PEGC_MODE_ADDR, 0x07);
	outportb(PEGC_MODE_ADDR, PEGC_BPPMODE_256c);
	outportb(PEGC_MODE_ADDR, PEGC_BPPMODE_DUAL_PAGE);
	outportb(PEGC_MODE_ADDR, 0x06);
 
	// Enable Packed Pixel mode
	_farpokeb(_dos_ds, PEGC_PIXELMODE_ADDR, PEGC_PIXELMODE_PACKED);

Enabling Linear Framebuffer

As mentioned earlier, by default, the VRAM address space is split into 32KB banked chunks, which need to be swapped in and out of the low memory address space (<1MB) in turn. However, the PC-9821 has that elusive linear framebuffer mode that appears to be so difficult to access.

The following fragment enables linear framebuffer access on supported PC-9821 hardware:

// Enable linear framebuffer at 16MB and 4095MB
        #include <dos.h>
        #include <sys/farptr.h>
 
        #define PEGC_FB_CONTROL_ADDR	0xE0102 // Sets whether the linear VRAM framebuffer is at F00000h-F7FFFFh (set with 0x01) or not (0x00) 
        #define PEGC_FB_ON	0x01	// Enable PEGC VRAM linear framebuffer
        #define PEGC_FB_OFF	0x00	// Disable PEGC VRAM linear framebuffer
 
	_farpokeb(_dos_ds, PEGC_FB_CONTROL_ADDR, PEGC_FB_ON);

The code is just that one line, but the following should be noted:

  • If you have less than 16MB of RAM - the framebuffer should appear at 0x00F00000
  • If you have > 16MB of RAM and have the BIOS option set “Do not use memory hole at 16MB as memory” - the framebuffer should appear at 0x00F00000
  • If you have > 16MB of RAM and have not set the BIOS option “Do not use memory hole at 16MB as memory” - the framebuffer should appear at 0xFFF00000
  • In all cases the framebuffer should also appear at 0xFFF00000

You can use the following code to detect if the memory hole at 16MB is in use or not, and alter the framebuffer address accordingly:

        #include <dos.h>
 
        #define PEGC_FB_LOCATION_LOW	0x00F00000 // The VRAM framebuffer is located at 16MB if the 15-16MB hole is present
        #define PEGC_FB_LOCATION_HIGH	0xFFF00000 // The VRAM framebuffer is located at 4095MB if the 15-16MB hole is in use
        #define MEMORY_HOLE_CHECK_ADDR	0x043B	// If bit 2 is set at this address, then the memory hole at 15-16MB is not present (it is being used by RAM)
 
        int x;
	x = inportb(MEMORY_HOLE_CHECK_ADDR);
 
        if (x & 0x04){
		printf("%s.%d\t Memory hole at 16MB is not available\n", __FILE__, __LINE__);
		printf("%s.%d\t Using VRAM framebuffer at 0x%x\n", __FILE__, __LINE__, PEGC_FB_LOCATION_HIGH);
		pegc_fb_location = PEGC_FB_LOCATION_HIGH;
	} else {
		printf("%s.%d\t Memory hole at 16MB is available\n", __FILE__, __LINE__);
		printf("%s.%d\t Using VRAM framebuffer at 0x%x\n", __FILE__, __LINE__, PEGC_FB_LOCATION_LOW);
		pegc_fb_location = PEGC_FB_LOCATION_LOW;
	}

Note: Currently (as of August 2020), the common NP21Kai emulator always has the 16MB memory hole in use as RAM (the BIOS option to disable it is not available), so the above check always fails and the far framebuffer address is always used.


Mapping Framebuffer to DOS Memory

Normally DOS applications are restricted to accessing memory within the first 1MB, unless using XMS or EMS memory mappers. However, since the code we're writing is in 32bit protected mode, we can use the DPMI services to access any and all addresses within the 32bit address space.

The example below shows the use of creating a DPMI mapper, and using it to access the framebuffer location in the address space (whether it's at the low 16MB location, or way up the top at 4095MB):

        #include <dos.h>
        #include <go32.h>
        #include <dpmi.h>
 
        int             pegc_fb_location;       // Where the framebuffer we be located
        __dpmi_meminfo	vram_dpmi;		// DPMI descriptor for far-memory location of framebuffer
        int		vram_dpmi_selector;	// Memory region selector handle	
 
	if (gfx_HasMemoryHole() < 0){
		printf("%s.%d\t Memory hole at 16MB is not available\n", __FILE__, __LINE__);
		printf("%s.%d\t Using VRAM framebuffer at 0x%x\n", __FILE__, __LINE__, PEGC_FB_LOCATION_HIGH);
		pegc_fb_location = PEGC_FB_LOCATION_HIGH;
	} else {
		printf("%s.%d\t Memory hole at 16MB is available\n", __FILE__, __LINE__);
		printf("%s.%d\t Using VRAM framebuffer at 0x%x\n", __FILE__, __LINE__, PEGC_FB_LOCATION_LOW);
		pegc_fb_location = PEGC_FB_LOCATION_LOW;
	}
 
	if (!__djgpp_nearptr_enable()){
		printf("%s.%d\t DPMI Error; Unable to enable near pointers\n", __FILE__, __LINE__);
		return -1;
	}
 
	vram_dpmi.address = pegc_fb_location;
	vram_dpmi.size    = PEGC_FB_SIZE;
 
	if (__dpmi_physical_address_mapping(&vram_dpmi) != 0){
		printf("%s.%d\t DPMI Error; Unable to set DPMI physical address mapping\n", __FILE__, __LINE__);
		return -1;
	}
	vram_dpmi_selector = __dpmi_allocate_ldt_descriptors(1);
	if (vram_dpmi_selector < 0){
		printf("%s.%d\t DPMI Error; Unable to get DPMI ldt descriptor\n", __FILE__, __LINE__);
		__dpmi_free_physical_address_mapping(&vram_dpmi);
		return -1;
	}
	__dpmi_set_segment_base_address(vram_dpmi_selector, vram_dpmi.address);
	__dpmi_set_segment_limit(vram_dpmi_selector, vram_dpmi.size - 1);
 
	printf("%s.%d\t DPMI mapper created for framebuffer\n", __FILE__, __LINE__);
	return 0;

It is this section of code which I used to test the functionality of the various DPMI services on PC-98 DOS. The only one which would correctly return all of the various dpmi calls was the MS-DOS 6.22 DPMI.EXE. There may be other third party DPMI services, but since this one comes with DOS itself, it's probably the easiest (and most reliable) one to obtain.

Note: You may want to create your DPMI memory info structure (vram_dpmi) and VRAM selector object (vram_dpmi_selector) as globals, since anything writing to the framebuffer (see next example, below) will need to access them.


Writing to Framebuffer

Normally we would use memcpy() or similar to move blocks of data around, but memcpy() doesn't work between low memory and DPMI mapped memory regions.

There are a few ways around this in DJGPP, but, be warned, some are rather hacky in nature. The cleanest methods are to use the following library functions:

  • _farpokeb(), _farpokew(), _farpokel() - To set individual bytes (8), short (16) or long (32) words
  • _farpeekb(), _farpeekw(), _farpeekl() - To get individual bytes, short or long words
  • movedata - To transfer contiguous blocks of memory

You can, therefore write directly to the framebuffer if you wish, but it's probably easier to create a local buffer which you write into first, compositing all of your display elements, and then transfer the entire buffer in one operation to the framebuffer hardware.

     #include <dos.h>
     #include <go32.h>
     #include <dpmi.h>
 
     // Define the size of the screen
     #define GFX_ROWS	400	// Number of pixels in a row
     #define GFX_COLS	640	// Number of pixels in a column
 
     // Create a local vram buffer
     unsigned char	vram_buffer[(GFX_ROWS * GFX_COLS)];
 
     // Do all your operations on the vram_buffer
     // ...
     // ...
 
     // Now transfer the local buffer to the actual graphics hardware
     // _my_ds() - is a DJGPP function which selects the correct DOS memory
     // selector of any variable in your running code.
     // vram_buffer - is the structure in local memory we have written our graphics into
     // vram_dpmi_selector - is the VRAM framebuffer DPMI selector we created earlier.
     // 0 - is the offset into the far memory we want to copy to, in this case we are copying the entire region, so it is 0
     // GFX_ROWS * GFX_COLS - is the number of bytes to copy from the source... this should be 256KB in this case
     movedata(_my_ds(), vram_buffer, vram_dpmi_selector, 0, (GFX_ROWS * GFX_COLS));

The use of movedata() is slightly restrictive in that the size of data transferred must be a multiple of 32bit long words, so it's better used for transferring larger blocks of memory; altering single (or a few) bytes is better done with the _farpoke() calls.

Note: In 256 colour 640×400 mode, you actually have two pages of VRAM to write to, so you could in fact do double-buffered output by writing first to the first 0-256KBytes of VRAM, displaying it, and then writing to the 256-512KBytes of VRAM. You can control which portion of VRAM is displayed at any given time by writing 0x00 or 0x01 to port 0xA4. If you have chosen a 640×480 resolution, then you can only have a single page of VRAM, as a single screen is >256KBytes in size.

Planar access in 256 colour mode is only supported on a small subset of PC-9821 machines according to http://radioc.web.fc2.com/column/pc98bas/pc98disphw_en.htm.

  • PC-9821Ap, Ap2 (Ap3 is specifically not supported)
  • PC-9821As, As2
  • PC-9821Ae
  • PC-9821Af
  • PC-9821An
  • PC-9821Ce, Ce2
  • PC-9821Cs2
  • PC-9821Ne

For this reason (it limits the use of any of your code to the small number of machines above), I am not intending to cover this specific mode.

There is a vanishingly small amount of existing software which uses this mode - the most well known being the PC-98 Windows 3.1 256 colour PEGC driver.

TO DO

TO DO

  • blog/pc98_devcode.txt
  • Last modified: 2020/08/22 09:48
  • by john