From 0d6bed04f741b9906b01adc8226bec51c0b57471 Mon Sep 17 00:00:00 2001 From: John Bintz Date: Wed, 14 Feb 2024 20:15:55 -0500 Subject: [PATCH] init --- .gitignore | 5 + bmp_loader.c | 135 +++++++++++++++ bmp_loader.h | 23 +++ build.bat | 6 + chicken.bmp | Bin 0 -> 83082 bytes game.c | 38 +++++ in_progress.md | 2 + keyboard.c | 56 ++++++ keyboard.h | 21 +++ mouse_io.c | 48 ++++++ mouse_io.h | 18 ++ pc_stuff.c | 23 +++ pc_stuff.h | 25 +++ sprtsht.bmp | Bin 0 -> 4302 bytes types.h | 3 + unch1.c | 155 +++++++++++++++++ vga.c | 456 +++++++++++++++++++++++++++++++++++++++++++++++++ vga.h | 51 ++++++ 18 files changed, 1065 insertions(+) create mode 100644 .gitignore create mode 100644 bmp_loader.c create mode 100644 bmp_loader.h create mode 100644 build.bat create mode 100644 chicken.bmp create mode 100644 game.c create mode 100644 in_progress.md create mode 100644 keyboard.c create mode 100644 keyboard.h create mode 100644 mouse_io.c create mode 100644 mouse_io.h create mode 100644 pc_stuff.c create mode 100644 pc_stuff.h create mode 100644 sprtsht.bmp create mode 100644 types.h create mode 100644 unch1.c create mode 100644 vga.c create mode 100644 vga.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9ab992 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.zip +*.OBJ +*.ERR +*.EXE +.ccls-cache/ diff --git a/bmp_loader.c b/bmp_loader.c new file mode 100644 index 0000000..74429e0 --- /dev/null +++ b/bmp_loader.c @@ -0,0 +1,135 @@ +#include +#include +#include +#include + +#include "bmp_loader.h" +#include "pc_stuff.h" +#include "vga.h" + +int readBMPHeader(FILE *fh, struct BMPImage *info) { + int sizeOfHeader; + unsigned int numberOfUsedColors; + + if ((fgetc(fh) != 'B') || (fgetc(fh) != 'M')) { + printf("Not a BMP file\n"); + return 1; + } + + fseek(fh, 12, SEEK_CUR); + + fread(&sizeOfHeader, sizeof(int), 1, fh); + fread(&info->width, sizeof(int), 1, fh); + fread(&info->height, sizeof(int), 1, fh); + fseek(fh, 2, SEEK_CUR); + fread(&info->bpp, sizeof(word), 1, fh); + + // for gimp, you need to add colors to the color map until it hits + // 16 colors, then the image will be a 256 color, 8 bpp image + // + // https://www.gimp-forum.net/Thread-indexing-into-8-bit?pid=13233#pid13233 + if (info->bpp != 8) return 1; + + fseek(fh, 16, SEEK_CUR); + fread(&numberOfUsedColors, sizeof(unsigned int), 1, fh); + if (numberOfUsedColors > 256) return 1; + + // get down to color data + fseek(fh, 14 + sizeOfHeader, SEEK_SET); + + fread(info->colors, sizeof(struct BMPColor), numberOfUsedColors, fh); + + return 0; +} + +int readBMPIntoMemory(FILE *fh, struct BMPImage *info) { + int x, y, plane; + int result; + byte *currentPointer; + + result = readBMPHeader(fh, info); + if (result) return result; + + currentPointer = info->memoryStart; + + for (y = 0; y < info->height; ++y) { + for (x = 0; x < info->width; ++x) { + currentPointer[(info->height - 1 - y) * info->width + x] = fgetc(fh); + } + } + + return 0; +} + +int readBMPIntoUnchainedMemory(FILE *fh, struct BMPImage* info) { + int x, y, plane; + int writeX, writeY, offset; + int unchainedLineWidth; + int result; + byte *rowHolder; + + result = readBMPHeader(fh, info); + if (result) return result; + + unchainedLineWidth = info->width / 4; + + // reserve a row's worth of data + // read the bitmap data into the row, interleaving for planes + // for each plane, memcpy the data to the plane + for (y = 0; y < info->height; ++y) { + writeY = ((info->height - 1) - y) * unchainedLineWidth; + + for (x = 0; x < info->width; ++x) { + plane = x & 0x03; + writeX = (x >> 2); + + offset = plane * (unchainedLineWidth * info->height); + + info->memoryStart[offset + writeY + writeX] = fgetc(fh); + } + } + + return 0; +} + +int readBMPIntoUnchainedVGAMemory(FILE *fh, struct BMPImage* info) { + int x, y, plane; + int unchainedLineWidth; + int result; + byte *rowHolder; + + result = readBMPHeader(fh, info); + if (result) return result; + + rowHolder = malloc(info->width); + unchainedLineWidth = info->width / 4; + + // reserve a row's worth of data + // read the bitmap data into the row, interleaving for planes + // for each plane, memcpy the data to the plane + for (y = 0; y < info->height; ++y) { + for (x = 0; x < info->width; ++x) { + rowHolder[((x & 3) * 80) + (x >> 2)] = fgetc(fh); + } + + for (plane = 0; plane < 4; ++plane) { + setActiveVGAMemoryPlanes(1 << plane); + + memcpy(info->memoryStart + ((info->height - 1) - y) * info->width / 4, rowHolder + unchainedLineWidth * plane, unchainedLineWidth); + } + } + + free(rowHolder); + + return 0; +} + +void bmp256ColorPaletteToVGAColorPalette(struct BMPImage* bmpImage, struct VGAColor colors[]) { + int i; + + for (i = 0; i < 256; ++i) { + colors[i].red = bmpImage->colors[i].red >> 2; + colors[i].green = bmpImage->colors[i].green >> 2; + colors[i].blue = bmpImage->colors[i].blue >> 2; + } +} diff --git a/bmp_loader.h b/bmp_loader.h new file mode 100644 index 0000000..b216371 --- /dev/null +++ b/bmp_loader.h @@ -0,0 +1,23 @@ +#include + +#include "types.h" + +struct BMPColor { + byte blue; + byte green; + byte red; + byte _a; +}; + +struct BMPImage { + int width; + int height; + word bpp; + struct BMPColor colors[256]; + byte *memoryStart; +}; + +int readBMPIntoUnchainedMemory(FILE *, struct BMPImage *); +int readBMPIntoMemory(FILE *, struct BMPImage *); +void bmp256ColorPaletteToVGAColorPalette(struct BMPImage*, struct VGAColor[]); + diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..112a3aa --- /dev/null +++ b/build.bat @@ -0,0 +1,6 @@ +wcc386.exe -q bmp_loader.c +wcc386.exe -q mouse_io.c +wcc386.exe -q pc_stuff.c +wcc386.exe -q vga.c +wcc386.exe -q keyboard.c +wcl386.exe -q unch1.c bmp_load.obj mouse_io.obj pc_stuff.obj vga.obj keyboard.obj diff --git a/chicken.bmp b/chicken.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f0db92fc872d2f7810a7985e4ea7896232856c30 GIT binary patch literal 83082 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

2@}qxd9UF^wx9|>T^@mfty=Z&-3m<#{6JK3 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 literal 0 HcmV?d00001 diff --git a/game.c b/game.c new file mode 100644 index 0000000..d303251 --- /dev/null +++ b/game.c @@ -0,0 +1,38 @@ +#include "vga.h" +#include "keyboard.h" +#include "mouse_io.h" + +struct VGAColor colors[4] = { + { 0, 0, 0 }, { 255, 255, 255 }, { 255, 0, 0 }, { 0, 0, 255 } +} + +int main(void) { + int keepRunning = 1; + struct MouseStatus mouseStatus; + + installKeyboardHandler(); + activateMouse(&mouseStatus); + + while (keepRunning) { + + } + // states: + // * main menu + // * play + // * quit + // * play + // * draw base map + // * draw sidebar + // * Game loop + // * get mouse + // * get keyboard + // * set character position + // * check for enemy spawn and spawn enemy if needed + // * check for character firing and able to fire, spawn bullet if allowed + // * check for each enemy firing and able to fire, spawn enemy bullet if allowed + // * check for bullet collisions + // * enemies are destroyed + // * character is damaged + // * check for bullets hitting the edges of the screen and remove + // * +} diff --git a/in_progress.md b/in_progress.md new file mode 100644 index 0000000..7f8f974 --- /dev/null +++ b/in_progress.md @@ -0,0 +1,2 @@ +* Preserve video mode +* Rename files to 8.3 diff --git a/keyboard.c b/keyboard.c new file mode 100644 index 0000000..2581197 --- /dev/null +++ b/keyboard.c @@ -0,0 +1,56 @@ +#include +#include + +#include "keyboard.h" + +unsigned char keystateBits[16]; +struct KeyboardKeydownState keyboardKeydownState; + +void far (interrupt *int9Save)(); + +void populateKeyboardKeydownState() { + keyboardKeydownState.KEY_W = keystateBits[2] & 0x02; + keyboardKeydownState.KEY_A = keystateBits[3] & 0x40; + keyboardKeydownState.KEY_S = keystateBits[3] & 0x80; + keyboardKeydownState.KEY_D = keystateBits[4] & 0x01; +} + +void far interrupt keyboardHandler(void) { + unsigned char rawcode, scancode, ksbByte, temp; + unsigned char mask = 1; + + _disable(); + + rawcode = inp(0x60); + + scancode = rawcode & 0x7f; + mask = 1 << (scancode & 0x07); + ksbByte = scancode >> 3; + + // https://github.com/id-Software/wolf3d/blob/05167784ef009d0d0daefe8d012b027f39dc8541/WOLFSRC/ID_IN.C#L152-L154 + outp(0x61, (temp = inp(0x61)) | 0x80); + outp(0x61, temp); + + /* break */ + if (rawcode & 0x80) { + mask = 0xff - mask; + keystateBits[ksbByte] &= mask; + /* make */ + } else { + keystateBits[ksbByte] |= mask; + } + + // acknowledge we're done with the interrupt + outp(0x20, 0x20); + + _enable(); +} + +void installKeyboardHandler() { + int9Save = _dos_getvect(9); + _dos_setvect(9, keyboardHandler); +} + +void uninstallKeyboardHandler() { + _dos_setvect(9, int9Save); +} diff --git a/keyboard.h b/keyboard.h new file mode 100644 index 0000000..28e145e --- /dev/null +++ b/keyboard.h @@ -0,0 +1,21 @@ +#ifndef __KEYBOARD_H__ +#define __KEYBOARD_H__ 1 + +#include "types.h" + +extern unsigned char keystateBits[]; + +struct KeyboardKeydownState { + byte KEY_W; + byte KEY_A; + byte KEY_S; + byte KEY_D; +}; + +extern struct KeyboardKeydownState keyboardKeydownState; + +void installKeyboardHandler(); +void uninstallKeyboardHandler(); +void populateKeyboardKeydownState(); + +#endif diff --git a/mouse_io.c b/mouse_io.c new file mode 100644 index 0000000..8bf4eb3 --- /dev/null +++ b/mouse_io.c @@ -0,0 +1,48 @@ +#include +#include + +#include "mouse_io.h" + +int activateMouse(struct MouseStatus *status) { + union REGS regs; + int mouseActivated; + + regs.w.ax = MOUSE_DRIVER_RESET; + int386(MOUSE_DRIVER_INTERRUPT, ®s, ®s); + + mouseActivated = regs.w.ax; + + if (mouseActivated) { + status->isActive = regs.w.ax; + status->buttonCount = regs.w.bx; + + // set horiz and vert range + regs.w.ax = 0x07; + regs.w.cx = 0; + regs.w.dx = VGA_DISPLAY_WIDTH - 1; + int386(MOUSE_DRIVER_INTERRUPT, ®s, ®s); + + regs.w.ax = 0x08; + regs.w.cx = 0; + regs.w.dx = VGA_DISPLAY_HEIGHT - 1; + int386(MOUSE_DRIVER_INTERRUPT, ®s, ®s); + } + + return mouseActivated; +} + +void readMouse(struct MouseStatus *status) { + union REGS regs; + int buttonStatus; + + regs.w.ax = MOUSE_DRIVER_READ_STATE; + int386(MOUSE_DRIVER_INTERRUPT, ®s, ®s); + + buttonStatus = regs.w.bx; + status->xPosition = regs.w.cx; + status->yPosition = regs.w.dx; + + status->leftButtonDown = buttonStatus & 1; + status->rightButtonDown = (buttonStatus >> 1) & 1; +} + diff --git a/mouse_io.h b/mouse_io.h new file mode 100644 index 0000000..4759877 --- /dev/null +++ b/mouse_io.h @@ -0,0 +1,18 @@ +#include "vga.h" + +typedef struct MouseStatus { + int isActive; + int buttonCount; + int xPosition; + int yPosition; + int leftButtonDown; + int rightButtonDown; +}; + +#define MOUSE_DRIVER_INTERRUPT (0x33) +#define MOUSE_DRIVER_RESET (0x00) +#define MOUSE_DRIVER_READ_DELTA_MOTION (0x0B) +#define MOUSE_DRIVER_READ_STATE (0x03); + +int activateMouse(struct MouseStatus *); +void readMouse(struct MouseStatus *); diff --git a/pc_stuff.c b/pc_stuff.c new file mode 100644 index 0000000..d28eb77 --- /dev/null +++ b/pc_stuff.c @@ -0,0 +1,23 @@ +#include "pc_stuff.h" + +#define INPUT_STATUS (0x03da) +#define VBLANK (0x08) +#define BIOS_SET_VIDEO_MODE (0x00) + +byte *VGA = (byte *)0xA0000; + +void setVideoMode(byte videoMode) { + union REGS regs; + + regs.h.ah = BIOS_SET_VIDEO_MODE; + regs.h.al = videoMode; + int386(BIOS_VIDEO_INTERRUPT, ®s, ®s); +} + +void waitStartVbl() { + while (inp(INPUT_STATUS) & VBLANK); +} + +void waitEndVbl() { + while (!(inp(INPUT_STATUS) & VBLANK)); +} diff --git a/pc_stuff.h b/pc_stuff.h new file mode 100644 index 0000000..b12718b --- /dev/null +++ b/pc_stuff.h @@ -0,0 +1,25 @@ +#ifndef __PC_STUFF_H__ +#define __PC_STUFF_H__ + +#include +#include + +#include "types.h" + +extern byte *VGA; + +#define BIOS_GET_VIDEO_MODE (0x0F) +#define BIOS_VIDEO_INTERRUPT (0x10) + +#define VIDEO_MODE_VGA_256 (0x13) +#define VIDEO_MODE_80x25_TEXT (0x03) + +// remember, little endian, so the "first" value is "last" +#define outpw_to_register(indexPort, dataRegister, data) \ + outpw(indexPort, (((word)(data) << 8)) + (dataRegister)) + +#endif + +void setVideoMode(byte); +void waitStartVbl(); +void waitEndVbl(); diff --git a/sprtsht.bmp b/sprtsht.bmp new file mode 100644 index 0000000000000000000000000000000000000000..534d3a476020ff74877c5f077eb724901f190e0e GIT binary patch literal 4302 zcmeH_F%H5o3`G;Bj&KHc&QNt>V&N*RoUvCbPMY{TjU`$MG5v^?7uU~4zf6z$9P*mw z`j$PG9m60bOYUE9>%Pb_?GTpw$*1Z5d}TOrIc<8EG?R-7FaajO1egF5U;<2l2`~XB z@UH}7v+7f0tn}@>(bYQQ;|Z`pxa*Vc1cQ7FRoe*$q0=YZ2?qHVs +#include +#include +#include +#include +#include + +#include "types.h" +#include "mouse_io.h" +#include "pc_stuff.h" +#include "bmp_loader.h" +#include "vga.h" +#include "keyboard.h" + +byte MOUSE_POINTER[8][8] = { + { 2, 2, 2, 2, 2, 0, 0, 0 }, + { 2, 1, 1, 1, 2, 0, 0, 0 }, + { 2, 1, 1, 1, 2, 0, 0, 0 }, + { 2, 1, 1, 1, 2, 0, 0, 0 }, + { 2, 2, 2, 255, 1, 2, 0, 0 }, + { 0, 0, 0, 0, 2, 1, 2, 0 }, + { 0, 0, 0, 0, 0, 2, 1, 2 }, + { 0, 0, 0, 0, 0, 0, 2, 0 }, +}; + +byte WALKER[9][8] = { + { 0, 0, 0, 4, 4, 0, 0, 0 }, + { 0, 0, 4, 5, 5, 4, 0, 0 }, + { 0, 4, 5, 5, 5, 5, 4, 0 }, + { 0, 0, 0, 4, 4, 0, 0, 0 }, + { 0, 0, 4, 5, 255, 4, 0, 0 }, + { 0, 4, 5, 5, 5, 5, 4, 0 }, + { 0, 0, 0, 4, 4, 0, 0, 0 }, + { 0, 0, 4, 5, 5, 4, 0, 0 }, + { 0, 4, 5, 5, 5, 5, 4, 0 } +}; + +int main(void) { + FILE *fh; + struct BMPImage bmpImage, spriteSheetImage; + struct MouseStatus mouseStatus = { .xPosition = 0, .yPosition = 0, .leftButtonDown = 0 }; + struct VGAColor colors[256]; + byte *spriteSheet, *chicken, *drawBuffer; + int i, x, y; + + int currentMouseSprite = 0; + int mouseSpriteDelayCount = 0; + + float measuredTime; + + int walkerX = 0, walkerY = 0; + int yOffset; + + struct SpriteRender mousePointer = { + .data = (byte *)MOUSE_POINTER, + .transparentColor = 0, + .width = 8, + .height = 8, + .modulo = 48 + }; + + struct SpriteRender walker = { + .data = (byte *)WALKER, + .transparentColor = 0, + .width = 8, + .height = 9, + .modulo = 0 + }; + + installKeyboardHandler(); + + initializeDrawBuffer(); + + spriteSheet = malloc(64 * 64); + spriteSheetImage.memoryStart = spriteSheet; + + fh = fopen("sprtsht.bmp", "rb"); + if (readBMPIntoMemory(fh, &spriteSheetImage)) return 1; + fclose(fh); + + setVideoMode(VIDEO_MODE_VGA_256); + + chicken = malloc(320 * 256); + bmpImage.memoryStart = chicken; + + fh = fopen("chicken.bmp", "rb"); + readBMPIntoMemory(fh, &bmpImage); + fclose(fh); + + mousePointer.data = spriteSheet; + mousePointer.width = 16; + mousePointer.height = 16; + mousePointer.modulo = 48; + + bmp256ColorPaletteToVGAColorPalette(&bmpImage, colors); + setVGAColors(colors); + + activateMouse(&mouseStatus); + readMouse(&mouseStatus); + + while (!mouseStatus.leftButtonDown) { + readMouse(&mouseStatus); + + populateKeyboardKeydownState(); + + if (keyboardKeydownState.KEY_W) walkerY -= 1; + if (keyboardKeydownState.KEY_S) walkerY += 1; + if (keyboardKeydownState.KEY_A) walkerX -= 1; + if (keyboardKeydownState.KEY_D) walkerX += 1; + + + drawBuffer = getDrawBuffer(); + + memcpy(drawBuffer, chicken, 320 * 200); + + mousePointer.x = mouseStatus.xPosition; + mousePointer.y = mouseStatus.yPosition; + drawSprite(&mousePointer); + + for (i = 0; i < 100; ++i) { + walker.x = walkerX + i; + walker.y = walkerY + i; + drawSprite(&walker); + } + + waitStartVbl(); + memcpy((byte *)0xa0000, drawBuffer, 320 * 200); + waitEndVbl(); + + //drawBufferToUnchainedMemory(); + + + //copyUnchainedMemoryToActive(chicken); + + //swapPlanes(); + + mouseSpriteDelayCount += 1; + if (mouseSpriteDelayCount >= 15) { + currentMouseSprite = 1 - currentMouseSprite; + mousePointer.data = spriteSheet + (currentMouseSprite * 16); + mouseSpriteDelayCount = 0; + } + } + + uninstallKeyboardHandler(); + setVideoMode(VIDEO_MODE_80x25_TEXT); + free(spriteSheet); + free(chicken); + + freeDrawBuffer(); + + fprintf(stderr, "%lu %lu %lu\n", startTime, endTime, (clock_t)(endTime - startTime)); + + return 0; +} diff --git a/vga.c b/vga.c new file mode 100644 index 0000000..d13360a --- /dev/null +++ b/vga.c @@ -0,0 +1,456 @@ +#include "vga.h" +#include "pc_stuff.h" +#include +#include +#include +#include +#include + +#define VGA_SEQUENCE_CONTROLLER_INDEX (0x03C4) +#define VGA_SEQUENCE_CONTROLLER_DATA (0x03C5) + +#define VGA_SEQUENCE_CONTROLLER_MEMORY_MODE (0x04) +#define VGA_SEQUENCE_CONTROLLER_ODD_EVEN_SEQUENTIAL_ACCESS (0x04) +#define VGA_SEQUENCE_CONTROLLER_EXTENDED_MEMORY (0x02) + +#define VGA_SEQUENCE_CONTROLLER_MAP_MASK_MODE (0x02) +#define VGA_SEQUENCE_CONTROLLER_ALL_PLANES (0xff) + +#define VGA_GRAPHICS_REGISTERS_INDEX (0x03CE) +#define VGA_GRAPHICS_REGISTERS_DATA (0x03CF) + +#define VGA_GRAPHICS_DATA_ROTATE (0x03) + +#define VGA_CRT_CONTROLLER_INDEX (0x03d4) +#define VGA_CRT_CONTROLLER_DATA (0x03d5) + +#define VGA_CRT_CONTROLLER_UNDERLINE_MODE (0x14) +#define VGA_CRT_CONTROLLER_MODE_CONTROL (0x17) + +#define VGA_CRT_CONTROLLER_DISPLAY_HIGH_ADDRESS (0x0c) +#define VGA_CRT_CONTROLLER_DISPLAY_LOW_ADDRESS (0x0d) + +#define VGA_CRT_CONTROLLER_VERTICAL_RETRACE_START (0x10) +#define VGA_CRT_CONTROLLER_VERTICAL_RETRACE_END (0x11) +#define VGA_CRT_CONTROLLER_VERTICAL_DISPLAY_END (0x12) +#define VGA_CRT_CONTROLLER_VERTICAL_BLANK_START (0x15) +#define VGA_CRT_CONTROLLER_VERTICAL_BLANK_END (0x16) +#define VGA_CRT_CONTROLLER_VERTICAL_TOTAL (0x06) +#define VGA_CRT_CONTROLLER_OVERFLOW (0x07) + +int activePage = 0; +byte *drawBuffer, *displayMirrorBuffer; + +void initializeDrawBuffer() { + int i; + + drawBuffer = (byte *)malloc(VGA_DISPLAY_WIDTH * VGA_DISPLAY_HEIGHT); + displayMirrorBuffer = (byte *)malloc(VGA_DISPLAY_WIDTH * VGA_DISPLAY_HEIGHT); + + for (i = 0; i < VGA_DISPLAY_WIDTH * VGA_DISPLAY_HEIGHT; ++i) { + drawBuffer[i] = 0; + displayMirrorBuffer[i] = 0; + } +} + +void freeDrawBuffer() { + free(drawBuffer); + free(displayMirrorBuffer); +} + +void setActiveVGAMemoryPlanes(int planes) { + outpw_to_register( + VGA_SEQUENCE_CONTROLLER_INDEX, + VGA_SEQUENCE_CONTROLLER_MAP_MASK_MODE, + planes + ); +} + +byte *getDrawBuffer() { + return drawBuffer; +} + +clock_t startTime, endTime; + +void drawBufferToUnchainedMemory() { + int x, y, plane, pageOffset, yOffset, yDisplayOffset; + byte *vgaStart; + unsigned long data; + + int rowsToCopy[VGA_DISPLAY_HEIGHT], minRow = -1, maxRow = 0; + + unsigned long *drawBufferLong = (unsigned long *)drawBuffer, + *displayMirrorBufferLong = (unsigned long *)displayMirrorBuffer; + + byte copyRow[VGA_DISPLAY_WIDTH]; + + startTime = clock(); + + pageOffset = activePage * PAGE_SIZE; + vgaStart = (byte *)(VGA + pageOffset); + + for (y = 0; y < VGA_DISPLAY_HEIGHT; ++y) { + rowsToCopy[y] = 0; + yOffset = y * VGA_UNCHAINED_LINE_WIDTH; + + for (x = 0; x < VGA_UNCHAINED_LINE_WIDTH; x++) { + data = *(drawBufferLong++); + + if (displayMirrorBufferLong[yOffset] != data) { + rowsToCopy[y] = 1; + + break; + } + + yOffset++; + } + } + + memcpy(displayMirrorBuffer, drawBuffer, VGA_DISPLAY_WIDTH * VGA_DISPLAY_HEIGHT); + + for (y = 0; y < VGA_DISPLAY_HEIGHT; ++y) { + if (rowsToCopy[y]) { + if (minRow == -1) minRow = y; + maxRow = y; + } + } + + + if (minRow == -1) return; + + drawBufferLong = (unsigned long *)drawBuffer; + + for (y = minRow; y <= maxRow; ++y) { + if (!rowsToCopy[y]) continue; + + yOffset = y * VGA_UNCHAINED_LINE_WIDTH; + + for (x = 0; x < VGA_UNCHAINED_LINE_WIDTH; ++x) { + data = drawBufferLong[yOffset + x]; + + for (plane = 0; plane < 4; plane++) { + copyRow[x + (plane * VGA_UNCHAINED_LINE_WIDTH)] = data & 0xff; + data >>= 8; + } + } + + for (plane = 0; plane < 4; ++plane) { + setActiveVGAMemoryPlanes(1 << plane); + + memcpy(vgaStart + yOffset, copyRow + (plane * VGA_UNCHAINED_LINE_WIDTH), VGA_UNCHAINED_LINE_WIDTH); + } + } + + endTime = clock(); + +} + +void enableUnchainedVGAMode() { + word clearOffset; + + // convert VGA pointer into a word sized + ulong *ptr = (ulong *)VGA; + + // The VGA registers are (mostly) byte sized, and are grouped + // logically behind a pair of index/data ports. + // To access them, you prep the index port on the VGA card + // for the register you want to access, then do that access + // via the data port. + // + // Visual: VGA has pouches which contain organization boxes of + // registers. + + // target the Sequence Memory Mode Register for Memory Mode + // enable sequential access and extended memory + outpw_to_register( + VGA_SEQUENCE_CONTROLLER_INDEX, + VGA_SEQUENCE_CONTROLLER_MEMORY_MODE, + VGA_SEQUENCE_CONTROLLER_EXTENDED_MEMORY | VGA_SEQUENCE_CONTROLLER_ODD_EVEN_SEQUENTIAL_ACCESS + ); + + // target the Sequence Memory Mode Register for map mask + outpw_to_register( + VGA_SEQUENCE_CONTROLLER_INDEX, + VGA_SEQUENCE_CONTROLLER_MAP_MASK_MODE, + VGA_SEQUENCE_CONTROLLER_ALL_PLANES + ); + + for (clearOffset = 0; clearOffset < PAGE_SIZE; ++clearOffset) { + *ptr++ = 0; + } + + // there's a way to address memory as words or longs, but we're + // going to set it to byte addressing because it makes mroe sense. + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_UNDERLINE_MODE, + 0x00 + ); + + // * enable hsync and vsync + // * activate byte Mode + // * ...something with address wrapping that I don't quite understand yet... + // * ...even more with address funkiness that I don't understand + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_MODE_CONTROL, + 0xe3 + ); + + outp(0x3c2, 0xe3); +} + +void enable320x256VGAMode() { + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_VERTICAL_RETRACE_END, + 0x2c + ); + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_VERTICAL_TOTAL, + 0x0d + ); + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_OVERFLOW, + 0x3e + ); + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_VERTICAL_RETRACE_START, + 0xea + ); + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_VERTICAL_RETRACE_END, + 0xac + ); + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_VERTICAL_DISPLAY_END, + 0xdf + ); + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_VERTICAL_BLANK_START, + 0xe7 + ); + outpw_to_register( + VGA_CRT_CONTROLLER_INDEX, + VGA_CRT_CONTROLLER_VERTICAL_BLANK_END, + 0x06 + ); +} + +void clearWithColor(byte color) { + int x, y; + int pageOffset, drawY; + long *start; + long newColor = (color << 24) + (color << 16) + (color << 8) + (color); + + // write to all planes + outpw_to_register( + VGA_SEQUENCE_CONTROLLER_INDEX, + VGA_SEQUENCE_CONTROLLER_MAP_MASK_MODE, + 0xff + ); + + pageOffset = activePage * PAGE_SIZE; + start = (long *)(VGA + pageOffset); + + for (y = 0; y < VGA_DISPLAY_HEIGHT; ++y) { + for (x = 0; x < VGA_UNCHAINED_LINE_WIDTH / 4; ++x) { + *(start++) = newColor; + } + } +} + +/** + * this is very temporary + */ +void copyUnchainedMemoryToActive(byte *src) { + int y, plane, _testSize, _testOffset; + + _testSize = 80 * 240; + _testOffset = 80 * 256; + + for (plane = 0; plane < 4; ++plane) { + // copying to/from VGA memory requires setting the plane to write to... + outpw_to_register( + VGA_SEQUENCE_CONTROLLER_INDEX, + VGA_SEQUENCE_CONTROLLER_MAP_MASK_MODE, + 1 << plane + ); + + memcpy(VGA + activePage * PAGE_SIZE, src + plane * _testOffset, _testSize); + } +} + +void copyScratchPlanesToActive() { + int y, plane; + + for (plane = 0; plane < 4; ++plane) { + // copying to/from VGA memory requires setting the plane to write to... + outpw_to_register( + VGA_SEQUENCE_CONTROLLER_INDEX, + VGA_SEQUENCE_CONTROLLER_MAP_MASK_MODE, + 1 << plane + ); + + // and to read from + outpw_to_register( + 0x3ce, + 0x04, + plane + ); + + memcpy(VGA + activePage * PAGE_SIZE, VGA + SCRATCH_PAGE, PAGE_SIZE); + } +} + +void copyScratchPlanesToActiveViaLatches() { + int pixel; + byte *src, *dest; + + // TODO: VGA latches + outpw_to_register( + VGA_SEQUENCE_CONTROLLER_INDEX, + VGA_SEQUENCE_CONTROLLER_MAP_MASK_MODE, + 0xff + ); + + outpw_to_register( + VGA_GRAPHICS_REGISTERS_INDEX, + VGA_GRAPHICS_DATA_ROTATE, + 0x10 + ); + + src = VGA + SCRATCH_PAGE; + dest = VGA + activePage * PAGE_SIZE; + + for (pixel = 0; pixel < PAGE_SIZE; ++pixel) { + volatile char _temp = *(src++); + *(dest++) = 0; + } + + outpw_to_register( + VGA_GRAPHICS_REGISTERS_INDEX, + VGA_GRAPHICS_DATA_ROTATE, + 0x00 + ); +} + +void drawSprite(struct SpriteRender* sprite) { + int x, y; + byte pixel; + + byte* spriteData = sprite->data; + byte* drawBufferPos = drawBuffer + sprite->x + (sprite->y * VGA_DISPLAY_WIDTH); + + for (y = 0; y < sprite->height; ++y) { + for (x = 0; x < sprite->width; ++x) { + pixel = *(spriteData++); + if (pixel != sprite->transparentColor) { + *(drawBufferPos) = pixel; + } + + drawBufferPos++; + } + + drawBufferPos += (VGA_DISPLAY_WIDTH - sprite->width); + spriteData += sprite->modulo; + } +} + +void drawUnchainedSprite(struct SpriteRender* sprite) { + int dataX, dataY, currentPlane; + int drawX, drawY, vgaWriteYStart; + int startY, endY; + int nextStartX = 1; + int planeSampleX = 0, readPositionOffset; + int vgaWritePosition, readPosition, spriteAdvance; + int activePageOffset, alwaysVGAWriteStart, startVGAWritePosition; + byte readResult; + + // precalculate sprite sheet handling and VGA plane + currentPlane = sprite->x & 3; + spriteAdvance = sprite->width + sprite->modulo; + + outpw_to_register( + VGA_SEQUENCE_CONTROLLER_INDEX, + VGA_SEQUENCE_CONTROLLER_MAP_MASK_MODE, + 1 << currentPlane + ); + + // precalculate x pixel drawing + activePageOffset = activePage * PAGE_SIZE; + + alwaysVGAWriteStart = sprite->y; + if (alwaysVGAWriteStart < 0) alwaysVGAWriteStart = 0; + alwaysVGAWriteStart *= VGA_DISPLAY_WIDTH; + + // precalculate y pixel drawing + startY = sprite->y; + endY = sprite->y + sprite->height; + readPositionOffset = 0; + + if (startY < 0) { + readPositionOffset = (spriteAdvance * (-startY)); + startY = 0; + } + if (endY > VGA_DISPLAY_HEIGHT) { endY = VGA_DISPLAY_HEIGHT; } + + startVGAWritePosition = activePageOffset + (sprite->x + alwaysVGAWriteStart) / PLANE_PIXEL_DISTANCE; + + for (dataX = 0; dataX < sprite->width; ++dataX) { + if (planeSampleX >= sprite->width) { + planeSampleX = nextStartX; + nextStartX++; + currentPlane = (currentPlane + 1) & 3; + + startVGAWritePosition = activePageOffset + (planeSampleX + sprite->x + alwaysVGAWriteStart) / PLANE_PIXEL_DISTANCE; + + // save a second out + outp( + VGA_SEQUENCE_CONTROLLER_DATA, + 1 << currentPlane + ); + } + + drawX = planeSampleX + sprite->x; + if (drawX >= 0 && drawX < VGA_DISPLAY_WIDTH) { + vgaWritePosition = startVGAWritePosition; + readPosition = planeSampleX + readPositionOffset; + + for (drawY = startY; drawY < endY; ++drawY) { + readResult = sprite->data[readPosition]; + if (readResult != sprite->transparentColor) { + VGA[vgaWritePosition] = readResult; + } + vgaWritePosition += VGA_UNCHAINED_LINE_WIDTH; + readPosition += spriteAdvance; + } + } + + startVGAWritePosition++; + planeSampleX += 4; + } +} +void swapPlanes() { + outpw(VGA_CRT_CONTROLLER_INDEX, VGA_CRT_CONTROLLER_DISPLAY_HIGH_ADDRESS | ((PAGE_SIZE * activePage) & 0xff00)); + outpw(VGA_CRT_CONTROLLER_INDEX, VGA_CRT_CONTROLLER_DISPLAY_LOW_ADDRESS | ((PAGE_SIZE * activePage) << 8)); + + activePage = 1 - activePage; +} + +void setVGAColors(struct VGAColor colors[], int totalColors) { + int i; + + outp(0x3c8,0); + for (i = 0; i < totalColors; ++i) { + outp(0x3c9, colors[i].red); + outp(0x3c9, colors[i].green); + outp(0x3c9, colors[i].blue); + } +} diff --git a/vga.h b/vga.h new file mode 100644 index 0000000..cf9837a --- /dev/null +++ b/vga.h @@ -0,0 +1,51 @@ +#ifndef __VGA_H__ +#define __VGA_H__ + +#include "types.h" +#include + +#define PLANE_PIXEL_DISTANCE (4) +#define VGA_DISPLAY_WIDTH (320) +#define VGA_DISPLAY_HEIGHT (200) +#define VGA_UNCHAINED_LINE_WIDTH (VGA_DISPLAY_WIDTH / PLANE_PIXEL_DISTANCE) + +// there are four of these in a row +#define PAGE_SIZE (VGA_DISPLAY_WIDTH*VGA_DISPLAY_HEIGHT/4) +#define SCRATCH_PAGE (PAGE_SIZE * 2) + +struct VGAColor { + byte red; + byte green; + byte blue; +}; + +struct SpriteRender { + byte* data; + int x; + int y; + unsigned int width; + unsigned int height; + int transparentColor; + unsigned int modulo; +}; + +extern clock_t startTime, endTime; + +void enableUnchainedVGAMode(); +void enable320x256VGAMode(); +void setVGAColors(struct VGAColor[], int); +void copyUnchainedMemoryToActive(); +void copyScratchPlanesToActive(); +void copyScratchPlanesToActiveViaLatches(); +void drawSprite(struct SpriteRender *sprite); +void swapPlanes(); +void setActiveVGAMemoryPlanes(int); +void clearWithColor(byte); + +void initializeDrawBuffer(); +void freeDrawBuffer(); +void drawBufferToUnchainedMemory(); + +byte *getDrawBuffer(); + +#endif