all repos — mgba @ 338d29f5adb7e4c132dc6b5d6b00a4d20c2bc243

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 (true) {
144		if (gx->swapBuffers) {
145			break;
146		}
147
148		DSRegGXSTAT gxstat = gx->p->memory.io9[DS9_REG_GXSTAT_LO >> 1];
149		int pvMatrixPointer = DSRegGXSTATGetPVMatrixStackLevel(gxstat);
150		int projMatrixPointer = DSRegGXSTATGetProjMatrixStackLevel(gxstat);
151
152		if (CircleBufferSize(&gx->pipe) <= 2 * sizeof(struct DSGXEntry)) {
153			_pullPipe(gx);
154		}
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		switch (entry.command) {
172		case DS_GX_CMD_MTX_MODE:
173			if (entry.params[0] < 4) {
174				gx->mtxMode = entry.params[0];
175			} else {
176				mLOG(DS_GX, GAME_ERROR, "Invalid GX MTX_MODE %02X", entry.params[0]);
177			}
178			break;
179		case DS_GX_CMD_MTX_IDENTITY:
180			switch (gx->mtxMode) {
181			case 0:
182				DSGXMtxIdentity(&gx->projMatrix);
183				break;
184			case 2:
185				DSGXMtxIdentity(&gx->vecMatrix);
186				// Fall through
187			case 1:
188				DSGXMtxIdentity(&gx->posMatrix);
189				break;
190			case 3:
191				DSGXMtxIdentity(&gx->texMatrix);
192				break;
193			}
194			break;
195		case DS_GX_CMD_MTX_PUSH:
196			switch (gx->mtxMode) {
197			case 0:
198				memcpy(&gx->projMatrixStack, &gx->projMatrix, sizeof(gx->projMatrix));
199				++projMatrixPointer;
200				break;
201			case 2:
202				memcpy(&gx->vecMatrixStack[pvMatrixPointer & 0x1F], &gx->vecMatrix, sizeof(gx->vecMatrix));
203				// Fall through
204			case 1:
205				memcpy(&gx->posMatrixStack[pvMatrixPointer & 0x1F], &gx->posMatrix, sizeof(gx->posMatrix));
206				++pvMatrixPointer;
207				break;
208			case 3:
209				mLOG(DS_GX, STUB, "Unimplemented GX MTX_PUSH mode");
210				break;
211			}
212			break;
213		case DS_GX_CMD_MTX_POP: {
214			int8_t offset = entry.params[0];
215			offset <<= 2;
216			offset >>= 2;
217			switch (gx->mtxMode) {
218			case 0:
219				projMatrixPointer -= offset;
220				memcpy(&gx->projMatrix, &gx->projMatrixStack, sizeof(gx->projMatrix));
221				break;
222			case 1:
223				pvMatrixPointer -= offset;
224				memcpy(&gx->posMatrix, &gx->posMatrixStack[pvMatrixPointer & 0x1F], sizeof(gx->posMatrix));
225				break;
226			case 2:
227				pvMatrixPointer -= offset;
228				memcpy(&gx->vecMatrix, &gx->vecMatrixStack[pvMatrixPointer & 0x1F], sizeof(gx->vecMatrix));
229				memcpy(&gx->posMatrix, &gx->posMatrixStack[pvMatrixPointer & 0x1F], sizeof(gx->posMatrix));
230				break;
231			case 3:
232				mLOG(DS_GX, STUB, "Unimplemented GX MTX_POP mode");
233				break;
234			}
235			break;
236		}
237		case DS_GX_CMD_SWAP_BUFFERS:
238			gx->swapBuffers = true;
239			break;
240		default:
241			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]);
242			break;
243		}
244
245		gxstat = DSRegGXSTATSetPVMatrixStackLevel(gxstat, pvMatrixPointer);
246		gxstat = DSRegGXSTATSetProjMatrixStackLevel(gxstat, projMatrixPointer);
247		gxstat = DSRegGXSTATTestFillMatrixStackError(gxstat, projMatrixPointer || pvMatrixPointer >= 0x1F);
248		gx->p->memory.io9[DS9_REG_GXSTAT_LO >> 1] = gxstat;
249
250		if (cyclesLate >= cycles) {
251			cyclesLate -= cycles;
252		}
253		if (!CircleBufferSize(&gx->pipe)) {
254			cycles = 0;
255			break;
256		}
257	}
258	DSGXUpdateGXSTAT(gx);
259	if (cycles && !gx->swapBuffers) {
260		mTimingSchedule(timing, &gx->fifoEvent, cycles - cyclesLate);
261	}
262}
263
264void DSGXInit(struct DSGX* gx) {
265	gx->renderer = &dummyRenderer;
266	CircleBufferInit(&gx->fifo, sizeof(struct DSGXEntry) * DS_GX_FIFO_SIZE);
267	CircleBufferInit(&gx->pipe, sizeof(struct DSGXEntry) * DS_GX_PIPE_SIZE);
268	gx->fifoEvent.name = "DS GX FIFO";
269	gx->fifoEvent.priority = 0xC;
270	gx->fifoEvent.context = gx;
271	gx->fifoEvent.callback = _fifoRun;
272}
273
274void DSGXDeinit(struct DSGX* gx) {
275	DSGXAssociateRenderer(gx, &dummyRenderer);
276	CircleBufferDeinit(&gx->fifo);
277	CircleBufferDeinit(&gx->pipe);
278}
279
280void DSGXReset(struct DSGX* gx) {
281	CircleBufferClear(&gx->fifo);
282	CircleBufferClear(&gx->pipe);
283	DSGXMtxIdentity(&gx->projMatrix);
284	DSGXMtxIdentity(&gx->texMatrix);
285	DSGXMtxIdentity(&gx->posMatrix);
286	DSGXMtxIdentity(&gx->vecMatrix);
287
288	DSGXMtxIdentity(&gx->projMatrixStack);
289	DSGXMtxIdentity(&gx->texMatrixStack);
290	int i;
291	for (i = 0; i < 32; ++i) {
292		DSGXMtxIdentity(&gx->posMatrixStack[i]);
293		DSGXMtxIdentity(&gx->vecMatrixStack[i]);
294	}
295	gx->swapBuffers = false;
296	gx->bufferIndex = 0;
297	gx->mtxMode = 0;
298
299	memset(gx->outstandingParams, 0, sizeof(gx->outstandingParams));
300	memset(gx->outstandingCommand, 0, sizeof(gx->outstandingCommand));
301}
302
303void DSGXAssociateRenderer(struct DSGX* gx, struct DSGXRenderer* renderer) {
304	gx->renderer->deinit(gx->renderer);
305	gx->renderer = renderer;
306	gx->renderer->init(gx->renderer);
307}
308
309void DSGXUpdateGXSTAT(struct DSGX* gx) {
310	uint32_t value = gx->p->memory.io9[DS9_REG_GXSTAT_HI >> 1] << 16;
311	value = DSRegGXSTATIsDoIRQ(value);
312
313	size_t entries = CircleBufferSize(&gx->fifo) / sizeof(struct DSGXEntry);
314	// XXX
315	if (gx->swapBuffers) {
316		entries++;
317	}
318	value = DSRegGXSTATSetFIFOEntries(value, entries);
319	value = DSRegGXSTATSetFIFOLtHalf(value, entries < (DS_GX_FIFO_SIZE / 2));
320	value = DSRegGXSTATSetFIFOEmpty(value, entries == 0);
321
322	if ((DSRegGXSTATGetDoIRQ(value) == 1 && entries < (DS_GX_FIFO_SIZE / 2)) ||
323		(DSRegGXSTATGetDoIRQ(value) == 2 && entries == 0)) {
324		DSRaiseIRQ(gx->p->ds9.cpu, gx->p->ds9.memory.io, DS_IRQ_GEOM_FIFO);
325	}
326
327	value = DSRegGXSTATSetBusy(value, mTimingIsScheduled(&gx->p->ds9.timing, &gx->fifoEvent) || gx->swapBuffers);
328
329	gx->p->memory.io9[DS9_REG_GXSTAT_HI >> 1] = value >> 16;
330}
331
332static void DSGXUnpackCommand(struct DSGX* gx, uint32_t command) {
333	gx->outstandingCommand[0] = command;
334	gx->outstandingCommand[1] = command >> 8;
335	gx->outstandingCommand[2] = command >> 16;
336	gx->outstandingCommand[3] = command >> 24;
337	if (gx->outstandingCommand[0] >= DS_GX_CMD_MAX) {
338		gx->outstandingCommand[0] = 0;
339	}
340	if (gx->outstandingCommand[1] >= DS_GX_CMD_MAX) {
341		gx->outstandingCommand[1] = 0;
342	}
343	if (gx->outstandingCommand[2] >= DS_GX_CMD_MAX) {
344		gx->outstandingCommand[2] = 0;
345	}
346	if (gx->outstandingCommand[3] >= DS_GX_CMD_MAX) {
347		gx->outstandingCommand[3] = 0;
348	}
349	gx->outstandingParams[0] = _gxCommandParams[gx->outstandingCommand[0]];
350	gx->outstandingParams[1] = _gxCommandParams[gx->outstandingCommand[1]];
351	gx->outstandingParams[2] = _gxCommandParams[gx->outstandingCommand[2]];
352	gx->outstandingParams[3] = _gxCommandParams[gx->outstandingCommand[3]];
353}
354
355static void DSGXWriteFIFO(struct DSGX* gx, struct DSGXEntry entry) {
356	if (gx->outstandingParams[0]) {
357		entry.command = gx->outstandingCommand[0];
358		--gx->outstandingParams[0];
359		if (!gx->outstandingParams[0]) {
360			// TODO: improve this
361			memmove(&gx->outstandingParams[0], &gx->outstandingParams[1], sizeof(gx->outstandingParams[0]) * 3);
362			memmove(&gx->outstandingCommand[0], &gx->outstandingCommand[1], sizeof(gx->outstandingCommand[0]) * 3);
363			gx->outstandingParams[3] = 0;
364		}
365	} else {
366		gx->outstandingCommand[0] = entry.command;
367		gx->outstandingParams[0] = _gxCommandParams[entry.command];
368		if (gx->outstandingParams[0]) {
369			--gx->outstandingParams[0];
370		}
371	}
372	uint32_t cycles = _gxCommandCycleBase[entry.command];
373	if (!cycles) {
374		return;
375	}
376	if (CircleBufferSize(&gx->fifo) == 0 && CircleBufferSize(&gx->pipe) < (DS_GX_PIPE_SIZE * sizeof(entry))) {
377		CircleBufferWrite8(&gx->pipe, entry.command);
378		CircleBufferWrite8(&gx->pipe, entry.params[0]);
379		CircleBufferWrite8(&gx->pipe, entry.params[1]);
380		CircleBufferWrite8(&gx->pipe, entry.params[2]);
381		CircleBufferWrite8(&gx->pipe, entry.params[3]);
382	} else if (CircleBufferSize(&gx->fifo) < (DS_GX_FIFO_SIZE * sizeof(entry))) {
383		CircleBufferWrite8(&gx->fifo, entry.command);
384		CircleBufferWrite8(&gx->fifo, entry.params[0]);
385		CircleBufferWrite8(&gx->fifo, entry.params[1]);
386		CircleBufferWrite8(&gx->fifo, entry.params[2]);
387		CircleBufferWrite8(&gx->fifo, entry.params[3]);
388	} else {
389		mLOG(DS_GX, STUB, "Unimplemented GX full");
390	}
391	if (!gx->swapBuffers && !mTimingIsScheduled(&gx->p->ds9.timing, &gx->fifoEvent)) {
392		mTimingSchedule(&gx->p->ds9.timing, &gx->fifoEvent, cycles);
393	}
394}
395
396uint16_t DSGXWriteRegister(struct DSGX* gx, uint32_t address, uint16_t value) {
397	uint16_t oldValue = gx->p->memory.io9[address >> 1];
398	switch (address) {
399	case DS9_REG_DISP3DCNT:
400		mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%04X", address, value);
401		break;
402	case DS9_REG_GXSTAT_LO:
403		value = DSRegGXSTATIsMatrixStackError(value);
404		if (value) {
405			oldValue = DSRegGXSTATClearMatrixStackError(oldValue);
406			oldValue = DSRegGXSTATClearProjMatrixStackLevel(oldValue);
407		}
408		value = oldValue;
409		break;
410	case DS9_REG_GXSTAT_HI:
411		value = DSRegGXSTATIsDoIRQ(value << 16) >> 16;
412		gx->p->memory.io9[address >> 1] = value;
413		DSGXUpdateGXSTAT(gx);
414		value = gx->p->memory.io9[address >> 1];
415		break;
416	default:
417		if (address < DS9_REG_GXFIFO_00) {
418			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%04X", address, value);
419		} else if (address <= DS9_REG_GXFIFO_1F) {
420			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%04X", address, value);
421		} else if (address < DS9_REG_GXSTAT_LO) {
422			struct DSGXEntry entry = {
423				.command = (address & 0x1FC) >> 2,
424				.params = {
425					value,
426					value >> 8,
427				}
428			};
429			if (entry.command < DS_GX_CMD_MAX) {
430				DSGXWriteFIFO(gx, entry);
431			}
432		} else {
433			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%04X", address, value);
434		}
435		break;
436	}
437	return value;
438}
439
440uint32_t DSGXWriteRegister32(struct DSGX* gx, uint32_t address, uint32_t value) {
441	switch (address) {
442	case DS9_REG_DISP3DCNT:
443		mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%08X", address, value);
444		break;
445	case DS9_REG_GXSTAT_LO:
446		value = (value & 0xFFFF0000) | DSGXWriteRegister(gx, DS9_REG_GXSTAT_LO, value);
447		value = (value & 0x0000FFFF) | (DSGXWriteRegister(gx, DS9_REG_GXSTAT_HI, value >> 16) << 16);
448		break;
449	default:
450		if (address < DS9_REG_GXFIFO_00) {
451			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%08X", address, value);
452		} else if (address <= DS9_REG_GXFIFO_1F) {
453			if (gx->outstandingParams[0]) {
454				struct DSGXEntry entry = {
455					.command = gx->outstandingCommand[0],
456					.params = {
457						value,
458						value >> 8,
459						value >> 16,
460						value >> 24
461					}
462				};
463				DSGXWriteFIFO(gx, entry);
464			} else {
465				DSGXUnpackCommand(gx, value);
466			}
467		} else if (address < DS9_REG_GXSTAT_LO) {
468			struct DSGXEntry entry = {
469				.command = (address & 0x1FC) >> 2,
470				.params = {
471					value,
472					value >> 8,
473					value >> 16,
474					value >> 24
475				}
476			};
477			DSGXWriteFIFO(gx, entry);
478		} else {
479			mLOG(DS_GX, STUB, "Unimplemented GX write %03X:%08X", address, value);
480		}
481		break;
482	}
483	return value;
484}
485
486void DSGXSwapBuffers(struct DSGX* gx) {
487	mLOG(DS_GX, STUB, "Unimplemented GX swap buffers");
488	gx->swapBuffers = false;
489
490	// TODO
491	DSGXUpdateGXSTAT(gx);
492	if (CircleBufferSize(&gx->fifo)) {
493		mTimingSchedule(&gx->p->ds9.timing, &gx->fifoEvent, 0);
494	}
495}
496
497static void DSGXDummyRendererInit(struct DSGXRenderer* renderer) {
498	UNUSED(renderer);
499	// Nothing to do
500}
501
502static void DSGXDummyRendererReset(struct DSGXRenderer* renderer) {
503	UNUSED(renderer);
504	// Nothing to do
505}
506
507static void DSGXDummyRendererDeinit(struct DSGXRenderer* renderer) {
508	UNUSED(renderer);
509	// Nothing to do
510}
511
512static void DSGXDummyRendererSetRAM(struct DSGXRenderer* renderer, struct DSGXVertex* verts, struct DSGXPolygon* polys, unsigned polyCount) {
513	UNUSED(renderer);
514	UNUSED(verts);
515	UNUSED(polys);
516	UNUSED(polyCount);
517	// Nothing to do
518}
519
520static void DSGXDummyRendererDrawScanline(struct DSGXRenderer* renderer, int y) {
521	UNUSED(renderer);
522	UNUSED(y);
523	// Nothing to do
524}
525
526static void DSGXDummyRendererGetScanline(struct DSGXRenderer* renderer, int y, color_t** output) {
527	UNUSED(renderer);
528	UNUSED(y);
529	*output = NULL;
530	// Nothing to do
531}