pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
f18a_modes.c
1/*
2 * pico9918-core - turn the F18A on
3 *
4 * Copyright (c) 2026 Troy Schrapel
5 *
6 * This code is licensed under the MIT license
7 *
8 * https://github.com/visrealm/pico9918-core
9 *
10 * \example f18a_modes.c
11 *
12 * The F18A boots as a TMS9918A and stays one until a program unlocks it, so this draws
13 * the same name table twice: once locked, then again after the unlock, with four
14 * enhanced features turned on. Every cell holds the same tile in both frames - all the
15 * difference is in the registers.
16 *
17 * R49 bits 5:4 enhanced colour mode 2: two bitplanes, four colours a cell
18 * R49 bit 7 the second tile layer, composited over the first
19 * R50 bit 1 attributes by screen position instead of by tile name
20 * R29 bits 3:2 how far apart the bitplanes sit (512 bytes here)
21 * R10, R11 layer 2's name and colour tables
22 * R24, R25 layer 2's horizontal and vertical scroll
23 * R27, R28 layer 1's
24 *
25 * Above ECM0 a colour byte stops being an fg/bg pair and becomes an attribute:
26 *
27 * bit 7 priority over sprites bit 6 flip X bit 5 flip Y
28 * bit 4 transparent bits 3:0 sub-palette
29 *
30 * The sub-palette supplies the high bits of the palette index and the pixel's bitplanes
31 * the low ones, so at ECM2 each sub-palette is four of the F18A's 64 colours. Position
32 * attributes are what let one repeated tile take a different four in every cell.
33 *
34 * cmake -S examples -B build-examples
35 * cmake --build build-examples
36 * ./build-examples/f18a_modes locked.ppm f18a.ppm
37 */
38
39#include "pico9918.h"
40#include "pico9918_util.h"
41
42#include <stdio.h>
43#include <stdlib.h>
44#include <string.h>
45
46#define ROWS 192
47#define COLS TMS9918_PIXELS_X
48#define CELLS_X (COLS / 8)
49#define CELLS_Y (ROWS / 8)
50#define CELLS (CELLS_X * CELLS_Y)
51
52/* 16KB, every table on the boundary its register can express: name tables on 1KB with
53 all four scroll pages free, colour tables on 64 bytes, patterns on 2KB. */
54#define PATT 0x0000
55#define PLANE2 0x0200 /* the stride R29 selects */
56#define SPRITE_PATT 0x0800
57#define SPRITE_ATTR 0x0C00
58#define NAME1 0x1000
59#define NAME2 0x2000
60#define COLOR1 0x3000
61#define COLOR2 0x3800
62
63#define VRAM_SIZE 0x4000
64
65#define TILE_FIELD 1 /* layer 1 draws this one everywhere */
66#define TILE_STRIPE 2 /* layer 2's ribbon; tile 0 is blank, which is the rest of layer 2 */
67#define NUM_TILES (TILE_STRIPE + 1)
68
69static void writeReg(PICO9918_INST_ARG pico9918_register_t reg, uint8_t value)
70{
72}
73
74static void writeVram(PICO9918_INST_ARG uint16_t addr, const uint8_t* data, size_t len)
75{
78}
79
80/* Plane 1 is the pixel's low bit and plane 2 its high one, so a pixel is 0-3 and each
81 tile carries four colours of whatever sub-palette its attribute names. TILE_FIELD is
82 a bordered block with one highlight, which uses three of the four and leaves the
83 fourth - value 0 - nowhere, so no cell is ever skipped as empty. Graphics I reads
84 plane 1 alone, which is what the locked frame is a picture of. */
85static void buildTiles(uint8_t* plane1, uint8_t* plane2, size_t len)
86{
87 memset(plane1, 0, len);
88 memset(plane2, 0, len);
89 for (int y = 0; y < 8; ++y)
90 {
91 for (int x = 0; x < 8; ++x)
92 {
93 const int edge = (x == 0 || x == 7 || y == 0 || y == 7);
94 const int highlight = (x >= 1 && x <= 2 && y >= 1 && y <= 2);
95 const int field = edge ? 1 : highlight ? 2 : 3;
96 if (field & 1) plane1[TILE_FIELD * 8 + y] |= 0x80 >> x;
97 if (field & 2) plane2[TILE_FIELD * 8 + y] |= 0x80 >> x;
98
99 if (x & 4) /* four opaque pixels, four with every plane zero - so transparent */
100 {
101 plane1[TILE_STRIPE * 8 + y] |= 0x80 >> x;
102 plane2[TILE_STRIPE * 8 + y] |= 0x80 >> x;
103 }
104 }
105 }
106}
107
108static void renderPpm(PICO9918_INST_ARG const char* path)
109{
110 static uint8_t rgb[(size_t)COLS * ROWS * 3];
111 for (uint16_t y = 0; y < ROWS; ++y)
112 {
114
115 const uint8_t* line = pico9918_line_source(PICO9918_INST_ONLY);
116 uint8_t* px = rgb + (size_t)y * COLS * 3;
117 for (int x = 0; x < COLS; ++x)
118 {
119 /* six bits of index now: the F18A's palette is 64 entries, not 16 */
120 const uint16_t argb = pico9918_default_palette(line[x] & 0x3F);
121 *px++ = (uint8_t)(((argb >> 8) & 0xF) * 17);
122 *px++ = (uint8_t)(((argb >> 4) & 0xF) * 17);
123 *px++ = (uint8_t)((argb & 0xF) * 17);
124 }
125 }
126
127 FILE* f = fopen(path, "wb");
128 if (!f)
129 {
130 perror(path);
131 return;
132 }
133 fprintf(f, "P6\n%d %d\n255\n", COLS, ROWS);
134 fwrite(rgb, 3, (size_t)COLS * ROWS, f);
135 fclose(f);
136 printf("wrote %s\n", path);
137}
138
139int main(int argc, char** argv)
140{
141 const char* lockedOut = argc > 1 ? argv[1] : "locked.ppm";
142 const char* f18aOut = argc > 2 ? argv[2] : "f18a.ppm";
143
144 static uint8_t zeros[VRAM_SIZE];
145 uint8_t plane1[NUM_TILES * 8], plane2[NUM_TILES * 8];
146 uint8_t cells[CELLS];
147
148#if PICO9918_SINGLE_INSTANCE
149 pico9918_init();
150#else
151 pico9918_t* tms9918 = pico9918_new();
152 if (!tms9918) return 1;
153#endif
155
156 /* pico9918_reset leaves VRAM alone, as the chip does. */
157 writeVram(PICO9918_INST 0, zeros, sizeof zeros);
158
159 buildTiles(plane1, plane2, sizeof plane1);
160 writeVram(PICO9918_INST PATT, plane1, sizeof plane1);
161 writeVram(PICO9918_INST PLANE2, plane2, sizeof plane2);
162
163 memset(cells, TILE_FIELD, sizeof cells);
164 writeVram(PICO9918_INST NAME1, cells, sizeof cells);
165
166 /* A Graphics I colour table, which is one fg/bg pair per eight names. */
167 memset(cells, pico9918_fg_bg_color(TMS_CYAN, TMS_DK_BLUE), 32);
168 writeVram(PICO9918_INST COLOR1, cells, 32);
169
170 /* 0xD0 as a sprite's Y ends the list, so no sprites are drawn */
171 writeVram(PICO9918_INST SPRITE_ATTR, (const uint8_t[]){0xD0}, 1);
172
178 pico9918_set_fg_bg_color(PICO9918_INST TMS_WHITE, TMS_BLACK);
179 writeReg(PICO9918_INST TMS_REG_1, TMS_R1_RAM_16K | TMS_R1_DISP_ACTIVE);
180
181 /* Written now, while the chip is locked, and dropped: a locked F18A takes R0-R7 and
182 nothing else. The two files are the proof - this one renders as plain Graphics I
183 with R49 already asking for ECM2 and a second layer. */
186 renderPpm(PICO9918_INST lockedOut);
187
188 /* 0x1C twice into R57. It is the one register write a locked device honours. */
191
192 /* Attributes by position, one per cell, laid out exactly like the name table: diamond
193 bands out from the centre. The tile never changes, so every colour here comes from
194 this table. Sub-palettes 1-3 are palette entries 4-15, the TMS9918A's own sixteen;
195 0 is skipped because its first entry is the transparent one. */
196 for (int row = 0; row < CELLS_Y; ++row)
197 {
198 for (int col = 0; col < CELLS_X; ++col)
199 {
200 const int ring = (abs(col - CELLS_X / 2) + abs(row - CELLS_Y / 2)) / 2;
201 cells[row * CELLS_X + col] = (uint8_t)(1 + ring % 3);
202 }
203 }
204 writeVram(PICO9918_INST COLOR1, cells, sizeof cells);
205
206 /* Layer 2: a four-cell diagonal ribbon, blank everywhere else. Its attribute sets bit
207 4, so the tile's zero pixels are transparent and layer 1 shows between the stripes. */
208 for (int row = 0; row < CELLS_Y; ++row)
209 for (int col = 0; col < CELLS_X; ++col)
210 cells[row * CELLS_X + col] = (uint8_t)((((col - row) & 0x1F) < 4) ? TILE_STRIPE : 0);
211 writeVram(PICO9918_INST NAME2, cells, sizeof cells);
212
213 /* Transparent, on sub-palette 0 - the one layer 1 does not use, so the ribbon is a
214 colour nothing underneath it can be. */
215 memset(cells, 0x10, sizeof cells);
216 writeVram(PICO9918_INST COLOR2, cells, sizeof cells);
217
218 writeReg(PICO9918_INST PICO9918_REG_NAME_TABLE2, NAME2 >> 10);
219 writeReg(PICO9918_INST PICO9918_REG_COLOR_TABLE2, COLOR2 >> 6);
220 writeReg(PICO9918_INST PICO9918_REG_PAGE_SIZE, 0x88); /* 512 bytes a plane, tiles and sprites */
224
225 /* A different amount on each axis of each layer, which is where a scroll taken from
226 the wrong register shows. The low three bits are the fine offset, the rest cells. */
231
232 printf("unlocked: R49 reads back 0x%02X\n",
234
235 renderPpm(PICO9918_INST f18aOut);
236
237#if !PICO9918_SINGLE_INSTANCE
239#endif
240 return 0;
241}
void pico9918_destroy(pico9918_t *tms9918)
destroy a TMS9918
Definition pico9918.c:360
const uint8_t * pico9918_line_source(pico9918_t *tms9918)
where the scanline just generated actually is.
Definition pico9918.c:3529
uint8_t pico9918_scan_line(pico9918_t *tms9918, uint16_t y)
generate a scanline
Definition pico9918.c:3256
void pico9918_reset(pico9918_t *tms9918)
reset the new TMS9918
Definition pico9918.c:318
uint16_t pico9918_default_palette(int index)
a default palette entry, 0xargb
Definition pico9918.c:3536
uint8_t pico9918_reg_value(pico9918_t *tms9918, pico9918_register_t reg)
return a register value - see the header for the locked-device aliasing
Definition pico9918.c:3329
pico9918-core - core interface
pico9918_t * pico9918_new(void)
create a new TMS9918
#define PICO9918_R49_TILE2_ENABLE
register 49 bits: tile layer 2, row count, and the enhanced colour modes
Definition pico9918.h:342
#define TMS_R1_RAM_16K
register 1 bits: VRAM size, blanking, interrupt, mode and sprite size
Definition pico9918.h:297
#define PICO9918_INST_ARG
declare the instance ahead of other parameters
Definition pico9918.h:71
#define PICO9918_INST_ONLY
pass the instance as the only argument
Definition pico9918.h:74
#define TMS_R1_DISP_ACTIVE
render the active display
Definition pico9918.h:300
pico9918_register_t
the eight TMS9918 registers, by number and by what each one holds
Definition pico9918.h:198
@ PICO9918_REG_COLOR_TABLE2
tile layer 2 colour table base
Definition pico9918.h:218
@ PICO9918_REG_NAME_TABLE2
tile layer 2 name table base
Definition pico9918.h:217
@ PICO9918_REG_ENHANCED1
tile layer 2, 30-row mode, ECM levels, real Y
Definition pico9918.h:236
@ PICO9918_REG_T1_VSCROLL
tile layer 1 vertical scroll
Definition pico9918.h:225
@ PICO9918_REG_ENHANCED2
GPU triggers, per-position attributes, layer priority.
Definition pico9918.h:237
@ PICO9918_REG_T2_HSCROLL
tile layer 2 horizontal scroll
Definition pico9918.h:222
@ PICO9918_REG_T2_VSCROLL
tile layer 2 vertical scroll
Definition pico9918.h:223
@ PICO9918_REG_PAGE_SIZE
scroll page sizes, and the ECM pattern plane stride
Definition pico9918.h:226
@ PICO9918_REG_UNLOCK
0x1c twice unlocks the F18A personality; any other value locks
Definition pico9918.h:242
@ PICO9918_REG_T1_HSCROLL
tile layer 1 horizontal scroll
Definition pico9918.h:224
#define PICO9918_R49_ECM_TILE_2
tiles take two bitplanes, four colours
Definition pico9918.h:346
#define PICO9918_R57_UNLOCK
the value register 57 takes, twice in a row, to unlock
Definition pico9918.h:368
#define PICO9918_R50_POS_ATTR
tile attributes come per position, not per tile
Definition pico9918.h:361
#define PICO9918_INST
pass the instance ahead of other arguments
Definition pico9918.h:73
pico9918-core - utility / helper functions
static void pico9918_set_address_write(pico9918_t *tms9918, uint16_t addr)
point the VRAM address register at addr for writing, ie.
static void pico9918_set_fg_bg_color(pico9918_t *tms9918, pico9918_color_t fg, pico9918_color_t bg)
set register 7, the text-mode foreground colour and the backdrop
static void pico9918_set_color_table_addr(pico9918_t *tms9918, uint16_t addr)
set the colour table address, which register 3 holds in 64-byte units
static void pico9918_set_sprite_patt_table_addr(pico9918_t *tms9918, uint16_t addr)
set the sprite pattern table address, which register 6 holds in 2KB units
static void pico9918_set_name_table_addr(pico9918_t *tms9918, uint16_t addr)
set the name table address, which register 2 holds in 1KB units
static void pico9918_write_bytes(pico9918_t *tms9918, const uint8_t *bytes, size_t numBytes)
write a block of bytes to VRAM from the current address
static void pico9918_set_sprite_attr_table_addr(pico9918_t *tms9918, uint16_t addr)
set the sprite attribute table address, which register 5 holds in 128-byte units
static void pico9918_write_register_value(pico9918_t *tms9918, pico9918_register_t reg, uint8_t value)
write a VDP register as a host would, value byte first
static uint8_t pico9918_fg_bg_color(pico9918_color_t fg, pico9918_color_t bg)
pack fg into the high nibble and bg into the low nibble of one colour byte
static void pico9918_set_pattern_table_addr(pico9918_t *tms9918, uint16_t addr)
set the pattern table address, which register 4 holds in 2KB units