src/gba/gba-thread.c (view raw)
1/* Copyright (c) 2013-2014 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 "gba-thread.h"
7
8#include "arm.h"
9#include "gba.h"
10#include "gba-config.h"
11#include "gba-serialize.h"
12
13#include "debugger/debugger.h"
14
15#include "util/patch.h"
16#include "util/png-io.h"
17#include "util/vfs.h"
18
19#include "platform/commandline.h"
20
21#include <signal.h>
22
23static const float _defaultFPSTarget = 60.f;
24
25#ifdef USE_PTHREADS
26static pthread_key_t _contextKey;
27static pthread_once_t _contextOnce = PTHREAD_ONCE_INIT;
28
29static void _createTLS(void) {
30 pthread_key_create(&_contextKey, 0);
31}
32#elif _WIN32
33static DWORD _contextKey;
34static INIT_ONCE _contextOnce = INIT_ONCE_STATIC_INIT;
35
36static BOOL CALLBACK _createTLS(PINIT_ONCE once, PVOID param, PVOID* context) {
37 UNUSED(once);
38 UNUSED(param);
39 UNUSED(context);
40 _contextKey = TlsAlloc();
41 return TRUE;
42}
43#endif
44
45#ifndef DISABLE_THREADING
46static void _changeState(struct GBAThread* threadContext, enum ThreadState newState, bool broadcast) {
47 MutexLock(&threadContext->stateMutex);
48 threadContext->state = newState;
49 if (broadcast) {
50 ConditionWake(&threadContext->stateCond);
51 }
52 MutexUnlock(&threadContext->stateMutex);
53}
54
55static void _waitOnInterrupt(struct GBAThread* threadContext) {
56 while (threadContext->state == THREAD_INTERRUPTED) {
57 ConditionWait(&threadContext->stateCond, &threadContext->stateMutex);
58 }
59}
60
61static void _waitUntilNotState(struct GBAThread* threadContext, enum ThreadState oldState) {
62 while (threadContext->state == oldState) {
63 MutexUnlock(&threadContext->stateMutex);
64
65 MutexLock(&threadContext->sync.videoFrameMutex);
66 ConditionWake(&threadContext->sync.videoFrameRequiredCond);
67 MutexUnlock(&threadContext->sync.videoFrameMutex);
68
69 MutexLock(&threadContext->sync.audioBufferMutex);
70 ConditionWake(&threadContext->sync.audioRequiredCond);
71 MutexUnlock(&threadContext->sync.audioBufferMutex);
72
73 MutexLock(&threadContext->stateMutex);
74 ConditionWake(&threadContext->stateCond);
75 }
76}
77
78static void _pauseThread(struct GBAThread* threadContext, bool onThread) {
79 if (threadContext->debugger && threadContext->debugger->state == DEBUGGER_RUNNING) {
80 threadContext->debugger->state = DEBUGGER_EXITING;
81 }
82 threadContext->state = THREAD_PAUSING;
83 if (!onThread) {
84 _waitUntilNotState(threadContext, THREAD_PAUSING);
85 }
86}
87
88static void _changeVideoSync(struct GBASync* sync, bool frameOn) {
89 // Make sure the video thread can process events while the GBA thread is paused
90 MutexLock(&sync->videoFrameMutex);
91 if (frameOn != sync->videoFrameOn) {
92 sync->videoFrameOn = frameOn;
93 ConditionWake(&sync->videoFrameAvailableCond);
94 }
95 MutexUnlock(&sync->videoFrameMutex);
96}
97
98static THREAD_ENTRY _GBAThreadRun(void* context) {
99#ifdef USE_PTHREADS
100 pthread_once(&_contextOnce, _createTLS);
101#else
102 InitOnceExecuteOnce(&_contextOnce, _createTLS, NULL, 0);
103#endif
104
105 struct GBA gba;
106 struct ARMCore cpu;
107 struct Patch patch;
108 struct GBAThread* threadContext = context;
109 struct ARMComponent* components[1] = {};
110 int numComponents = 0;
111
112 if (threadContext->debugger) {
113 components[numComponents] = &threadContext->debugger->d;
114 ++numComponents;
115 }
116
117#if !defined(_WIN32) && defined(USE_PTHREADS)
118 sigset_t signals;
119 sigemptyset(&signals);
120 pthread_sigmask(SIG_SETMASK, &signals, 0);
121#endif
122
123 GBACreate(&gba);
124 ARMSetComponents(&cpu, &gba.d, numComponents, components);
125 ARMInit(&cpu);
126 gba.sync = &threadContext->sync;
127 threadContext->gba = &gba;
128 gba.logLevel = threadContext->logLevel;
129#ifdef USE_PTHREADS
130 pthread_setspecific(_contextKey, threadContext);
131#else
132 TlsSetValue(_contextKey, threadContext);
133#endif
134
135 if (threadContext->audioBuffers) {
136 GBAAudioResizeBuffer(&gba.audio, threadContext->audioBuffers);
137 } else {
138 threadContext->audioBuffers = GBA_AUDIO_SAMPLES;
139 }
140
141 if (threadContext->renderer) {
142 GBAVideoAssociateRenderer(&gba.video, threadContext->renderer);
143 }
144
145 if (threadContext->rom) {
146 GBALoadROM(&gba, threadContext->rom, threadContext->save, threadContext->fname);
147 if (threadContext->bios) {
148 GBALoadBIOS(&gba, threadContext->bios);
149 }
150
151 if (threadContext->patch && loadPatch(threadContext->patch, &patch)) {
152 GBAApplyPatch(&gba, &patch);
153 }
154 }
155
156 ARMReset(&cpu);
157
158 if (threadContext->debugger) {
159 threadContext->debugger->log = GBADebuggerLogShim;
160 GBAAttachDebugger(&gba, threadContext->debugger);
161 ARMDebuggerEnter(threadContext->debugger, DEBUGGER_ENTER_ATTACHED);
162 }
163
164 GBASIOSetDriverSet(&gba.sio, &threadContext->sioDrivers);
165
166 gba.keySource = &threadContext->activeKeys;
167
168 if (threadContext->startCallback) {
169 threadContext->startCallback(threadContext);
170 }
171
172 _changeState(threadContext, THREAD_RUNNING, true);
173
174 while (threadContext->state < THREAD_EXITING) {
175 if (threadContext->debugger) {
176 struct ARMDebugger* debugger = threadContext->debugger;
177 ARMDebuggerRun(debugger);
178 if (debugger->state == DEBUGGER_SHUTDOWN) {
179 _changeState(threadContext, THREAD_EXITING, false);
180 }
181 } else {
182 while (threadContext->state == THREAD_RUNNING) {
183 ARMRunLoop(&cpu);
184 }
185 }
186
187 int resetScheduled = 0;
188 MutexLock(&threadContext->stateMutex);
189 while (threadContext->state > THREAD_RUNNING && threadContext->state < THREAD_EXITING) {
190 if (threadContext->state == THREAD_PAUSING) {
191 threadContext->state = THREAD_PAUSED;
192 ConditionWake(&threadContext->stateCond);
193 }
194 if (threadContext->state == THREAD_INTERRUPTING) {
195 threadContext->state = THREAD_INTERRUPTED;
196 ConditionWake(&threadContext->stateCond);
197 }
198 if (threadContext->state == THREAD_RESETING) {
199 threadContext->state = THREAD_RUNNING;
200 resetScheduled = 1;
201 }
202 while (threadContext->state == THREAD_PAUSED || threadContext->state == THREAD_INTERRUPTED) {
203 ConditionWait(&threadContext->stateCond, &threadContext->stateMutex);
204 }
205 }
206 MutexUnlock(&threadContext->stateMutex);
207 if (resetScheduled) {
208 ARMReset(&cpu);
209 }
210 }
211
212 while (threadContext->state != THREAD_SHUTDOWN) {
213 _changeState(threadContext, THREAD_SHUTDOWN, false);
214 }
215
216 if (threadContext->cleanCallback) {
217 threadContext->cleanCallback(threadContext);
218 }
219
220 threadContext->gba = 0;
221 ARMDeinit(&cpu);
222 GBADestroy(&gba);
223
224 threadContext->sync.videoFrameOn = false;
225 ConditionWake(&threadContext->sync.videoFrameAvailableCond);
226 ConditionWake(&threadContext->sync.audioRequiredCond);
227
228 return 0;
229}
230
231void GBAMapOptionsToContext(const struct GBAOptions* opts, struct GBAThread* threadContext) {
232 threadContext->bios = VFileOpen(opts->bios, O_RDONLY);
233 threadContext->frameskip = opts->frameskip;
234 threadContext->logLevel = opts->logLevel;
235 threadContext->rewindBufferCapacity = opts->rewindBufferCapacity;
236 threadContext->rewindBufferInterval = opts->rewindBufferInterval;
237 threadContext->sync.audioWait = opts->audioSync;
238 threadContext->sync.videoFrameWait = opts->videoSync;
239
240 if (opts->fpsTarget) {
241 threadContext->fpsTarget = opts->fpsTarget;
242 }
243
244 if (opts->audioBuffers) {
245 threadContext->audioBuffers = opts->audioBuffers;
246 }
247}
248
249void GBAMapArgumentsToContext(const struct GBAArguments* args, struct GBAThread* threadContext) {
250 if (args->dirmode) {
251 threadContext->gameDir = VDirOpen(args->fname);
252 threadContext->stateDir = threadContext->gameDir;
253 } else {
254 threadContext->rom = VFileOpen(args->fname, O_RDONLY);
255#if ENABLE_LIBZIP
256 threadContext->gameDir = VDirOpenZip(args->fname, 0);
257#endif
258 }
259 threadContext->fname = args->fname;
260 threadContext->patch = VFileOpen(args->patch, O_RDONLY);
261}
262
263bool GBAThreadStart(struct GBAThread* threadContext) {
264 // TODO: error check
265 threadContext->activeKeys = 0;
266 threadContext->state = THREAD_INITIALIZED;
267 threadContext->sync.videoFrameOn = true;
268 threadContext->sync.videoFrameSkip = 0;
269
270 threadContext->rewindBufferNext = threadContext->rewindBufferInterval;
271 threadContext->rewindBufferSize = 0;
272 if (threadContext->rewindBufferCapacity) {
273 threadContext->rewindBuffer = calloc(threadContext->rewindBufferCapacity, sizeof(void*));
274 } else {
275 threadContext->rewindBuffer = 0;
276 }
277
278 if (!threadContext->fpsTarget) {
279 threadContext->fpsTarget = _defaultFPSTarget;
280 }
281
282 if (threadContext->rom && !GBAIsROM(threadContext->rom)) {
283 threadContext->rom->close(threadContext->rom);
284 threadContext->rom = 0;
285 }
286
287 if (threadContext->gameDir) {
288 threadContext->gameDir->rewind(threadContext->gameDir);
289 struct VDirEntry* dirent = threadContext->gameDir->listNext(threadContext->gameDir);
290 while (dirent) {
291 struct Patch patchTemp;
292 struct VFile* vf = threadContext->gameDir->openFile(threadContext->gameDir, dirent->name(dirent), O_RDONLY);
293 if (!vf) {
294 continue;
295 }
296 if (!threadContext->rom && GBAIsROM(vf)) {
297 threadContext->rom = vf;
298 } else if (!threadContext->patch && loadPatch(vf, &patchTemp)) {
299 threadContext->patch = vf;
300 } else {
301 vf->close(vf);
302 }
303 dirent = threadContext->gameDir->listNext(threadContext->gameDir);
304 }
305
306 }
307
308 if (!threadContext->rom) {
309 threadContext->state = THREAD_SHUTDOWN;
310 return false;
311 }
312
313 threadContext->save = VDirOptionalOpenFile(threadContext->stateDir, threadContext->fname, "sram", ".sav", O_CREAT | O_RDWR);
314
315 MutexInit(&threadContext->stateMutex);
316 ConditionInit(&threadContext->stateCond);
317
318 MutexInit(&threadContext->sync.videoFrameMutex);
319 ConditionInit(&threadContext->sync.videoFrameAvailableCond);
320 ConditionInit(&threadContext->sync.videoFrameRequiredCond);
321 MutexInit(&threadContext->sync.audioBufferMutex);
322 ConditionInit(&threadContext->sync.audioRequiredCond);
323
324 threadContext->interruptDepth = 0;
325
326#ifndef _WIN32
327 sigset_t signals;
328 sigemptyset(&signals);
329 sigaddset(&signals, SIGINT);
330 sigaddset(&signals, SIGTRAP);
331 pthread_sigmask(SIG_BLOCK, &signals, 0);
332#endif
333
334 MutexLock(&threadContext->stateMutex);
335 ThreadCreate(&threadContext->thread, _GBAThreadRun, threadContext);
336 while (threadContext->state < THREAD_RUNNING) {
337 ConditionWait(&threadContext->stateCond, &threadContext->stateMutex);
338 }
339 MutexUnlock(&threadContext->stateMutex);
340
341 return true;
342}
343
344bool GBAThreadHasStarted(struct GBAThread* threadContext) {
345 bool hasStarted;
346 MutexLock(&threadContext->stateMutex);
347 hasStarted = threadContext->state > THREAD_INITIALIZED;
348 MutexUnlock(&threadContext->stateMutex);
349 return hasStarted;
350}
351
352void GBAThreadEnd(struct GBAThread* threadContext) {
353 MutexLock(&threadContext->stateMutex);
354 if (threadContext->debugger && threadContext->debugger->state == DEBUGGER_RUNNING) {
355 threadContext->debugger->state = DEBUGGER_EXITING;
356 }
357 threadContext->state = THREAD_EXITING;
358 ConditionWake(&threadContext->stateCond);
359 MutexUnlock(&threadContext->stateMutex);
360 MutexLock(&threadContext->sync.audioBufferMutex);
361 threadContext->sync.audioWait = 0;
362 ConditionWake(&threadContext->sync.audioRequiredCond);
363 MutexUnlock(&threadContext->sync.audioBufferMutex);
364}
365
366void GBAThreadReset(struct GBAThread* threadContext) {
367 MutexLock(&threadContext->stateMutex);
368 _waitOnInterrupt(threadContext);
369 threadContext->state = THREAD_RESETING;
370 ConditionWake(&threadContext->stateCond);
371 MutexUnlock(&threadContext->stateMutex);
372}
373
374void GBAThreadJoin(struct GBAThread* threadContext) {
375 MutexLock(&threadContext->sync.videoFrameMutex);
376 threadContext->sync.videoFrameWait = 0;
377 ConditionWake(&threadContext->sync.videoFrameRequiredCond);
378 MutexUnlock(&threadContext->sync.videoFrameMutex);
379
380 ThreadJoin(threadContext->thread);
381
382 MutexDeinit(&threadContext->stateMutex);
383 ConditionDeinit(&threadContext->stateCond);
384
385 MutexDeinit(&threadContext->sync.videoFrameMutex);
386 ConditionWake(&threadContext->sync.videoFrameAvailableCond);
387 ConditionDeinit(&threadContext->sync.videoFrameAvailableCond);
388 ConditionWake(&threadContext->sync.videoFrameRequiredCond);
389 ConditionDeinit(&threadContext->sync.videoFrameRequiredCond);
390
391 ConditionWake(&threadContext->sync.audioRequiredCond);
392 ConditionDeinit(&threadContext->sync.audioRequiredCond);
393 MutexDeinit(&threadContext->sync.audioBufferMutex);
394
395 int i;
396 for (i = 0; i < threadContext->rewindBufferCapacity; ++i) {
397 if (threadContext->rewindBuffer[i]) {
398 GBADeallocateState(threadContext->rewindBuffer[i]);
399 }
400 }
401 free(threadContext->rewindBuffer);
402
403 if (threadContext->rom) {
404 threadContext->rom->close(threadContext->rom);
405 threadContext->rom = 0;
406 }
407
408 if (threadContext->save) {
409 threadContext->save->close(threadContext->save);
410 threadContext->save = 0;
411 }
412
413 if (threadContext->bios) {
414 threadContext->bios->close(threadContext->bios);
415 threadContext->bios = 0;
416 }
417
418 if (threadContext->patch) {
419 threadContext->patch->close(threadContext->patch);
420 threadContext->patch = 0;
421 }
422
423 if (threadContext->gameDir) {
424 if (threadContext->stateDir == threadContext->gameDir) {
425 threadContext->stateDir = 0;
426 }
427 threadContext->gameDir->close(threadContext->gameDir);
428 threadContext->gameDir = 0;
429 }
430
431 if (threadContext->stateDir) {
432 threadContext->stateDir->close(threadContext->stateDir);
433 threadContext->stateDir = 0;
434 }
435}
436
437bool GBAThreadIsActive(struct GBAThread* threadContext) {
438 return threadContext->state >= THREAD_RUNNING && threadContext->state < THREAD_EXITING;
439}
440
441void GBAThreadInterrupt(struct GBAThread* threadContext) {
442 MutexLock(&threadContext->stateMutex);
443 ++threadContext->interruptDepth;
444 if (threadContext->interruptDepth > 1 || !GBAThreadIsActive(threadContext)) {
445 MutexUnlock(&threadContext->stateMutex);
446 return;
447 }
448 threadContext->savedState = threadContext->state;
449 _waitOnInterrupt(threadContext);
450 threadContext->state = THREAD_INTERRUPTING;
451 if (threadContext->debugger && threadContext->debugger->state == DEBUGGER_RUNNING) {
452 threadContext->debugger->state = DEBUGGER_EXITING;
453 }
454 ConditionWake(&threadContext->stateCond);
455 _waitUntilNotState(threadContext, THREAD_INTERRUPTING);
456 MutexUnlock(&threadContext->stateMutex);
457}
458
459void GBAThreadContinue(struct GBAThread* threadContext) {
460 MutexLock(&threadContext->stateMutex);
461 --threadContext->interruptDepth;
462 if (threadContext->interruptDepth < 1 && GBAThreadIsActive(threadContext)) {
463 threadContext->state = threadContext->savedState;
464 ConditionWake(&threadContext->stateCond);
465 }
466 MutexUnlock(&threadContext->stateMutex);
467}
468
469void GBAThreadPause(struct GBAThread* threadContext) {
470 bool frameOn = true;
471 MutexLock(&threadContext->stateMutex);
472 _waitOnInterrupt(threadContext);
473 if (threadContext->state == THREAD_RUNNING) {
474 _pauseThread(threadContext, false);
475 frameOn = false;
476 }
477 MutexUnlock(&threadContext->stateMutex);
478
479 _changeVideoSync(&threadContext->sync, frameOn);
480}
481
482void GBAThreadUnpause(struct GBAThread* threadContext) {
483 MutexLock(&threadContext->stateMutex);
484 _waitOnInterrupt(threadContext);
485 if (threadContext->state == THREAD_PAUSED || threadContext->state == THREAD_PAUSING) {
486 threadContext->state = THREAD_RUNNING;
487 ConditionWake(&threadContext->stateCond);
488 }
489 MutexUnlock(&threadContext->stateMutex);
490
491 _changeVideoSync(&threadContext->sync, true);
492}
493
494bool GBAThreadIsPaused(struct GBAThread* threadContext) {
495 bool isPaused;
496 MutexLock(&threadContext->stateMutex);
497 _waitOnInterrupt(threadContext);
498 isPaused = threadContext->state == THREAD_PAUSED;
499 MutexUnlock(&threadContext->stateMutex);
500 return isPaused;
501}
502
503void GBAThreadTogglePause(struct GBAThread* threadContext) {
504 bool frameOn = true;
505 MutexLock(&threadContext->stateMutex);
506 _waitOnInterrupt(threadContext);
507 if (threadContext->state == THREAD_PAUSED || threadContext->state == THREAD_PAUSING) {
508 threadContext->state = THREAD_RUNNING;
509 ConditionWake(&threadContext->stateCond);
510 } else if (threadContext->state == THREAD_RUNNING) {
511 _pauseThread(threadContext, false);
512 frameOn = false;
513 }
514 MutexUnlock(&threadContext->stateMutex);
515
516 _changeVideoSync(&threadContext->sync, frameOn);
517}
518
519void GBAThreadPauseFromThread(struct GBAThread* threadContext) {
520 bool frameOn = true;
521 MutexLock(&threadContext->stateMutex);
522 _waitOnInterrupt(threadContext);
523 if (threadContext->state == THREAD_RUNNING) {
524 _pauseThread(threadContext, true);
525 frameOn = false;
526 }
527 MutexUnlock(&threadContext->stateMutex);
528
529 _changeVideoSync(&threadContext->sync, frameOn);
530}
531
532#ifdef USE_PTHREADS
533struct GBAThread* GBAThreadGetContext(void) {
534 pthread_once(&_contextOnce, _createTLS);
535 return pthread_getspecific(_contextKey);
536}
537#else
538struct GBAThread* GBAThreadGetContext(void) {
539 InitOnceExecuteOnce(&_contextOnce, _createTLS, NULL, 0);
540 return TlsGetValue(_contextKey);
541}
542#endif
543
544#ifdef USE_PNG
545void GBAThreadTakeScreenshot(struct GBAThread* threadContext) {
546 unsigned stride;
547 void* pixels = 0;
548 struct VFile* vf = VDirOptionalOpenIncrementFile(threadContext->stateDir, threadContext->gba->activeFile, "screenshot", "-", ".png", O_CREAT | O_TRUNC | O_WRONLY);
549 threadContext->gba->video.renderer->getPixels(threadContext->gba->video.renderer, &stride, &pixels);
550 png_structp png = PNGWriteOpen(vf);
551 png_infop info = PNGWriteHeader(png, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
552 PNGWritePixels(png, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS, stride, pixels);
553 PNGWriteClose(png, info);
554 vf->close(vf);
555}
556#endif
557
558void GBASyncPostFrame(struct GBASync* sync) {
559 if (!sync) {
560 return;
561 }
562
563 MutexLock(&sync->videoFrameMutex);
564 ++sync->videoFramePending;
565 --sync->videoFrameSkip;
566 if (sync->videoFrameSkip < 0) {
567 do {
568 ConditionWake(&sync->videoFrameAvailableCond);
569 if (sync->videoFrameWait) {
570 ConditionWait(&sync->videoFrameRequiredCond, &sync->videoFrameMutex);
571 }
572 } while (sync->videoFrameWait && sync->videoFramePending);
573 }
574 MutexUnlock(&sync->videoFrameMutex);
575
576 struct GBAThread* thread = GBAThreadGetContext();
577 if (!thread) {
578 return;
579 }
580
581 if (thread->rewindBuffer) {
582 --thread->rewindBufferNext;
583 if (thread->rewindBufferNext <= 0) {
584 thread->rewindBufferNext = thread->rewindBufferInterval;
585 GBARecordFrame(thread);
586 }
587 }
588 if (thread->stream) {
589 thread->stream->postVideoFrame(thread->stream, thread->renderer);
590 }
591 if (thread->frameCallback) {
592 thread->frameCallback(thread);
593 }
594}
595
596bool GBASyncWaitFrameStart(struct GBASync* sync, int frameskip) {
597 if (!sync) {
598 return true;
599 }
600
601 MutexLock(&sync->videoFrameMutex);
602 ConditionWake(&sync->videoFrameRequiredCond);
603 if (!sync->videoFrameOn && !sync->videoFramePending) {
604 return false;
605 }
606 if (sync->videoFrameOn && !sync->videoFramePending) {
607 ConditionWait(&sync->videoFrameAvailableCond, &sync->videoFrameMutex);
608 }
609 sync->videoFramePending = 0;
610 sync->videoFrameSkip = frameskip;
611 return true;
612}
613
614void GBASyncWaitFrameEnd(struct GBASync* sync) {
615 if (!sync) {
616 return;
617 }
618
619 MutexUnlock(&sync->videoFrameMutex);
620}
621
622bool GBASyncDrawingFrame(struct GBASync* sync) {
623 return sync->videoFrameSkip <= 0;
624}
625
626void GBASyncSuspendDrawing(struct GBASync* sync) {
627 _changeVideoSync(sync, false);
628}
629
630void GBASyncResumeDrawing(struct GBASync* sync) {
631 _changeVideoSync(sync, true);
632}
633
634void GBASyncProduceAudio(struct GBASync* sync, bool wait) {
635 if (sync->audioWait && wait) {
636 // TODO loop properly in event of spurious wakeups
637 ConditionWait(&sync->audioRequiredCond, &sync->audioBufferMutex);
638 }
639 MutexUnlock(&sync->audioBufferMutex);
640}
641
642void GBASyncLockAudio(struct GBASync* sync) {
643 MutexLock(&sync->audioBufferMutex);
644}
645
646void GBASyncUnlockAudio(struct GBASync* sync) {
647 MutexUnlock(&sync->audioBufferMutex);
648}
649
650void GBASyncConsumeAudio(struct GBASync* sync) {
651 ConditionWake(&sync->audioRequiredCond);
652 MutexUnlock(&sync->audioBufferMutex);
653}
654#endif