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