pico9918-core 1.3.0
TMS9918A / F18A video display processor emulation in C99
Loading...
Searching...
No Matches
render_frame.c
1/*
2 * pico9918-core - render one frame to a PPM
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 render_frame.c
11 *
12 * The whole library in one file: set a mode up, write the three tables a
13 * TMS9918A draws Graphics I from, then ask for each scanline in turn and colour
14 * the indices it hands back.
15 *
16 * The shape here is the shape a display driver has. `pico9918_scan_line` renders
17 * one line into an internal buffer and returns the status byte a host would have
18 * read; `pico9918_line_source` and `pico9918_line_bytes` are how you get at what
19 * it drew. Nothing allocates per line, and nothing is drawn until you ask - so a
20 * driver calls this from its own line interrupt and a program like this one just
21 * loops.
22 *
23 * Build it against an installed package:
24 *
25 * cmake -S examples -B build-examples
26 * cmake --build build-examples
27 * ./build-examples/render_frame frame.ppm [tms9918|tms9918a|f18a|pico9918|pro]
28 */
29
30#include "pico9918.h"
31#include "pico9918_util.h"
32
33#include <stdio.h>
34#include <string.h>
35
36/* A TMS9918A's active display. TMS9918_PIXELS_Y is the buffer the F18A's taller
37 * modes need, not what Graphics I draws. */
38#define ROWS 192
39#define COLS TMS9918_PIXELS_X
40
41/* Graphics I reads its tables from wherever the registers point. These are the
42 * addresses pico9918_initialise_gfx_i programs, and they are what the writes
43 * below assume. */
44#define PATTERNS TMS_DEFAULT_VRAM_PATT_ADDRESS
45#define NAMES TMS_DEFAULT_VRAM_NAME_ADDRESS
46#define COLORS TMS_DEFAULT_VRAM_COLOR_ADDRESS
47
48/* One 8x8 glyph: a filled square with a hollow centre, so a frame that renders
49 * correctly is obvious at a glance and a frame that does not is obviously wrong. */
50static const uint8_t kTile[8] = {
51 0xFF, 0x81, 0xBD, 0xA5, 0xA5, 0xBD, 0x81, 0xFF
52};
53
54/* PICO9918_INST_ARG in the signature and PICO9918_INST at the call is how a helper
55 * compiles under either instance mode: both expand to nothing when the library was
56 * built PICO9918_SINGLE_INSTANCE=1. */
57static void writeVram(PICO9918_INST_ARG uint16_t addr, const uint8_t* data, size_t len)
58{
60 pico9918_write_addr(PICO9918_INST 0x40 | (addr >> 8));
62}
63
64static int writePpm(const char* path, const uint8_t* rgb)
65{
66 FILE* f = fopen(path, "wb");
67 if (!f)
68 {
69 perror(path);
70 return 1;
71 }
72 fprintf(f, "P6\n%d %d\n255\n", COLS, ROWS);
73 fwrite(rgb, 3, (size_t)COLS * ROWS, f);
74 fclose(f);
75 return 0;
76}
77
78#if PICO9918_BUILD_RUNTIME_CHIP
79static int selectPersonality(PICO9918_INST_ARG const char* name)
80{
81 pico9918_chip_t chip;
82 if (strcmp(name, "tms9918") == 0)
84 else if (strcmp(name, "tms9918a") == 0)
86 else if (strcmp(name, "f18a") == 0)
87 chip = PICO9918_CHIP_F18A;
88 else if (strcmp(name, "pico9918") == 0)
90 else if (strcmp(name, "pro") == 0)
92 else
93 {
94 fprintf(stderr, "unknown chip '%s'\n", name);
95 return 1;
96 }
97
99 return 0;
100}
101#endif
102
103int main(int argc, char** argv)
104{
105 const char* out = argc > 1 ? argv[1] : "frame.ppm";
106 static uint8_t rgb[ROWS * COLS * 3];
107 uint8_t tiles[8 * 8];
108
109#if PICO9918_SINGLE_INSTANCE
110 pico9918_init();
111#else
112 pico9918_t* tms9918 = pico9918_new();
113 if (!tms9918)
114 {
115 fprintf(stderr, "pico9918_new failed\n");
116 return 1;
117 }
118#endif
119
120#if PICO9918_BUILD_RUNTIME_CHIP
121 if (argc > 2 && selectPersonality(PICO9918_INST argv[2])) return 1;
122#else
123 if (argc > 2)
124 {
125 fprintf(stderr, "this library was built without runtime chip selection\n");
126 return 1;
127 }
128#endif
129
132
133 /* After initialise_gfx_i, which clears VRAM last of all - so these writes go
134 * after it, not before. Pattern 1 is the glyph; pattern 0 stays blank. */
135 writeVram(PICO9918_INST PATTERNS + 8, kTile, sizeof kTile);
136
137 /* Graphics I gives one colour byte per eight patterns, so this colours the
138 * whole first group at once: white on dark blue. */
139 memset(tiles, pico9918_fg_bg_color(TMS_WHITE, TMS_DK_BLUE), 32);
140 writeVram(PICO9918_INST COLORS, tiles, 32);
141
142 /* A checkerboard of it, so both the glyph and the backdrop are on screen. */
143 for (int row = 0; row < ROWS / 8; ++row)
144 {
145 uint8_t names[COLS / 8];
146 for (int col = 0; col < COLS / 8; ++col)
147 {
148 names[col] = (uint8_t)((row + col) & 1);
149 }
150 writeVram(PICO9918_INST(uint16_t)(NAMES + row * (COLS / 8)), names, sizeof names);
151 }
152
155
156 for (uint16_t y = 0; y < ROWS; ++y)
157 {
159
160 const uint8_t* line = pico9918_line_source(PICO9918_INST_ONLY);
161 uint8_t* px = rgb + (size_t)y * COLS * 3;
162 for (int x = 0; x < COLS; ++x)
163 {
164 /* 0x0rgb, four bits a channel, so each one is scaled by 17 to fill a byte */
165 const uint16_t argb = pico9918_default_palette(line[x] & 0x0F);
166 *px++ = (uint8_t)(((argb >> 8) & 0xF) * 17);
167 *px++ = (uint8_t)(((argb >> 4) & 0xF) * 17);
168 *px++ = (uint8_t)(((argb) & 0xF) * 17);
169 }
170 }
171
172 printf("%s: %dx%d, mode %d\n", out, COLS, ROWS,
174
175#if !PICO9918_SINGLE_INSTANCE
177#endif
178 return writePpm(out, rgb);
179}
pico9918_mode_t pico9918_display_mode(pico9918_t *tms9918)
current display mode
Definition pico9918.c:3499
void pico9918_destroy(pico9918_t *tms9918)
destroy a TMS9918
Definition pico9918.c:360
void pico9918_write_addr(pico9918_t *tms9918, uint8_t data)
write an address (mode = 1) to the tms9918
Definition pico9918.c:373
void pico9918_set_chip(pico9918_t *tms9918, pico9918_chip_t chip)
select which chip this instance answers as
Definition pico9918.c:276
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
pico9918-core - core interface
pico9918_t * pico9918_new(void)
create a new TMS9918
#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_chip_t
which chip an instance answers as
Definition pico9918.h:150
@ PICO9918_CHIP_PICO9918
an F18A plus the PICO9918's own extensions
Definition pico9918.h:154
@ PICO9918_CHIP_TMS9918
a pre-A TMS9918: a TMS9918A without Graphics II
Definition pico9918.h:151
@ PICO9918_CHIP_PICO9918_PRO
a PICO9918 PRO: 8bpp 80-column text, its own splash
Definition pico9918.h:155
@ PICO9918_CHIP_TMS9918A
a TMS9918A: locked, no GPU, no extensions
Definition pico9918.h:152
@ PICO9918_CHIP_F18A
an F18A: unlock, enhanced renderer, GPU
Definition pico9918.h:153
#define PICO9918_INST
pass the instance ahead of other arguments
Definition pico9918.h:73
PICO9918_DLLEXPORT void pico9918_initialise_gfx_i(pico9918_t *tms9918)
program Graphics I, the default table addresses and black on cyan, then clear VRAM
pico9918-core - utility / helper functions
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_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