all repos — mgba @ 64034e387fb2696f05078dd00b9e9c70c27d5f66

mGBA Game Boy Advance Emulator

src/ds/gx.c (view raw)

  1/* Copyright (c) 2013-2017 Jeffrey Pfau
  2 *
  3 * This Source Code Form is subject to the terms of the Mozilla Public
  4 * License, v. 2.0. If a copy of the MPL was not distributed with this
  5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  6#include <mgba/internal/ds/gx.h>
  7
  8#include <mgba/internal/ds/ds.h>
  9#include <mgba/internal/ds/io.h>
 10
 11mLOG_DEFINE_CATEGORY(DS_GX, "DS GX");
 12
 13#define DS_GX_FIFO_SIZE 256
 14#define DS_GX_PIPE_SIZE 4
 15
 16static void DSGXDummyRendererInit(struct DSGXRenderer* renderer);
 17static void DSGXDummyRendererReset(struct DSGXRenderer* renderer);
 18static void DSGXDummyRendererDeinit(struct DSGXRenderer* renderer);
 19static void DSGXDummyRendererSetRAM(struct DSGXRenderer* renderer, struct DSGXVertex* verts, struct DSGXPolygon* polys, unsigned polyCount);
 20static void DSGXDummyRendererDrawScanline(struct DSGXRenderer* renderer, int y);
 21static void DSGXDummyRendererGetScanline(struct DSGXRenderer* renderer, int y, color_t** output);
 22
 23static const int32_t _gxCommandCycleBase[DS_GX_CMD_MAX] = {
 24	[DS_GX_CMD_NOP] = 0,
 25	[DS_GX_CMD_MTX_MODE] = 2,
 26	[DS_GX_CMD_MTX_PUSH] = 34,
 27	[DS_GX_CMD_MTX_POP] = 72,
 28	[DS_GX_CMD_MTX_STORE] = 34,
 29	[DS_GX_CMD_MTX_RESTORE] = 72,
 30	[DS_GX_CMD_MTX_IDENTITY] = 38,
 31	[DS_GX_CMD_MTX_LOAD_4x4] = 68,
 32	[DS_GX_CMD_MTX_LOAD_4x3] = 60,
 33	[DS_GX_CMD_MTX_MULT_4x4] = 70,
 34	[DS_GX_CMD_MTX_MULT_4x3] = 62,
 35	[DS_GX_CMD_MTX_MULT_3x3] = 56,
 36	[DS_GX_CMD_MTX_SCALE] = 44,
 37	[DS_GX_CMD_MTX_TRANS] = 44,
 38	[DS_GX_CMD_COLOR] = 2,
 39	[DS_GX_CMD_NORMAL] = 18,
 40	[DS_GX_CMD_TEXCOORD] = 2,
 41	[DS_GX_CMD_VTX_16] = 18,
 42	[DS_GX_CMD_VTX_10] = 16,
 43	[DS_GX_CMD_VTX_XY] = 16,
 44	[DS_GX_CMD_VTX_XZ] = 16,
 45	[DS_GX_CMD_VTX_YZ] = 16,
 46	[DS_GX_CMD_VTX_DIFF] = 16,
 47	[DS_GX_CMD_POLYGON_ATTR] = 2,
 48	[DS_GX_CMD_TEXIMAGE_PARAM] = 2,
 49	[DS_GX_CMD_PLTT_BASE] = 2,
 50	[DS_GX_CMD_DIF_AMB] = 8,
 51	[DS_GX_CMD_SPE_EMI] = 8,
 52	[DS_GX_CMD_LIGHT_VECTOR] = 12,
 53	[DS_GX_CMD_LIGHT_COLOR] = 2,
 54	[DS_GX_CMD_SHININESS] = 64,
 55	[DS_GX_CMD_BEGIN_VTXS] = 2,
 56	[DS_GX_CMD_END_VTXS] = 2,
 57	[DS_GX_CMD_SWAP_BUFFERS] = 784,
 58	[DS_GX_CMD_VIEWPORT] = 2,
 59	[DS_GX_CMD_BOX_TEST] = 206,
 60	[DS_GX_CMD_POS_TEST] = 18,
 61	[DS_GX_CMD_VEC_TEST] = 10,
 62};
 63
 64static const int32_t _gxCommandParams[DS_GX_CMD_MAX] = {
 65	[DS_GX_CMD_MTX_MODE] = 1,
 66	[DS_GX_CMD_MTX_POP] = 1,
 67	[DS_GX_CMD_MTX_STORE] = 1,
 68	[DS_GX_CMD_MTX_RESTORE] = 1,
 69	[DS_GX_CMD_MTX_LOAD_4x4] = 16,
 70	[DS_GX_CMD_MTX_LOAD_4x3] = 12,
 71	[DS_GX_CMD_MTX_MULT_4x4] = 16,
 72	[DS_GX_CMD_MTX_MULT_4x3] = 12,
 73	[DS_GX_CMD_MTX_MULT_3x3] = 9,
 74	[DS_GX_CMD_MTX_SCALE] = 3,
 75	[DS_GX_CMD_MTX_TRANS] = 3,
 76	[DS_GX_CMD_COLOR] = 1,
 77	[DS_GX_CMD_NORMAL] = 1,
 78	[DS_GX_CMD_TEXCOORD] = 1,
 79	[DS_GX_CMD_VTX_16] = 2,
 80	[DS_GX_CMD_VTX_10] = 1,
 81	[DS_GX_CMD_VTX_XY] = 1,
 82	[DS_GX_CMD_VTX_XZ] = 1,
 83	[DS_GX_CMD_VTX_YZ] = 1,
 84	[DS_GX_CMD_VTX_DIFF] = 1,
 85	[DS_GX_CMD_POLYGON_ATTR] = 1,
 86	[DS_GX_CMD_TEXIMAGE_PARAM] = 1,
 87	[DS_GX_CMD_PLTT_BASE] = 1,
 88	[DS_GX_CMD_DIF_AMB] = 1,
 89	[DS_GX_CMD_SPE_EMI] = 1,
 90	[DS_GX_CMD_LIGHT_VECTOR] = 1,
 91	[DS_GX_CMD_LIGHT_COLOR] = 1,
 92	[DS_GX_CMD_SHININESS] = 32,
 93	[DS_GX_CMD_BEGIN_VTXS] = 1,
 94	[DS_GX_CMD_SWAP_BUFFERS] = 1,
 95	[DS_GX_CMD_VIEWPORT] = 1,
 96	[DS_GX_CMD_BOX_TEST] = 3,
 97	[DS_GX_CMD_POS_TEST] = 2,
 98	[DS_GX_CMD_VEC_TEST] = 1,
 99};
100
101static struct DSGXRenderer dummyRenderer = {
102	.init = DSGXDummyRendererInit,
103	.reset = DSGXDummyRendererReset,
104	.deinit = DSGXDummyRendererDeinit,
105	.setRAM = DSGXDummyRendererSetRAM,
106	.drawScanline = DSGXDummyRendererDrawScanline,
107	.getScanline = DSGXDummyRendererGetScanline,
108};
109
110static void _pullPipe(struct DSGX* gx) {
111	if (CircleBufferSize(&gx->fifo) >= sizeof(struct DSGXEntry)) {
112		struct DSGXEntry entry = { 0 };
113		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.command);
114		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.params[0]);
115		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.params[1]);
116		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.params[2]);
117		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.params[3]);
118		CircleBufferWrite8(&gx->pipe, entry.command);
119		CircleBufferWrite8(&gx->pipe, entry.params[0]);
120		CircleBufferWrite8(&gx->pipe, entry.params[1]);
121		CircleBufferWrite8(&gx->pipe, entry.params[2]);
122		CircleBufferWrite8(&gx->pipe, entry.params[3]);
123	}
124	if (CircleBufferSize(&gx->fifo) >= sizeof(struct DSGXEntry)) {
125		struct DSGXEntry entry = { 0 };
126		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.command);
127		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.params[0]);
128		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.params[1]);
129		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.params[2]);
130		CircleBufferRead8(&gx->fifo, (int8_t*) &entry.params[3]);
131		CircleBufferWrite8(&gx->pipe, entry.command);
132		CircleBufferWrite8(&gx->pipe, entry.params[0]);
133		CircleBufferWrite8(&gx->pipe, entry.params[1]);
134		CircleBufferWrite8(&gx->pipe, entry.params[2]);
135		CircleBufferWrite8(&gx->pipe, entry.params[3]);
136	}
137}
138
139static void _fifoRun(struct mTiming* timing, void* context, uint32_t cyclesLate) {
140	struct DSGX* gx = context;
141	uint32_t cycles;
142	bool first = true;
143	while (!gx->swapBuffers) {
144		if (CircleBufferSize(&gx->pipe) <= 2 * sizeof(struct DSGXEntry)) {
145			_pullPipe(gx);
146		}
147
148		if (!CircleBufferSize(&gx->pipe)) {
149			cycles = 0;
150			break;
151		}
152
153		DSRegGXSTAT gxstat = gx->p->memory.io9[DS9_REG_GXSTAT_LO >> 1];
154		int projMatrixPointer = DSRegGXSTATGetProjMatrixStackLevel(gxstat);
155
156		struct DSGXEntry entry = { 0 };
157		CircleBufferDump(&gx->pipe, (int8_t*) &entry.command, 1);
158		cycles = _gxCommandCycleBase[entry.command];
159
160		if (first) {
161			first = false;
162		} else if (cycles > cyclesLate) {
163			break;
164		}
165		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.command);
166		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.params[0]);
167		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.params[1]);
168		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.params[2]);
169		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.params[3]);
170
171		if (gx->activeParams) {
172			int index = _gxCommandParams[entry.command] - gx->activeParams;
173			gx->activeEntries[index] = entry;
174			--gx->activeParams;
175		} else {
176			gx->activeParams = _gxCommandParams[entry.command];
177			if (gx->activeParams) {
178				--gx->activeParams;
179			}
180			if (gx->activeParams) {
181				gx->activeEntries[0] = entry;
182			}
183		}
184
185		if (gx->activeParams) {
186			continue;
187		}
188
189		switch (entry.command) {
190		case DS_GX_CMD_MTX_MODE:
191			if (entry.params[0] < 4) {
192				gx->mtxMode = entry.params[0];
193			} else {
194				mLOG(DS_GX, GAME_ERROR, "Invalid GX MTX_MODE %02X", entry.params[0]);
195			}
196			break;
197		case DS_GX_CMD_MTX_IDENTITY:
198			switch (gx->mtxMode) {
199			case 0:
200				DSGXMtxIdentity(&gx->projMatrix);
201				break;
202			case 2:
203				DSGXMtxIdentity(&gx->vecMatrix);
204				// Fall through
205			case 1:
206				DSGXMtxIdentity(&gx->posMatrix);
207				break;
208			case 3:
209				DSGXMtxIdentity(&gx->texMatrix);
210				break;
211			}
212			break;
213		case DS_GX_CMD_MTX_PUSH:
214			switch (gx->mtxMode) {
215			case 0:
216				memcpy(&gx->projMatrixStack, &gx->projMatrix, sizeof(gx->projMatrix));
217				++projMatrixPointer;
218				break;
219			case 2:
220				memcpy(&gx->vecMatrixStack[gx->pvMatrixPointer & 0x1F], &gx->vecMatrix, sizeof(gx->vecMatrix));
221				// Fall through
222			case 1:
223				memcpy(&gx->posMatrixStack[gx->pvMatrixPointer & 0x1F], &gx->posMatrix, sizeof(gx->posMatrix));
224				++gx->pvMatrixPointer;
225				break;
226			case 3:
227				mLOG(DS_GX, STUB, "Unimplemented GX MTX_PUSH mode");
228				break;
229			}
230			break;
231		case DS_GX_CMD_MTX_POP: {
232			int8_t offset = entry.params[0];
233			offset <<= 2;
234			offset >>= 2;
235			switch (gx->mtxMode) {
236			case 0:
237				projMatrixPointer -= offset;
238				memcpy(&gx->projMatrix, &gx->projMatrixStack, sizeof(gx->projMatrix));
239				break;
240			case 1:
241				gx->pvMatrixPointer -= offset;
242				memcpy(&gx->posMatrix, &gx->posMatrixStack[gx->pvMatrixPointer & 0x1F], sizeof(gx->posMatrix));
243				break;
244			case 2:
245				gx->pvMatrixPointer -= offset;
246				memcpy(&gx->vecMatrix, &gx->vecMatrixStack[gx->pvMatrixPointer & 0x1F], sizeof(gx->vecMatrix));
247				memcpy(&gx->posMatrix, &gx->posMatrixStack[gx->pvMatrixPointer & 0x1F], sizeof(gx->posMatrix));
248				break;
249			case 3:
250				mLOG(DS_GX, STUB, "Unimplemented GX MTX_POP mode");
251				break;
252			}
253			break;
254		}
255		case DS_GX_CMD_SWAP_BUFFERS:
256			gx->swapBuffers = true;
257			break;
258		default:
259			mLOG(DS_GX, STUB, "Unimplemented GX command %02X:%02X %02X %02X %02X", entry.command, entry.params[0], entry.params[1], entry.params[2], entry.params[3]);
260			break;
261		}
262
263		gxstat = DSRegGXSTATSetPVMatrixStackLevel(gxstat, gx->pvMatrixPointer);
264		gxstat = DSRegGXSTATSetProjMatrixStackLevel(gxstat, projMatrixPointer);
265		gxstat = DSRegGXSTATTestFillMatrixStackError(gxstat, projMatrixPointer || gx->pvMatrixPointer >= 0x1F);
266		gx->p->memory.io9[DS9_REG_GXSTAT_LO >> 1] = gxstat;
267
268		if (cyclesLate >= cycles) {
269			cyclesLate -= cycles;
270		} else {
271			break;
272		}
273	}
274	DSGXUpdateGXSTAT(gx);
275	if (cycles && !gx->swapBuffers) {
276		mTimingSchedule(timing, &gx->fifoEvent, cycles - cyclesLate);
277	}
278}
279
280void DSGXInit(struct DSGX* gx) {
281	gx->renderer = &dummyRenderer;
282	CircleBufferInit(&gx->fifo, sizeof(struct DSGXEntry) * DS_GX_FIFO_SIZE);
283	CircleBufferInit(&gx->pipe, sizeof(struct DSGXEntry) * DS_GX_PIPE_SIZE);
284	gx->fifoEvent.name = "DS GX FIFO";
285	gx->fifoEvent.priority = 0xC;
286	gx->fifoEvent.context = gx;
287	gx->fifoEvent.callback = _fifoRun;
288}
289
290void DSGXDeinit(struct DSGX* gx) {
291	DSGXAssociateRenderer(gx, &dummyRenderer);
292	CircleBufferDeinit(&gx->fifo);
293	CircleBufferDeinit(&gx->pipe);
294}
295
296void DSGXReset(struct DSGX* gx) {
297	CircleBufferClear(&gx->fifo);
298	CircleBufferClear(&gx->pipe);
299	DSGXMtxIdentity(&gx->projMatrix);
300	DSGXMtxIdentity(&gx->texMatrix);
301	DSGXMtxIdentity(&gx->posMatrix);
302	DSGXMtxIdentity(&gx->vecMatrix);
303
304	DSGXMtxIdentity(&gx->projMatrixStack);
305	DSGXMtxIdentity(&gx->texMatrixStack);
306	int i;
307	for (i = 0; i < 32; ++i) {
308		DSGXMtxIdentity(&gx->posMatrixStack[i]);
309		DSGXMtxIdentity(&gx->vecMatrixStack[i]);
310	}
311	gx->swapBuffers = false;
312	gx->bufferIndex = 0;
313	gx->mtxMode = 0;
314	gx->pvMatrixPointer = 0;
315
316	memset(gx->outstandingParams, 0, sizeof(gx->outstandingParams));
317	memset(gx->outstandingCommand, 0, sizeof(gx->outstandingCommand));
318	gx->activeParams = 0;
319}
320
321void DSGXAssociateRenderer(struct DSGX* gx, struct DSGXRenderer* renderer) {
322	gx->renderer->deinit(gx->renderer);
323	gx->renderer = renderer;
324	gx->renderer->init(gx->renderer);
325}
326
327void DSGXUpdateGXSTAT(struct DSGX* gx) {
328	uint32_t value = gx->p->memory.io9[DS9_REG_GXSTAT_HI >> 1] << 16;
329	value = DSRegGXSTATIsDoIRQ(value);
330
331	size_t entries = CircleBufferSize(&gx->fifo) / sizeof(struct DSGXEntry);
332	// XXX
333	if (gx->swapBuffers) {
334		entries++;
335	}
336	value = DSRegGXSTATSetFIFOEntries(value, entries);
337	value = DSRegGXSTATSetFIFOLtHalf(value, entries < (DS_GX_FIFO_SIZE / 2));
338	value = DSRegGXSTATSetFIFOEmpty(value, entries == 0);
339
340	if ((DSRegGXSTATGetDoIRQ(value) == 1 && entries < (DS_GX_FIFO_SIZE / 2)) ||
341		(DSRegGXSTATGetDoIRQ(value) == 2 && entries == 0)) {
342		DSRaiseIRQ(gx->p->ds9.cpu, gx->p->ds9.memory.io, DS_IRQ_GEOM_FIFO);
343	}
344
345	value = DSRegGXSTATSetBusy(value, mTimingIsScheduled(&gx->p->ds9.timing, &gx->fifoEvent) || gx->swapBuffers);
346
347	gx->p->memory.io9[DS9_REG_GXSTAT_HI >> 1] = value >> 16;
348}
349
350static void DSGXUnpackCommand(struct DSGX* gx, uint32_t command) {
351	gx->outstandingCommand[0] = command;
352	gx->outstandingCommand[1] = command >> 8;
353	gx->outstandingCommand[2] = command >> 16;
354	gx->outstandingCommand[3] = command >> 24;
355	if (gx->outstandingCommand[0] >= DS_GX_CMD_MAX) {
356		gx->outstandingCommand[0] = 0;
357	}
358	if (gx->outstandingCommand[1] >= DS_GX_CMD_MAX) {
359		gx->outstandingCommand[1] = 0;
360	}
361	if (gx->outstandingCommand[2] >= DS_GX_CMD_MAX) {
362		gx->outstandingCommand[2] = 0;
363	}
364	if (gx->outstandingCommand[3] >= DS_GX_CMD_MAX) {
365		gx->outstandingCommand[3] = 0;
366	}
367	gx->outstandingParams[0] = _gxCommandParams[gx->outstandingCommand[0]];
368	gx->outstandingParams[1] = _gxCommandParams[gx->outstandingCommand[1]];
369	gx->outstandingParams[2] = _gxCommandParams[gx->outstandingCommand[2]];
370	gx->outstandingParams[3] = _gxCommandParams[gx->outstandingCommand[3]];
371}
372
373static void DSGXWriteFIFO(struct DSGX* gx, struct DSGXEntry entry) {
374	if (gx->outstandingParams[0]) {
375		entry.command = gx->outstandingCommand[0];
376		--gx->outstandingParams[0];
377		if (!gx->outstandingParams[0]) {
378			// TODO: improve this
379			memmove(&gx->outstandingParams[0], &gx->outstandingParams[1], sizeof(gx->outstandingParams[0]) * 3);
380			memmove(&gx->outstandingCommand[0], &gx->outstandingCommand[1], sizeof(gx->outstandingCommand[0]) * 3);
381			gx->outstandingParams[3] = 0;
382		}
383	} else {
384		gx->outstandingCommand[0] = entry.command;
385		gx->outstandingParams[0] = _gxCommandParams[entry.command];
386		if (gx->outstandingParams[0]) {
387			--gx->outstandingParams[0];
388		}
389	}
390	uint32_t cycles = _gxCommandCycleBase[entry.command];
391	if (!cycles) {
392		return;
393	}
394	if (CircleBufferSize(&gx->fifo) == 0 && CircleBufferSize(&gx->pipe) < (DS_GX_PIPE_SIZE * sizeof(entry))) {
395		CircleBufferWrite8(&gx->pipe, entry.command);
396		CircleBufferWrite8(&gx->pipe, entry.params[0]);
397		CircleBufferWrite8(&gx->pipe, entry.params[1]);
398		CircleBufferWrite8(&gx->pipe, entry.params[2]);
399		CircleBufferWrite8(&gx->pipe, entry.params[3]);
400	} else if (CircleBufferSize(&gx->fifo) < (DS_GX_FIFO_SIZE * sizeof(entry))) {
401		CircleBufferWrite8(&gx->fifo, entry.command);
402		CircleBufferWrite8(&gx->fifo, entry.params[0]);
403		CircleBufferWrite8(&gx->fifo, entry.params[1]);
404		CircleBufferWrite8(&gx->fifo, entry.params[2]);
405		CircleBufferWrite8(&gx->fifo, entry.params[3]);
406	} else {
407		mLOG(DS_GX, STUB, "Unimplemented GX full");
408	}
409	if (!gx->swapBuffers && !mTimingIsScheduled(&gx->p->ds9.timing, &gx->fifoEvent)) {
410		mTimingSchedule(&gx->p->ds9.timing, &gx->fifoEvent, cycles);
411	}
412}
413
414uint16_t DSGXWriteRegister(struct DSGX* gx, uint32_t address, uint16_t value) {
415	uint16_t oldValue = gx->p->memory.io9[address >> 1];
416	switch (address) {
417	case DS9_REG_DISP3DCNT:
418		mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%04X", address, value);
419		break;
420	case DS9_REG_GXSTAT_LO:
421		value = DSRegGXSTATIsMatrixStackError(value);
422		if (value) {
423			oldValue = DSRegGXSTATClearMatrixStackError(oldValue);
424			oldValue = DSRegGXSTATClearProjMatrixStackLevel(oldValue);
425		}
426		value = oldValue;
427		break;
428	case DS9_REG_GXSTAT_HI:
429		value = DSRegGXSTATIsDoIRQ(value << 16) >> 16;
430		gx->p->memory.io9[address >> 1] = value;
431		DSGXUpdateGXSTAT(gx);
432		value = gx->p->memory.io9[address >> 1];
433		break;
434	default:
435		if (address < DS9_REG_GXFIFO_00) {
436			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%04X", address, value);
437		} else if (address <= DS9_REG_GXFIFO_1F) {
438			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%04X", address, value);
439		} else if (address < DS9_REG_GXSTAT_LO) {
440			struct DSGXEntry entry = {
441				.command = (address & 0x1FC) >> 2,
442				.params = {
443					value,
444					value >> 8,
445				}
446			};
447			if (entry.command < DS_GX_CMD_MAX) {
448				DSGXWriteFIFO(gx, entry);
449			}
450		} else {
451			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%04X", address, value);
452		}
453		break;
454	}
455	return value;
456}
457
458uint32_t DSGXWriteRegister32(struct DSGX* gx, uint32_t address, uint32_t value) {
459	switch (address) {
460	case DS9_REG_DISP3DCNT:
461		mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%08X", address, value);
462		break;
463	case DS9_REG_GXSTAT_LO:
464		value = (value & 0xFFFF0000) | DSGXWriteRegister(gx, DS9_REG_GXSTAT_LO, value);
465		value = (value & 0x0000FFFF) | (DSGXWriteRegister(gx, DS9_REG_GXSTAT_HI, value >> 16) << 16);
466		break;
467	default:
468		if (address < DS9_REG_GXFIFO_00) {
469			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%08X", address, value);
470		} else if (address <= DS9_REG_GXFIFO_1F) {
471			if (gx->outstandingParams[0]) {
472				struct DSGXEntry entry = {
473					.command = gx->outstandingCommand[0],
474					.params = {
475						value,
476						value >> 8,
477						value >> 16,
478						value >> 24
479					}
480				};
481				DSGXWriteFIFO(gx, entry);
482			} else {
483				DSGXUnpackCommand(gx, value);
484			}
485		} else if (address < DS9_REG_GXSTAT_LO) {
486			struct DSGXEntry entry = {
487				.command = (address & 0x1FC) >> 2,
488				.params = {
489					value,
490					value >> 8,
491					value >> 16,
492					value >> 24
493				}
494			};
495			DSGXWriteFIFO(gx, entry);
496		} else {
497			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%08X", address, value);
498		}
499		break;
500	}
501	return value;
502}
503
504void DSGXSwapBuffers(struct DSGX* gx) {
505	mLOG(DS_GX, STUB, "Unimplemented GX swap buffers");
506	gx->swapBuffers = false;
507
508	// TODO
509	DSGXUpdateGXSTAT(gx);
510	if (CircleBufferSize(&gx->fifo)) {
511		mTimingSchedule(&gx->p->ds9.timing, &gx->fifoEvent, 0);
512	}
513}
514
515static void DSGXDummyRendererInit(struct DSGXRenderer* renderer) {
516	UNUSED(renderer);
517	// Nothing to do
518}
519
520static void DSGXDummyRendererReset(struct DSGXRenderer* renderer) {
521	UNUSED(renderer);
522	// Nothing to do
523}
524
525static void DSGXDummyRendererDeinit(struct DSGXRenderer* renderer) {
526	UNUSED(renderer);
527	// Nothing to do
528}
529
530static void DSGXDummyRendererSetRAM(struct DSGXRenderer* renderer, struct DSGXVertex* verts, struct DSGXPolygon* polys, unsigned polyCount) {
531	UNUSED(renderer);
532	UNUSED(verts);
533	UNUSED(polys);
534	UNUSED(polyCount);
535	// Nothing to do
536}
537
538static void DSGXDummyRendererDrawScanline(struct DSGXRenderer* renderer, int y) {
539	UNUSED(renderer);
540	UNUSED(y);
541	// Nothing to do
542}
543
544static void DSGXDummyRendererGetScanline(struct DSGXRenderer* renderer, int y, color_t** output) {
545	UNUSED(renderer);
546	UNUSED(y);
547	*output = NULL;
548	// Nothing to do
549}