====== 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. [[blog:pc98_devtools|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.... ===== Pre-Requisites ===== After [[blog:pc98_devtools#makefiles_Compiling|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. ===== Non-hardware Specific Examples ===== === 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 #include 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; } ===== PC-9821 PEGC 256 Colour Graphics Hardware ===== There are several generations of custom graphics hardware on the PC-98: * PC-9801, GDC: 640x400, 8 colour, 96KB VRAM (later 192KB) * PC-9801VF, GC: 640x400, 16 colour, 96KB VRAM (later 192KB) * PC-9801VX, EGC: 640x400, 16 colour, 256KB VRAM, added raster operation functions, bitblt and bit shift functions * PC-9821AP, PEGC: 640x400, 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. ==== Packed-Pixel or Chunky Video Modes ==== 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: {{:blog:pegc_memory_hole_unset.png?400|}} Alternatively, on a machine on which the BIOS setting has been enabled, the value of //0x43B// __should__ be clear: {{:blog:pegc_memory_hole_set.png?400|}} 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 [[http://www.delorie.com/djgpp/v2faq/faq18_7.html|__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 [[http://www.delorie.com/djgpp/doc/libc/libc_605.html|outportb()]] (included in dos.h) for ports in the low memory range, and with [[http://www.delorie.com/djgpp/doc/libc/libc_304.html|_farpokeb()]] (included in farptr.h) for ports in the extended memory range. // 256 colour packed-pixel mode #include #include #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 #include #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 #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 #include #include 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 [[http://www.delorie.com/djgpp/doc/libc/libc_566.html|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 [[http://www.delorie.com/djgpp/v2faq/faq18_4.html|few]] [[http://www.delorie.com/djgpp/v2faq/faq18_7.html|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 * [[http://www.delorie.com/djgpp/doc/libc/libc_583.html|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 #include #include // 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 640x400 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 640x480 resolution, then you can only have a single page of VRAM, as a single screen is >256KBytes in size.// ==== Planar Video Modes ==== 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 [[https://github.com/joncampbell123/dosbox-x/issues/1061|PC-98 Windows 3.1 256 colour PEGC driver]]. ===== PC-9821 EGC 16 Colour Graphics Hardware ===== TO DO ==== Planar Video Modes ==== TO DO