all repos — mgba @ 55330698cb08493324a8be5e812e86f94e2ac1fe

mGBA Game Boy Advance Emulator

src/platform/qt/CoreController.cpp (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 "CoreController.h"
  7
  8#include "ConfigController.h"
  9#include "InputController.h"
 10#include "LogController.h"
 11#include "MultiplayerController.h"
 12#include "Override.h"
 13
 14#include <QDateTime>
 15#include <QMutexLocker>
 16
 17#include <mgba/core/serialize.h>
 18#include <mgba/feature/video-logger.h>
 19#ifdef M_CORE_GBA
 20#include <mgba/internal/gba/gba.h>
 21#include <mgba/internal/gba/renderers/tile-cache.h>
 22#include <mgba/internal/gba/sharkport.h>
 23#endif
 24#ifdef M_CORE_GB
 25#include <mgba/internal/gb/gb.h>
 26#include <mgba/internal/gb/renderers/tile-cache.h>
 27#endif
 28#include <mgba-util/vfs.h>
 29
 30using namespace QGBA;
 31
 32
 33CoreController::CoreController(mCore* core, QObject* parent)
 34	: QObject(parent)
 35	, m_saveStateFlags(SAVESTATE_SCREENSHOT | SAVESTATE_SAVEDATA | SAVESTATE_CHEATS | SAVESTATE_RTC)
 36	, m_loadStateFlags(SAVESTATE_SCREENSHOT | SAVESTATE_RTC)
 37{
 38	m_threadContext.core = core;
 39	m_threadContext.userData = this;
 40
 41	QSize size = screenDimensions();
 42	m_buffers[0].resize(size.width() * size.height() * sizeof(color_t));
 43	m_buffers[1].resize(size.width() * size.height() * sizeof(color_t));
 44	m_activeBuffer = &m_buffers[0];
 45
 46	m_threadContext.core->setVideoBuffer(m_threadContext.core, reinterpret_cast<color_t*>(m_activeBuffer->data()), size.width());
 47
 48	m_threadContext.startCallback = [](mCoreThread* context) {
 49		CoreController* controller = static_cast<CoreController*>(context->userData);
 50		context->core->setPeripheral(context->core, mPERIPH_ROTATION, controller->m_inputController->rotationSource());
 51		context->core->setPeripheral(context->core, mPERIPH_RUMBLE, controller->m_inputController->rumble());
 52
 53		switch (context->core->platform(context->core)) {
 54#ifdef M_CORE_GBA
 55		case PLATFORM_GBA:
 56			context->core->setPeripheral(context->core, mPERIPH_GBA_LUMINANCE, controller->m_inputController->luminance());
 57			break;
 58#endif
 59		default:
 60			break;
 61		}
 62
 63		if (controller->m_override) {
 64			controller->m_override->identify(context->core);
 65			controller->m_override->apply(context->core);
 66		}
 67
 68		if (mCoreLoadState(context->core, 0, controller->m_loadStateFlags)) {
 69			mCoreDeleteState(context->core, 0);
 70		}
 71
 72		if (controller->m_multiplayer) {
 73			controller->m_multiplayer->attachGame(controller);
 74		}
 75
 76		QMetaObject::invokeMethod(controller, "started");
 77	};
 78
 79	m_threadContext.resetCallback = [](mCoreThread* context) {
 80		CoreController* controller = static_cast<CoreController*>(context->userData);
 81		for (auto action : controller->m_resetActions) {
 82			action();
 83		}
 84		controller->m_resetActions.clear();
 85
 86		controller->m_activeBuffer->fill(0xFF);
 87		controller->finishFrame();
 88	};
 89
 90	m_threadContext.frameCallback = [](mCoreThread* context) {
 91		CoreController* controller = static_cast<CoreController*>(context->userData);
 92
 93		controller->finishFrame();
 94	};
 95
 96	m_threadContext.cleanCallback = [](mCoreThread* context) {
 97		CoreController* controller = static_cast<CoreController*>(context->userData);
 98
 99		controller->clearMultiplayerController();
100		QMetaObject::invokeMethod(controller, "stopping");
101	};
102
103	m_threadContext.logger.d.log = [](mLogger* logger, int category, enum mLogLevel level, const char* format, va_list args) {
104		mThreadLogger* logContext = reinterpret_cast<mThreadLogger*>(logger);
105		mCoreThread* context = logContext->p;
106
107		static const char* savestateMessage = "State %i loaded";
108		static const char* savestateFailedMessage = "State %i failed to load";
109		static int biosCat = -1;
110		static int statusCat = -1;
111		if (!context) {
112			return;
113		}
114		CoreController* controller = static_cast<CoreController*>(context->userData);
115		QString message;
116		if (biosCat < 0) {
117			biosCat = mLogCategoryById("gba.bios");
118		}
119		if (statusCat < 0) {
120			statusCat = mLogCategoryById("core.status");
121		}
122#ifdef M_CORE_GBA
123		if (level == mLOG_STUB && category == biosCat) {
124			va_list argc;
125			va_copy(argc, args);
126			int immediate = va_arg(argc, int);
127			va_end(argc);
128			QMetaObject::invokeMethod(controller, "unimplementedBiosCall", Q_ARG(int, immediate));
129		} else
130#endif
131		if (category == statusCat) {
132			// Slot 0 is reserved for suspend points
133			if (strncmp(savestateMessage, format, strlen(savestateMessage)) == 0) {
134				va_list argc;
135				va_copy(argc, args);
136				int slot = va_arg(argc, int);
137				va_end(argc);
138				if (slot == 0) {
139					format = "Loaded suspend state";
140				}
141			} else if (strncmp(savestateFailedMessage, format, strlen(savestateFailedMessage)) == 0) {
142				va_list argc;
143				va_copy(argc, args);
144				int slot = va_arg(argc, int);
145				va_end(argc);
146				if (slot == 0) {
147					return;
148				}
149			}
150			message = QString().vsprintf(format, args);
151			QMetaObject::invokeMethod(controller, "statusPosted", Q_ARG(const QString&, message));
152		}
153		if (level == mLOG_FATAL) {
154			mCoreThreadMarkCrashed(controller->thread());
155			QMetaObject::invokeMethod(controller, "crashed", Q_ARG(const QString&, QString().vsprintf(format, args)));
156		}
157		message = QString().vsprintf(format, args);
158		QMetaObject::invokeMethod(controller, "logPosted", Q_ARG(int, level), Q_ARG(int, category), Q_ARG(const QString&, message));
159	};
160}
161
162CoreController::~CoreController() {
163	endVideoLog();
164	stop();
165	disconnect();
166
167	if (m_tileCache) {
168		mTileCacheDeinit(m_tileCache.get());
169		m_tileCache.reset();
170	}
171
172	mCoreThreadJoin(&m_threadContext);
173
174	mCoreConfigDeinit(&m_threadContext.core->config);
175	m_threadContext.core->deinit(m_threadContext.core);
176}
177
178color_t* CoreController::drawContext() {
179	QMutexLocker locker(&m_mutex);
180	if (!m_completeBuffer) {
181		return nullptr;
182	}
183	return reinterpret_cast<color_t*>(m_completeBuffer->data());
184}
185
186bool CoreController::isPaused() {
187	return mCoreThreadIsPaused(&m_threadContext);
188}
189
190mPlatform CoreController::platform() const {
191	return m_threadContext.core->platform(m_threadContext.core);
192}
193
194QSize CoreController::screenDimensions() const {
195	unsigned width, height;
196	m_threadContext.core->desiredVideoDimensions(m_threadContext.core, &width, &height);
197
198	return QSize(width, height);
199}
200
201void CoreController::loadConfig(ConfigController* config) {
202	Interrupter interrupter(this);
203	m_loadStateFlags = config->getOption("loadStateExtdata").toInt();
204	m_saveStateFlags = config->getOption("saveStateExtdata").toInt();
205	m_fastForwardRatio = config->getOption("fastForwardRatio").toFloat();
206	m_videoSync = config->getOption("videoSync").toInt();
207	m_audioSync = config->getOption("audioSync").toInt();
208	m_fpsTarget = config->getOption("fpsTarget").toFloat();
209	updateFastForward();
210	mCoreLoadForeignConfig(m_threadContext.core, config->config());
211	mCoreThreadRewindParamsChanged(&m_threadContext);
212}
213
214#ifdef USE_DEBUGGERS
215void CoreController::setDebugger(mDebugger* debugger) {
216	Interrupter interrupter(this);
217	if (debugger) {
218		mDebuggerAttach(debugger, m_threadContext.core);
219	} else {
220		m_threadContext.core->detachDebugger(m_threadContext.core);
221	}
222}
223#endif
224
225void CoreController::setMultiplayerController(MultiplayerController* controller) {
226	if (controller == m_multiplayer) {
227		return;
228	}
229	clearMultiplayerController();
230	m_multiplayer = controller;
231	if (!mCoreThreadHasStarted(&m_threadContext)) {
232		return;
233	}
234	mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* thread) {
235		CoreController* controller = static_cast<CoreController*>(thread->userData);
236		controller->m_multiplayer->attachGame(controller);
237	});
238}
239
240void CoreController::clearMultiplayerController() {
241	if (!m_multiplayer) {
242		return;
243	}
244	m_multiplayer->detachGame(this);
245	m_multiplayer = nullptr;
246}
247
248mTileCache* CoreController::tileCache() {
249	if (m_tileCache) {
250		return m_tileCache.get();
251	}
252	Interrupter interrupter(this);
253	switch (platform()) {
254#ifdef M_CORE_GBA
255	case PLATFORM_GBA: {
256		GBA* gba = static_cast<GBA*>(m_threadContext.core->board);
257		m_tileCache = std::make_unique<mTileCache>();
258		GBAVideoTileCacheInit(m_tileCache.get());
259		GBAVideoTileCacheAssociate(m_tileCache.get(), &gba->video);
260		mTileCacheSetPalette(m_tileCache.get(), 0);
261		break;
262	}
263#endif
264#ifdef M_CORE_GB
265	case PLATFORM_GB: {
266		GB* gb = static_cast<GB*>(m_threadContext.core->board);
267		m_tileCache = std::make_unique<mTileCache>();
268		GBVideoTileCacheInit(m_tileCache.get());
269		GBVideoTileCacheAssociate(m_tileCache.get(), &gb->video);
270		mTileCacheSetPalette(m_tileCache.get(), 0);
271		break;
272	}
273#endif
274	default:
275		return nullptr;
276	}
277	return m_tileCache.get();
278}
279
280void CoreController::setOverride(std::unique_ptr<Override> override) {
281	Interrupter interrupter(this);
282	m_override = std::move(override);
283	m_override->identify(m_threadContext.core);
284}
285
286void CoreController::setInputController(InputController* inputController) {
287	m_inputController = inputController;
288}
289
290void CoreController::setLogger(LogController* logger) {
291	disconnect(m_log);
292	m_log = logger;
293	m_threadContext.logger.d.filter = logger->filter();
294	connect(this, &CoreController::logPosted, m_log, &LogController::postLog);
295}
296
297void CoreController::start() {
298	if (!m_patched) {
299		mCoreAutoloadPatch(m_threadContext.core);
300	}
301	if (!mCoreThreadStart(&m_threadContext)) {
302		emit failed();
303		emit stopping();
304	}
305}
306
307void CoreController::stop() {
308#ifdef USE_DEBUGGERS
309	setDebugger(nullptr);
310#endif
311	setPaused(false);
312	mCoreThreadEnd(&m_threadContext);
313	emit stopping();
314}
315
316void CoreController::reset() {
317	bool wasPaused = isPaused();
318	setPaused(false);
319	Interrupter interrupter(this);
320	mCoreThreadReset(&m_threadContext);
321	if (wasPaused) {
322		setPaused(true);
323	}
324}
325
326void CoreController::setPaused(bool paused) {
327	if (paused == isPaused()) {
328		return;
329	}
330	if (paused) {
331		QMutexLocker locker(&m_mutex);
332		m_frameActions.append([this]() {
333			mCoreThreadPauseFromThread(&m_threadContext);
334			QMetaObject::invokeMethod(this, "paused");
335		});
336	} else {
337		mCoreThreadUnpause(&m_threadContext);
338		emit unpaused();
339	}
340}
341
342void CoreController::frameAdvance() {
343	QMutexLocker locker(&m_mutex);
344	m_frameActions.append([this]() {
345		mCoreThreadPauseFromThread(&m_threadContext);
346	});
347	setPaused(false);
348}
349
350void CoreController::setSync(bool sync) {
351	if (sync) {
352		m_threadContext.impl->sync.audioWait = m_audioSync;
353		m_threadContext.impl->sync.videoFrameWait = m_videoSync;
354	} else {
355		m_threadContext.impl->sync.audioWait = false;
356		m_threadContext.impl->sync.videoFrameWait = false;
357	}
358}
359
360void CoreController::setRewinding(bool rewind) {
361	if (!m_threadContext.core->opts.rewindEnable) {
362		return;
363	}
364	if (rewind && m_multiplayer && m_multiplayer->attached() > 1) {
365		return;
366	}
367
368	if (rewind && isPaused()) {
369		setPaused(false);
370		// TODO: restore autopausing
371	}
372	mCoreThreadSetRewinding(&m_threadContext, rewind);
373}
374
375void CoreController::rewind(int states) {
376	{
377		Interrupter interrupter(this);
378		if (!states) {
379			states = INT_MAX;
380		}
381		for (int i = 0; i < states; ++i) {
382			if (!mCoreRewindRestore(&m_threadContext.impl->rewind, m_threadContext.core)) {
383				break;
384			}
385		}
386	}
387	emit frameAvailable();
388	emit rewound();
389}
390
391void CoreController::setFastForward(bool enable) {
392	m_fastForward = enable;
393	updateFastForward();
394}
395
396void CoreController::forceFastForward(bool enable) {
397	m_fastForwardForced = enable;
398	updateFastForward();
399}
400
401void CoreController::loadState(int slot) {
402	if (slot > 0 && slot != m_stateSlot) {
403		m_stateSlot = slot;
404		m_backupSaveState.clear();
405	}
406	mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* context) {
407		CoreController* controller = static_cast<CoreController*>(context->userData);
408		if (!controller->m_backupLoadState.isOpen()) {
409			controller->m_backupLoadState = VFileMemChunk(nullptr, 0);
410		}
411		mCoreSaveStateNamed(context->core, controller->m_backupLoadState, controller->m_saveStateFlags);
412		if (mCoreLoadState(context->core, controller->m_stateSlot, controller->m_loadStateFlags)) {
413			emit controller->frameAvailable();
414			emit controller->stateLoaded();
415		}
416	});
417}
418
419void CoreController::saveState(int slot) {
420	if (slot > 0) {
421		m_stateSlot = slot;
422	}
423	mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* context) {
424		CoreController* controller = static_cast<CoreController*>(context->userData);
425		VFile* vf = mCoreGetState(context->core, controller->m_stateSlot, false);
426		if (vf) {
427			controller->m_backupSaveState.resize(vf->size(vf));
428			vf->read(vf, controller->m_backupSaveState.data(), controller->m_backupSaveState.size());
429			vf->close(vf);
430		}
431		mCoreSaveState(context->core, controller->m_stateSlot, controller->m_saveStateFlags);
432	});
433}
434
435void CoreController::loadBackupState() {
436	if (!m_backupLoadState.isOpen()) {
437		return;
438	}
439
440	mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* context) {
441		CoreController* controller = static_cast<CoreController*>(context->userData);
442		controller->m_backupLoadState.seek(0);
443		if (mCoreLoadStateNamed(context->core, controller->m_backupLoadState, controller->m_loadStateFlags)) {
444			mLOG(STATUS, INFO, "Undid state load");
445			controller->frameAvailable();
446			controller->stateLoaded();
447		}
448		controller->m_backupLoadState.close();
449	});
450}
451
452void CoreController::saveBackupState() {
453	if (m_backupSaveState.isEmpty()) {
454		return;
455	}
456
457	mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* context) {
458		CoreController* controller = static_cast<CoreController*>(context->userData);
459		VFile* vf = mCoreGetState(context->core, controller->m_stateSlot, true);
460		if (vf) {
461			vf->write(vf, controller->m_backupSaveState.constData(), controller->m_backupSaveState.size());
462			vf->close(vf);
463			mLOG(STATUS, INFO, "Undid state save");
464		}
465		controller->m_backupSaveState.clear();
466	});
467}
468
469void CoreController::loadSave(const QString& path, bool temporary) {
470	m_resetActions.append([this, path, temporary]() {
471		VFile* vf = VFileDevice::open(path, temporary ? O_RDONLY : O_RDWR);
472		if (!vf) {
473			LOG(QT, ERROR) << tr("Failed to open save file: %1").arg(path);
474			return;
475		}
476
477		if (temporary) {
478			m_threadContext.core->loadTemporarySave(m_threadContext.core, vf);
479		} else {
480			m_threadContext.core->loadSave(m_threadContext.core, vf);
481		}
482	});
483	reset();
484}
485
486void CoreController::loadPatch(const QString& patchPath) {
487	Interrupter interrupter(this);
488	VFile* patch = VFileDevice::open(patchPath, O_RDONLY);
489	if (patch) {
490		m_threadContext.core->loadPatch(m_threadContext.core, patch);
491		m_patched = true;
492	}
493	patch->close(patch);
494	if (mCoreThreadHasStarted(&m_threadContext)) {
495		reset();
496	}
497}
498
499void CoreController::replaceGame(const QString& path) {
500	QFileInfo info(path);
501	if (!info.isReadable()) {
502		LOG(QT, ERROR) << tr("Failed to open game file: %1").arg(path);
503		return;
504	}
505	QString fname = info.canonicalFilePath();
506	Interrupter interrupter(this);
507	mDirectorySetDetachBase(&m_threadContext.core->dirs);
508	mCoreLoadFile(m_threadContext.core, fname.toLocal8Bit().constData());
509}
510
511void CoreController::yankPak() {
512#ifdef M_CORE_GBA
513	if (platform() != PLATFORM_GBA) {
514		return;
515	}
516	Interrupter interrupter(this);
517	GBAYankROM(static_cast<GBA*>(m_threadContext.core->board));
518#endif
519}
520
521void CoreController::addKey(int key) {
522	m_activeKeys |= 1 << key;
523}
524
525void CoreController::clearKey(int key) {
526	m_activeKeys &= ~(1 << key);
527}
528
529void CoreController::setAutofire(int key, bool enable) {
530	if (key >= 32 || key < 0) {
531		return;
532	}
533
534	m_autofire[key] = enable;
535	m_autofireStatus[key] = 0;
536}
537
538#ifdef USE_PNG
539void CoreController::screenshot() {
540	mCoreThreadRunFunction(&m_threadContext, [](mCoreThread* context) {
541		mCoreTakeScreenshot(context->core);
542	});
543}
544#endif
545
546void CoreController::setRealTime() {
547	m_threadContext.core->rtc.override = RTC_NO_OVERRIDE;
548}
549
550void CoreController::setFixedTime(const QDateTime& time) {
551	m_threadContext.core->rtc.override = RTC_FIXED;
552	m_threadContext.core->rtc.value = time.toMSecsSinceEpoch();
553}
554
555void CoreController::setFakeEpoch(const QDateTime& time) {
556	m_threadContext.core->rtc.override = RTC_FAKE_EPOCH;
557	m_threadContext.core->rtc.value = time.toMSecsSinceEpoch();
558}
559
560void CoreController::importSharkport(const QString& path) {
561#ifdef M_CORE_GBA
562	if (platform() != PLATFORM_GBA) {
563		return;
564	}
565	VFile* vf = VFileDevice::open(path, O_RDONLY);
566	if (!vf) {
567		LOG(QT, ERROR) << tr("Failed to open snapshot file for reading: %1").arg(path);
568		return;
569	}
570	Interrupter interrupter(this);
571	GBASavedataImportSharkPort(static_cast<GBA*>(m_threadContext.core->board), vf, false);
572	vf->close(vf);
573#endif
574}
575
576void CoreController::exportSharkport(const QString& path) {
577#ifdef M_CORE_GBA
578	if (platform() != PLATFORM_GBA) {
579		return;
580	}
581	VFile* vf = VFileDevice::open(path, O_WRONLY | O_CREAT | O_TRUNC);
582	if (!vf) {
583		LOG(QT, ERROR) << tr("Failed to open snapshot file for writing: %1").arg(path);
584		return;
585	}
586	Interrupter interrupter(this);
587	GBASavedataExportSharkPort(static_cast<GBA*>(m_threadContext.core->board), vf);
588	vf->close(vf);
589#endif
590}
591
592void CoreController::setAVStream(mAVStream* stream) {
593	Interrupter interrupter(this);
594	m_threadContext.core->setAVStream(m_threadContext.core, stream);
595}
596
597void CoreController::clearAVStream() {
598	Interrupter interrupter(this);
599	m_threadContext.core->setAVStream(m_threadContext.core, nullptr);
600}
601
602void CoreController::clearOverride() {
603	m_override.reset();
604}
605
606void CoreController::startVideoLog(const QString& path) {
607	if (m_vl) {
608		return;
609	}
610
611	Interrupter interrupter(this);
612	m_vl = mVideoLogContextCreate(m_threadContext.core);
613	m_vlVf = VFileDevice::open(path, O_WRONLY | O_CREAT | O_TRUNC);
614	mVideoLogContextSetOutput(m_vl, m_vlVf);
615	mVideoLogContextWriteHeader(m_vl, m_threadContext.core);
616}
617
618void CoreController::endVideoLog() {
619	if (!m_vl) {
620		return;
621	}
622
623	Interrupter interrupter(this);
624	mVideoLogContextDestroy(m_threadContext.core, m_vl);
625	if (m_vlVf) {
626		m_vlVf->close(m_vlVf);
627		m_vlVf = nullptr;
628	}
629	m_vl = nullptr;
630}
631
632void CoreController::updateKeys() {
633	int activeKeys = m_activeKeys | updateAutofire() | m_inputController->pollEvents();
634	m_threadContext.core->setKeys(m_threadContext.core, activeKeys);
635}
636
637int CoreController::updateAutofire() {
638	int active = 0;
639	for (int k = 0; k < 32; ++k) {
640		if (!m_autofire[k]) {
641			continue;
642		}
643		++m_autofireStatus[k];
644		if (m_autofireStatus[k]) {
645			m_autofireStatus[k] = 0;
646			active |= 1 << k;
647		}
648	}
649	return active;
650}
651
652void CoreController::finishFrame() {
653	QMutexLocker locker(&m_mutex);
654	m_completeBuffer = m_activeBuffer;
655
656	// TODO: Generalize this to triple buffering?
657	m_activeBuffer = &m_buffers[0];
658	if (m_activeBuffer == m_completeBuffer) {
659		m_activeBuffer = &m_buffers[1];
660	}
661	m_threadContext.core->setVideoBuffer(m_threadContext.core, reinterpret_cast<color_t*>(m_activeBuffer->data()), screenDimensions().width());
662
663	for (auto& action : m_frameActions) {
664		action();
665	}
666	m_frameActions.clear();
667	updateKeys();
668
669	QMetaObject::invokeMethod(this, "frameAvailable");
670}
671
672void CoreController::updateFastForward() {
673	if (m_fastForward || m_fastForwardForced) {
674		if (m_fastForwardRatio > 0) {
675			m_threadContext.impl->sync.fpsTarget = m_fpsTarget * m_fastForwardRatio;
676		} else {
677			setSync(false);
678		}
679	} else {
680		m_threadContext.impl->sync.fpsTarget = m_fpsTarget;
681		setSync(true);
682	}
683}
684
685CoreController::Interrupter::Interrupter(CoreController* parent, bool fromThread)
686	: m_parent(parent)
687{
688	if (!m_parent->thread()->impl) {
689		return;
690	}
691	if (!fromThread) {
692		mCoreThreadInterrupt(m_parent->thread());
693	} else {
694		mCoreThreadInterruptFromThread(m_parent->thread());
695	}
696}
697
698CoreController::Interrupter::Interrupter(std::shared_ptr<CoreController> parent, bool fromThread)
699	: m_parent(parent.get())
700{
701	if (!m_parent->thread()->impl) {
702		return;
703	}
704	if (!fromThread) {
705		mCoreThreadInterrupt(m_parent->thread());
706	} else {
707		mCoreThreadInterruptFromThread(m_parent->thread());
708	}
709}
710
711CoreController::Interrupter::Interrupter(const Interrupter& other)
712	: m_parent(other.m_parent)
713{
714	if (!m_parent->thread()->impl) {
715		return;
716	}
717	mCoreThreadInterrupt(m_parent->thread());
718}
719
720CoreController::Interrupter::~Interrupter() {
721	if (!m_parent->thread()->impl) {
722		return;
723	}
724	mCoreThreadContinue(m_parent->thread());
725}