From 04eb9ffc3e82418c5e45d1edb55a139e6b496bf7 Mon Sep 17 00:00:00 2001 From: GreaseMonkey Date: Sun, 7 Jan 2024 17:07:24 +1300 Subject: [PATCH] unittester: Add WIP specification document --- doc/specifications/86box-unit-tester.md | 240 ++++++++++++++++++++++++ src/device/unittester.c | 1 + src/include/86box/unittester.h | 1 + 3 files changed, 242 insertions(+) create mode 100644 doc/specifications/86box-unit-tester.md diff --git a/doc/specifications/86box-unit-tester.md b/doc/specifications/86box-unit-tester.md new file mode 100644 index 000000000..77279a3ee --- /dev/null +++ b/doc/specifications/86box-unit-tester.md @@ -0,0 +1,240 @@ +# 86Box Unit Tester device specification + +**TODO: DESCRIBE ME!** + +---------------------------------------------------------------------------- + +## Conventions + +### Integer types + +- `i8` denotes a signed 8-bit value. +- `u8` denotes an unsigned 8-bit value. +- `w8` denotes an 8-bit value which wraps around. +- `x8` denotes an 8-bit value where the signedness is irrelevant. +- `e8` ("either") denotes an 8-bit value where the most significant bit is clear - in effect, this is a 7-bit unsigned value. +- `u16L` denotes a little-endian unsigned 16-bit value. +- `u16B` would denote a big-endian unsigned 16-bit value if we had any big-endian values. +- `[N]T` denotes an array of `N` values of type `T`, whatever `N` and `T` are. + +---------------------------------------------------------------------------- + +## Usage + +### Accessing the device and configuring the I/O base address + +Find an area in I/O space where 2 addresses are confirmed (or assumed) to be unused. +There is no need for the 2 addresses to be 2-byte-aligned. + +Send the following sequence of bytes to port 0x80 with INTERRUPTS DISABLED: + + '8', '6', 'B', 'o', 'x', (IOBASE & 0xFF), (IOBASE >> 8) + +Alternatively denoted in hex: + + 38 36 42 6F 78 yy xx + +There are no timing constraints. This is an emulator, after all. + +To confirm that this has happened, read the status port at IOBASE+0x00. +If it's 0xFF, then the device is most likely not present. +Otherwise, one can potentially assume that it exists and has been configured successfully. +(You *did* make sure that the space was unused *before* doing this, right?) + +IOBASE is allowed to overlap the trigger port, but please don't do this! + +### Hiding the device + +Set the I/O base address to 0xFFFF using the above method. + +### Executing commands + +**TODO: IMPLEMENT ME!** + +The ports at IOBASE+0x00 and IOBASE+0x01 are all 8 bits wide. + +Writing to IOBASE+0x00 cancels any in-flight commands and sends a new command. + +Reading from IOBASE+0x00 reads the status: + +- bit 0: There is data to be read from this device + - If one reads with this bit clear, the returned data will be 0xFF. +- bit 1: The device is expecting data to be sent to it + - If one writes with this bit clear, the data will be ignored. +- bit 2: There is no command in flight + - If this is set, then bits 0 and 1 will be clear. +- bit 3: The previously-sent command does not exist. +- bits 4 .. 7: Reserved, should be 0. + +Writing to IOBASE+0x01 provides data to the device if said data is needed. + +Reading from IOBASE+0x01 fetches the next byte data to the device if said data is needed. + +### General flow of executing a command: + +This is how most commands will work. + +- Write the command to IOBASE+0x00. +- If data needs to be written or read: + - Read the status from IOBASE+0x00 and confirm that bit 2 is clear. + If it is set, then the command may not exist. + Check bit 3 if that's the case. +- If data needs to be written: + - Write all the data one needs to write. +- If data needs to be read: + - Read the status from IOBASE+0x00 and wait until bit 0 is set. + If it is set, then the command may not exist. + Check bit 3 if that's the case. + - Keep reading bytes until one is satisfied. +- Otherwise: + - Read the status from IOBASE+0x00 and wait until any of the bottom 3 bits are set. + +---------------------------------------------------------------------------- + +## Command reference + +### 0x00: No-op + +**TODO: IMPLEMENT ME!** + +This does nothing, takes no input, and gives no output. + +This is an easy way to reset the status to 0x04 (no command in flight, not waiting for reads or writes, and no errors). + +### 0x01: Capture Screen Snapshot + +**TODO: IMPLEMENT ME!** + +Captures a snapshot of the current screen state and stores it in the current snapshot buffer. + +The initial state of the screen snapshot buffer has an image area of 0x0, an overscanned area of 0x0, and an image start offset of (0,0). + +Input: + +* u8 monitor + - 0x00 = no monitor - clear the screen snapshot + - 0x01 = primary monitor + - 0x02 = secondary monitor + - Any monitor which is not available is treated as 0x00, and clears the screen snapshot. + +Output: + +* `e16L` image width in pixels +* `e16L` image height in pixels +* `e16L` overscanned width in pixels +* `e16L` overscanned height in pixels +* `e16L` X offset of image start +* `e16L` Y offset of image start + +If there is no screen snapshot, then all values will be 0 as per the initial screen snapshot buffer state. + +### 0x02: Read Screen Snapshot Rectangle + +**TODO: IMPLEMENT ME!** + +Returns a rectangular snapshot of the screen snapshot buffer. + +Input: + +* `e16L` w: rectangle width in pixels +* `e16L` h: rectangle height in pixels +* `i16L` x: X offset relative to image start +* `i16L` y: Y offset relative to image start + +Output: + +* `[h][w][4]u8`: image data + - `[y][x][0]` is the blue component, or 0x00 if the pixel is outside the snapshot area. + - `[y][x][1]` is the green component, or 0x00 if the pixel is outside the snapshot area. + - `[y][x][2]` is the red component, or 0x00 if the pixel is outside the snapshot area. + - `[y][x][3]` is 0x00, or 0xFF if the pixel is outside the snapshot area. + +### 0x03: Verify Screen Snapshot Rectangle + +**TODO: IMPLEMENT ME!** + +As per 0x02 "Read Screen Snapshot Rectangle", except instead of returning the pixel data, it returns a CRC-32 of the data. + +The CRC is as per zlib's `crc32()` function. Specifically, one uses a right-shifting Galois LFSR with a polynomial of 0xEDB88320, bytes XORed against the least significant byte, the initial seed is 0xFFFFFFFF, and all bits of the output are inverted. + +(Rationale: There are better CRCs, but this one is ubiquitous and still really good... and we don't need to protect against deliberate tampering.) + +Input: + +* `e16L` w: rectangle width in pixels +* `e16L` h: rectangle height in pixels +* `i16L` x: X offset relative to image start +* `i16L` y: Y offset relative to image start + +Output: + +* `u32L` crc: CRC-32 of rectangle data + +### 0x04: Exit 86Box + +**TODO: IMPLEMENT ME!** + +Exits 86Box, unless this command is disabled. + +- If the command is enabled, then program execution terminates immediately. +- If the command is disabled, it still counts as having executed correctly, but program execution continues. This makes it useful to show a "results" screen for a unit test. + +Input: + +* u8 exit code: + - The actual exit code is clamped to no greater than the maximum valid exit code. + - In practice, this is probably going to be 0x7F. + +---------------------------------------------------------------------------- + +## Implementation notes + +### Port 0x80 sequence detection + +In order to ensure that one can always trigger the activation sequence, there are effectively two finite state machines in action. + +FSM1: +- Wait for 8. +- Wait for 6. +- Wait for B. +- Wait for o. +- Wait for x. + Once received, set FSM2 to "Wait for low byte", + then go back to "Wait for 8". + +If at any point an 8 arrives, jump to the "Wait for 6" step. + +Otherwise, if any other unexpected byte arrives, jump to the "Wait for 8" step. + +FSM2: +- Idle. +- Wait for low byte. Once received, store this in a temporary location. +- Wait for high byte. + Once received, replace IOBASE with this byte in the high byte and the temporary value in the low byte, + then go back to "Idle". + +### Command processing state machine + +**TODO: IMPLEMENT ME!** + +**TODO: DOCUMENT ME!** + +---------------------------------------------------------------------------- + +## Extending the protocol + +### Adding new commands + +Commands 0x01 through 0x7F accept a single command byte. + +Command bytes 0x80 through 0xFB are reserved for 16-bit command IDs, to be written in a similar way to this: + +- Write the first command byte (0x80 through 0xFF) to the command register. +- If this block of commands does not exist, then the command is cancelled and the status is set to 0x0C. +- Otherwise, the status is set to 0x0 +- Write the next command byte (0x00 through 0xFF) to the data register. +- If this block of commands does not exist, then the command is cancelled and the status is set to 0x0C. +- Otherwise, the command exists and the status is set according to the command. + +Command bytes 0xFC through 0xFF are reserved for if we somehow need more than 16 bits worth of command ID. + diff --git a/src/device/unittester.c b/src/device/unittester.c index 2f706c8e3..ff605e119 100644 --- a/src/device/unittester.c +++ b/src/device/unittester.c @@ -7,6 +7,7 @@ * This file is part of the 86Box distribution. * * Debug device for assisting in unit testing. + * See doc/specifications/86box-unit-tester.md for more info. * * * diff --git a/src/include/86box/unittester.h b/src/include/86box/unittester.h index 1e1255f52..f6dedeae5 100644 --- a/src/include/86box/unittester.h +++ b/src/include/86box/unittester.h @@ -7,6 +7,7 @@ * This file is part of the 86Box distribution. * * Debug device for assisting in unit testing. + * See doc/specifications/86box-unit-tester.md for more info. * * *