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