all repos — mgba @ 2ab7289a059ab3055f00f0677f7c20bfed7591d5

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 pvMatrixPointer = DSRegGXSTATGetPVMatrixStackLevel(gxstat);
155		int projMatrixPointer = DSRegGXSTATGetProjMatrixStackLevel(gxstat);
156
157		struct DSGXEntry entry = { 0 };
158		CircleBufferDump(&gx->pipe, (int8_t*) &entry.command, 1);
159		cycles = _gxCommandCycleBase[entry.command];
160
161		if (first) {
162			first = false;
163		} else if (cycles > cyclesLate) {
164			break;
165		}
166		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.command);
167		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.params[0]);
168		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.params[1]);
169		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.params[2]);
170		CircleBufferRead8(&gx->pipe, (int8_t*) &entry.params[3]);
171
172		if (gx->activeParams) {
173			int index = _gxCommandParams[entry.command] - gx->activeParams;
174			gx->activeEntries[index] = entry;
175			--gx->activeParams;
176		} else {
177			gx->activeParams = _gxCommandParams[entry.command];
178			if (gx->activeParams) {
179				--gx->activeParams;
180			}
181			if (gx->activeParams) {
182				gx->activeEntries[0] = entry;
183			}
184		}
185
186		if (gx->activeParams) {
187			continue;
188		}
189
190		switch (entry.command) {
191		case DS_GX_CMD_MTX_MODE:
192			if (entry.params[0] < 4) {
193				gx->mtxMode = entry.params[0];
194			} else {
195				mLOG(DS_GX, GAME_ERROR, "Invalid GX MTX_MODE %02X", entry.params[0]);
196			}
197			break;
198		case DS_GX_CMD_MTX_IDENTITY:
199			switch (gx->mtxMode) {
200			case 0:
201				DSGXMtxIdentity(&gx->projMatrix);
202				break;
203			case 2:
204				DSGXMtxIdentity(&gx->vecMatrix);
205				// Fall through
206			case 1:
207				DSGXMtxIdentity(&gx->posMatrix);
208				break;
209			case 3:
210				DSGXMtxIdentity(&gx->texMatrix);
211				break;
212			}
213			break;
214		case DS_GX_CMD_MTX_PUSH:
215			switch (gx->mtxMode) {
216			case 0:
217				memcpy(&gx->projMatrixStack, &gx->projMatrix, sizeof(gx->projMatrix));
218				++projMatrixPointer;
219				break;
220			case 2:
221				memcpy(&gx->vecMatrixStack[pvMatrixPointer & 0x1F], &gx->vecMatrix, sizeof(gx->vecMatrix));
222				// Fall through
223			case 1:
224				memcpy(&gx->posMatrixStack[pvMatrixPointer & 0x1F], &gx->posMatrix, sizeof(gx->posMatrix));
225				++pvMatrixPointer;
226				break;
227			case 3:
228				mLOG(DS_GX, STUB, "Unimplemented GX MTX_PUSH mode");
229				break;
230			}
231			break;
232		case DS_GX_CMD_MTX_POP: {
233			int8_t offset = entry.params[0];
234			offset <<= 2;
235			offset >>= 2;
236			switch (gx->mtxMode) {
237			case 0:
238				projMatrixPointer -= offset;
239				memcpy(&gx->projMatrix, &gx->projMatrixStack, sizeof(gx->projMatrix));
240				break;
241			case 1:
242				pvMatrixPointer -= offset;
243				memcpy(&gx->posMatrix, &gx->posMatrixStack[pvMatrixPointer & 0x1F], sizeof(gx->posMatrix));
244				break;
245			case 2:
246				pvMatrixPointer -= offset;
247				memcpy(&gx->vecMatrix, &gx->vecMatrixStack[pvMatrixPointer & 0x1F], sizeof(gx->vecMatrix));
248				memcpy(&gx->posMatrix, &gx->posMatrixStack[pvMatrixPointer & 0x1F], sizeof(gx->posMatrix));
249				break;
250			case 3:
251				mLOG(DS_GX, STUB, "Unimplemented GX MTX_POP mode");
252				break;
253			}
254			break;
255		}
256		case DS_GX_CMD_SWAP_BUFFERS:
257			gx->swapBuffers = true;
258			break;
259		default:
260			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]);
261			break;
262		}
263
264		gxstat = DSRegGXSTATSetPVMatrixStackLevel(gxstat, pvMatrixPointer);
265		gxstat = DSRegGXSTATSetProjMatrixStackLevel(gxstat, projMatrixPointer);
266		gxstat = DSRegGXSTATTestFillMatrixStackError(gxstat, projMatrixPointer || pvMatrixPointer >= 0x1F);
267		gx->p->memory.io9[DS9_REG_GXSTAT_LO >> 1] = gxstat;
268
269		if (cyclesLate >= cycles) {
270			cyclesLate -= cycles;
271		} else {
272			break;
273		}
274	}
275	DSGXUpdateGXSTAT(gx);
276	if (cycles && !gx->swapBuffers) {
277		mTimingSchedule(timing, &gx->fifoEvent, cycles - cyclesLate);
278	}
279}
280
281void DSGXInit(struct DSGX* gx) {
282	gx->renderer = &dummyRenderer;
283	CircleBufferInit(&gx->fifo, sizeof(struct DSGXEntry) * DS_GX_FIFO_SIZE);
284	CircleBufferInit(&gx->pipe, sizeof(struct DSGXEntry) * DS_GX_PIPE_SIZE);
285	gx->fifoEvent.name = "DS GX FIFO";
286	gx->fifoEvent.priority = 0xC;
287	gx->fifoEvent.context = gx;
288	gx->fifoEvent.callback = _fifoRun;
289}
290
291void DSGXDeinit(struct DSGX* gx) {
292	DSGXAssociateRenderer(gx, &dummyRenderer);
293	CircleBufferDeinit(&gx->fifo);
294	CircleBufferDeinit(&gx->pipe);
295}
296
297void DSGXReset(struct DSGX* gx) {
298	CircleBufferClear(&gx->fifo);
299	CircleBufferClear(&gx->pipe);
300	DSGXMtxIdentity(&gx->projMatrix);
301	DSGXMtxIdentity(&gx->texMatrix);
302	DSGXMtxIdentity(&gx->posMatrix);
303	DSGXMtxIdentity(&gx->vecMatrix);
304
305	DSGXMtxIdentity(&gx->projMatrixStack);
306	DSGXMtxIdentity(&gx->texMatrixStack);
307	int i;
308	for (i = 0; i < 32; ++i) {
309		DSGXMtxIdentity(&gx->posMatrixStack[i]);
310		DSGXMtxIdentity(&gx->vecMatrixStack[i]);
311	}
312	gx->swapBuffers = false;
313	gx->bufferIndex = 0;
314	gx->mtxMode = 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}