src/platform/qt/FrameView.cpp (view raw)
1/* Copyright (c) 2013-2019 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 "FrameView.h"
7
8#include <QMouseEvent>
9#include <QPalette>
10
11#include <array>
12#include <cmath>
13
14#include "CoreController.h"
15#include "GBAApp.h"
16
17#include <mgba/core/core.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/io.h>
22#include <mgba/internal/gba/memory.h>
23#include <mgba/internal/gba/video.h>
24#endif
25#ifdef M_CORE_GB
26#include <mgba/internal/gb/gb.h>
27#include <mgba/internal/gb/io.h>
28#include <mgba/internal/gb/memory.h>
29#endif
30
31using namespace QGBA;
32
33FrameView::FrameView(std::shared_ptr<CoreController> controller, QWidget* parent)
34 : AssetView(controller, parent)
35{
36 m_ui.setupUi(this);
37
38 m_glowTimer.setInterval(33);
39 connect(&m_glowTimer, &QTimer::timeout, this, [this]() {
40 ++m_glowFrame;
41 invalidateQueue();
42 });
43
44 m_ui.compositedView->installEventFilter(this);
45
46 connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) {
47 Layer& layer = m_queue[item->data(Qt::UserRole).toInt()];
48 layer.enabled = item->checkState() == Qt::Checked;
49 if (layer.enabled) {
50 m_disabled.remove(layer.id);
51 } else {
52 m_disabled.insert(layer.id);
53 }
54 invalidateQueue();
55 });
56 connect(m_ui.queue, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* item) {
57 if (item) {
58 m_active = m_queue[item->data(Qt::UserRole).toInt()].id;
59 } else {
60 m_active = {};
61 }
62 invalidateQueue();
63 });
64 connect(m_ui.magnification, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]() {
65 invalidateQueue();
66 });
67 connect(m_ui.exportButton, &QAbstractButton::pressed, this, &FrameView::exportFrame);
68 connect(m_ui.reset, &QAbstractButton::pressed, this, &FrameView::reset);
69
70 m_backdropPicker = ColorPicker(m_ui.backdrop, QColor(0, 0, 0, 0));
71 connect(&m_backdropPicker, &ColorPicker::colorChanged, this, [this](const QColor& color) {
72 m_overrideBackdrop = color;
73 });
74 m_controller->addFrameAction(std::bind(&FrameView::frameCallback, this, m_callbackLocker));
75
76 {
77 CoreController::Interrupter interrupter(m_controller);
78 refreshVl();
79 }
80 m_controller->frameAdvance();
81}
82
83FrameView::~FrameView() {
84 QMutexLocker locker(&m_mutex);
85 *m_callbackLocker = false;
86
87 if (m_nextFrame) {
88 m_controller->endVideoLog(true);
89 }
90 if (m_vl) {
91 mCoreConfigDeinit(&m_vl->config);
92 m_vl->deinit(m_vl);
93 }
94}
95
96bool FrameView::lookupLayer(const QPointF& coord, Layer*& out) {
97 for (Layer& layer : m_queue) {
98 if (!layer.enabled || m_disabled.contains(layer.id)) {
99 continue;
100 }
101 QPointF location = layer.location;
102 QSizeF layerDims(layer.image.width(), layer.image.height());
103 QRegion region;
104 if (layer.repeats) {
105 if (location.x() + layerDims.width() < 0) {
106 location.setX(std::fmod(location.x(), layerDims.width()));
107 }
108 if (location.y() + layerDims.height() < 0) {
109 location.setY(std::fmod(location.y(), layerDims.height()));
110 }
111
112 region += layer.mask.translated(location.x(), location.y());
113 region += layer.mask.translated(location.x() + layerDims.width(), location.y());
114 region += layer.mask.translated(location.x(), location.y() + layerDims.height());
115 region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height());
116 } else {
117 region = layer.mask.translated(location.x(), location.y());
118 }
119
120 if (region.contains(QPoint(coord.x(), coord.y()))) {
121 out = &layer;
122 return true;
123 }
124 }
125 return false;
126}
127
128void FrameView::selectLayer(const QPointF& coord) {
129 Layer* layer;
130 if (!lookupLayer(coord, layer)) {
131 return;
132 }
133 if (layer->id == m_active) {
134 m_active = {};
135 } else {
136 m_active = layer->id;
137 }
138 m_glowFrame = 0;
139}
140
141void FrameView::disableLayer(const QPointF& coord) {
142 Layer* layer;
143 if (!lookupLayer(coord, layer)) {
144 return;
145 }
146 layer->enabled = false;
147 m_disabled.insert(layer->id);
148}
149
150#ifdef M_CORE_GBA
151void FrameView::updateTilesGBA(bool) {
152 if (m_ui.freeze->checkState() == Qt::Checked) {
153 return;
154 }
155 QMutexLocker locker(&m_mutex);
156 m_queue.clear();
157 {
158 CoreController::Interrupter interrupter(m_controller);
159
160 uint16_t* io = static_cast<GBA*>(m_controller->thread()->core->board)->memory.io;
161 QRgb backdrop = M_RGB5_TO_RGB8(static_cast<GBA*>(m_controller->thread()->core->board)->video.palette[0]);
162 GBARegisterDISPCNT gbaDispcnt = io[REG_DISPCNT >> 1];
163 int mode = GBARegisterDISPCNTGetMode(gbaDispcnt);
164
165 std::array<bool, 4> enabled{
166 bool(GBARegisterDISPCNTIsBg0Enable(gbaDispcnt)),
167 bool(GBARegisterDISPCNTIsBg1Enable(gbaDispcnt)),
168 bool(GBARegisterDISPCNTIsBg2Enable(gbaDispcnt)),
169 bool(GBARegisterDISPCNTIsBg3Enable(gbaDispcnt)),
170 };
171
172 if (GBARegisterDISPCNTIsWin0Enable(gbaDispcnt)) {
173 m_queue.append({
174 { LayerId::WINDOW, 0 },
175 !m_disabled.contains({ LayerId::WINDOW, 0 }),
176 {},
177 {}, {0, 0}, true, false
178 });
179 }
180
181 if (GBARegisterDISPCNTIsWin1Enable(gbaDispcnt)) {
182 m_queue.append({
183 { LayerId::WINDOW, 1 },
184 !m_disabled.contains({ LayerId::WINDOW, 1 }),
185 {},
186 {}, {0, 0}, true, false
187 });
188 }
189
190 if (GBARegisterDISPCNTIsObjwinEnable(gbaDispcnt)) {
191 m_queue.append({
192 { LayerId::WINDOW, 2 },
193 !m_disabled.contains({ LayerId::WINDOW, 2 }),
194 {},
195 {}, {0, 0}, true, false
196 });
197
198 }
199
200 for (int priority = 0; priority < 4; ++priority) {
201 for (int sprite = 0; sprite < 128; ++sprite) {
202 ObjInfo info;
203 lookupObj(sprite, &info);
204
205 if (!info.enabled || info.priority != priority) {
206 continue;
207 }
208
209 QPointF offset(info.x, info.y);
210 QImage obj(compositeObj(info));
211 if (info.hflip || info.vflip) {
212 obj = obj.mirrored(info.hflip, info.vflip);
213 }
214 if (!info.xform.isIdentity()) {
215 offset += QPointF(obj.width(), obj.height()) / 2;
216 obj = obj.transformed(info.xform);
217 offset -= QPointF(obj.width() / 2, obj.height() / 2);
218 }
219 m_queue.append({
220 { LayerId::SPRITE, sprite },
221 !m_disabled.contains({ LayerId::SPRITE, sprite }),
222 QPixmap::fromImage(obj),
223 {}, offset, false, false
224 });
225 if (m_queue.back().image.hasAlpha()) {
226 m_queue.back().mask = QRegion(m_queue.back().image.mask());
227 } else {
228 m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
229 }
230 }
231
232 for (int bg = 0; bg < 4; ++bg) {
233 if (!enabled[bg]) {
234 continue;
235 }
236 if (GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + bg]) != priority) {
237 continue;
238 }
239
240 QPointF offset;
241 if (mode == 0) {
242 offset.setX(-(io[(REG_BG0HOFS >> 1) + (bg << 1)] & 0x1FF));
243 offset.setY(-(io[(REG_BG0VOFS >> 1) + (bg << 1)] & 0x1FF));
244 };
245 m_queue.append({
246 { LayerId::BACKGROUND, bg },
247 !m_disabled.contains({ LayerId::BACKGROUND, bg }),
248 QPixmap::fromImage(compositeMap(bg, m_mapStatus[bg])),
249 {}, offset, true, false
250 });
251 if (m_queue.back().image.hasAlpha()) {
252 m_queue.back().mask = QRegion(m_queue.back().image.mask());
253 } else {
254 m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
255 }
256 }
257 }
258 QImage backdropImage(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS), QImage::Format_Mono);
259 backdropImage.fill(1);
260 backdropImage.setColorTable({backdrop, backdrop | 0xFF000000 });
261 m_queue.append({
262 { LayerId::BACKDROP },
263 !m_disabled.contains({ LayerId::BACKDROP }),
264 QPixmap::fromImage(backdropImage),
265 {}, {0, 0}, false, true
266 });
267 updateRendered();
268 }
269 invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS));
270}
271
272void FrameView::injectGBA() {
273 mVideoLogger* logger = m_vl->videoLogger;
274 GBA* gba = static_cast<GBA*>(m_vl->board);
275 gba->video.renderer->highlightBG[0] = false;
276 gba->video.renderer->highlightBG[1] = false;
277 gba->video.renderer->highlightBG[2] = false;
278 gba->video.renderer->highlightBG[3] = false;
279 for (int i = 0; i < 128; ++i) {
280 gba->video.renderer->highlightOBJ[i] = false;
281 }
282 QPalette palette;
283 gba->video.renderer->highlightColor = M_RGB8_TO_NATIVE(palette.color(QPalette::Highlight).rgb());
284 gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 48 + 64;
285 if (!m_overrideBackdrop.isValid()) {
286 QRgb backdrop = M_RGB5_TO_RGB8(gba->video.palette[0]) | 0xFF000000;
287 m_backdropPicker.setColor(backdrop);
288 }
289
290 m_vl->reset(m_vl);
291 for (const Layer& layer : m_queue) {
292 switch (layer.id.type) {
293 case LayerId::SPRITE:
294 if (!layer.enabled) {
295 mVideoLoggerInjectOAM(logger, layer.id.index << 2, 0x200);
296 }
297 if (layer.id == m_active) {
298 gba->video.renderer->highlightOBJ[layer.id.index] = true;
299 }
300 break;
301 case LayerId::BACKGROUND:
302 m_vl->enableVideoLayer(m_vl, layer.id.index, layer.enabled);
303 if (layer.id == m_active) {
304 gba->video.renderer->highlightBG[layer.id.index] = true;
305 }
306 break;
307 case LayerId::WINDOW:
308 m_vl->enableVideoLayer(m_vl, GBA_LAYER_WIN0 + layer.id.index, layer.enabled);
309 break;
310 case LayerId::BACKDROP:
311 case LayerId::FRAME:
312 case LayerId::NONE:
313 break;
314 }
315 }
316 if (m_overrideBackdrop.isValid()) {
317 mVideoLoggerInjectPalette(logger, 0, M_RGB8_TO_RGB5(m_overrideBackdrop.rgb()));
318 }
319}
320#endif
321
322#ifdef M_CORE_GB
323void FrameView::updateTilesGB(bool) {
324 if (m_ui.freeze->checkState() == Qt::Checked) {
325 return;
326 }
327 m_queue.clear();
328 {
329 CoreController::Interrupter interrupter(m_controller);
330 QPointF origin;
331 GB* gb = static_cast<GB*>(m_controller->thread()->core->board);
332 if (gb->video.sgbBorders && (gb->model & GB_MODEL_SGB)) {
333 origin = QPointF(48, 40);
334 }
335 uint8_t* io = gb->memory.io;
336 GBRegisterLCDC lcdc = io[GB_REG_LCDC];
337
338 for (int sprite = 0; sprite < 40; ++sprite) {
339 ObjInfo info;
340 lookupObj(sprite, &info);
341
342 if (!info.enabled) {
343 continue;
344 }
345
346 QPointF offset(info.x, info.y);
347 QImage obj(compositeObj(info));
348 if (info.hflip || info.vflip) {
349 obj = obj.mirrored(info.hflip, info.vflip);
350 }
351 m_queue.append({
352 { LayerId::SPRITE, sprite },
353 !m_disabled.contains({ LayerId::SPRITE, sprite }),
354 QPixmap::fromImage(obj),
355 {}, offset + origin, false, false
356 });
357 if (m_queue.back().image.hasAlpha()) {
358 m_queue.back().mask = QRegion(m_queue.back().image.mask());
359 } else {
360 m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
361 }
362 }
363
364 if (GBRegisterLCDCIsWindow(lcdc)) {
365 m_queue.append({
366 { LayerId::WINDOW },
367 !m_disabled.contains({ LayerId::WINDOW }),
368 {},
369 {}, origin, false, false
370 });
371 }
372
373 m_queue.append({
374 { LayerId::BACKGROUND },
375 !m_disabled.contains({ LayerId::BACKGROUND }),
376 {},
377 {}, origin, false, false
378 });
379
380 updateRendered();
381 }
382 invalidateQueue(m_controller->screenDimensions());
383}
384
385void FrameView::injectGB() {
386 mVideoLogger* logger = m_vl->videoLogger;
387 GB* gb = static_cast<GB*>(m_vl->board);
388 gb->video.renderer->highlightBG = false;
389 gb->video.renderer->highlightWIN = false;
390 for (int i = 0; i < 40; ++i) {
391 gb->video.renderer->highlightOBJ[i] = false;
392 }
393 QPalette palette;
394 gb->video.renderer->highlightColor = M_RGB8_TO_NATIVE(palette.color(QPalette::Highlight).rgb());
395 gb->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 48 + 64;
396
397 m_vl->reset(m_vl);
398 for (const Layer& layer : m_queue) {
399 switch (layer.id.type) {
400 case LayerId::SPRITE:
401 if (!layer.enabled) {
402 mVideoLoggerInjectOAM(logger, layer.id.index << 2, 0);
403 }
404 if (layer.id == m_active) {
405 gb->video.renderer->highlightOBJ[layer.id.index] = true;
406 }
407 break;
408 case LayerId::BACKGROUND:
409 m_vl->enableVideoLayer(m_vl, GB_LAYER_BACKGROUND, layer.enabled);
410 if (layer.id == m_active) {
411 gb->video.renderer->highlightBG = true;
412 }
413 break;
414 case LayerId::WINDOW:
415 m_vl->enableVideoLayer(m_vl, GB_LAYER_WINDOW, layer.enabled);
416 if (layer.id == m_active) {
417 gb->video.renderer->highlightWIN = true;
418 }
419 break;
420 case LayerId::FRAME: // TODO for SGB
421 case LayerId::BACKDROP:
422 case LayerId::NONE:
423 break;
424 }
425 }
426}
427#endif
428
429void FrameView::invalidateQueue(const QSize& dims) {
430 if (dims.isValid()) {
431 m_dims = dims;
432 }
433 bool blockSignals = m_ui.queue->blockSignals(true);
434 QMutexLocker locker(&m_mutex);
435 if (m_vl) {
436 mVideoLogger* logger = m_vl->videoLogger;
437 mVideoLoggerInjectionPoint(logger, LOGGER_INJECTION_FIRST_SCANLINE);
438 switch (m_controller->platform()) {
439#ifdef M_CORE_GBA
440 case mPLATFORM_GBA:
441 injectGBA();
442 break;
443#endif
444#ifdef M_CORE_GB
445 case mPLATFORM_GB:
446 injectGB();
447 break;
448#endif
449 case mPLATFORM_NONE:
450 break;
451 }
452 if (m_ui.disableScanline->checkState() == Qt::Checked) {
453 mVideoLoggerIgnoreAfterInjection(logger, (1 << DIRTY_PALETTE) | (1 << DIRTY_OAM) | (1 << DIRTY_REGISTER));
454 } else {
455 mVideoLoggerIgnoreAfterInjection(logger, 0);
456 }
457 m_vl->runFrame(m_vl);
458 }
459
460 for (int i = 0; i < m_queue.count(); ++i) {
461 const Layer& layer = m_queue[i];
462 QListWidgetItem* item;
463 if (i >= m_ui.queue->count()) {
464 item = new QListWidgetItem;
465 m_ui.queue->addItem(item);
466 } else {
467 item = m_ui.queue->item(i);
468 }
469 item->setText(layer.id.readable());
470 item->setFlags(Qt::ItemIsSelectable | (layer.fixed ? Qt::NoItemFlags : Qt::ItemIsUserCheckable) | Qt::ItemIsEnabled);
471 item->setCheckState(layer.enabled ? Qt::Checked : Qt::Unchecked);
472 item->setData(Qt::UserRole, i);
473 item->setSelected(layer.id == m_active);
474 }
475
476 while (m_ui.queue->count() > m_queue.count()) {
477 delete m_ui.queue->takeItem(m_queue.count());
478 }
479 m_ui.queue->blockSignals(blockSignals);
480
481 QPixmap composited;
482 if (m_framebuffer.isNull()) {
483 updateRendered();
484 composited = m_rendered;
485 } else {
486 QImage framebuffer(m_framebuffer);
487 m_ui.exportButton->setEnabled(true);
488 if (framebuffer.size() != m_dims) {
489 framebuffer = framebuffer.copy({QPoint(), m_dims});
490 }
491 composited.convertFromImage(framebuffer);
492 }
493 m_composited = composited.scaled(m_dims * m_ui.magnification->value());
494 m_ui.compositedView->setPixmap(m_composited);
495}
496
497void FrameView::updateRendered() {
498 if (m_ui.freeze->checkState() == Qt::Checked) {
499 return;
500 }
501 m_rendered.convertFromImage(m_controller->getPixels());
502}
503
504bool FrameView::eventFilter(QObject*, QEvent* event) {
505 QPointF pos;
506 switch (event->type()) {
507 case QEvent::MouseButtonPress:
508 pos = static_cast<QMouseEvent*>(event)->localPos();
509 pos /= m_ui.magnification->value();
510 selectLayer(pos);
511 return true;
512 case QEvent::MouseButtonDblClick:
513 pos = static_cast<QMouseEvent*>(event)->localPos();
514 pos /= m_ui.magnification->value();
515 disableLayer(pos);
516 return true;
517 default:
518 break;
519 }
520 return false;
521}
522
523void FrameView::refreshVl() {
524 QMutexLocker locker(&m_mutex);
525 if (m_currentFrame) {
526 m_currentFrame->close(m_currentFrame);
527 }
528 m_currentFrame = m_nextFrame;
529 m_nextFrame = VFileDevice::openMemory();
530 if (m_currentFrame) {
531 m_controller->endVideoLog(false);
532 QMetaObject::invokeMethod(this, "newVl");
533 }
534 m_controller->endVideoLog();
535 m_controller->startVideoLog(m_nextFrame, false);
536}
537
538void FrameView::newVl() {
539 if (!m_glowTimer.isActive()) {
540 m_glowTimer.start();
541 }
542 QMutexLocker locker(&m_mutex);
543 if (!m_currentFrame) {
544 return;
545 }
546 if (m_vl) {
547 mCoreConfigDeinit(&m_vl->config);
548 m_vl->deinit(m_vl);
549 }
550 m_vl = mCoreFindVF(m_currentFrame);
551 m_vl->init(m_vl);
552 m_vl->loadROM(m_vl, m_currentFrame);
553 m_currentFrame = nullptr;
554 mCoreInitConfig(m_vl, nullptr);
555#ifdef M_CORE_GB
556 if (m_controller->platform() == mPLATFORM_GB) {
557 mCoreConfigSetIntValue(&m_vl->config, "sgb.borders", static_cast<GB*>(m_controller->thread()->core->board)->video.sgbBorders);
558 m_vl->reloadConfigOption(m_vl, "sgb.borders", nullptr);
559 }
560#endif
561 unsigned width, height;
562 m_vl->desiredVideoDimensions(m_vl, &width, &height);
563 m_framebuffer = QImage(width, height, QImage::Format_RGBX8888);
564 m_vl->setVideoBuffer(m_vl, reinterpret_cast<color_t*>(m_framebuffer.bits()), width);
565 m_vl->reset(m_vl);
566}
567
568void FrameView::frameCallback(FrameView* viewer, std::shared_ptr<bool> lock) {
569 if (!*lock) {
570 return;
571 }
572 CoreController::Interrupter interrupter(viewer->m_controller);
573 viewer->refreshVl();
574 viewer->m_controller->addFrameAction(std::bind(&FrameView::frameCallback, viewer, lock));
575}
576
577void FrameView::exportFrame() {
578 QString filename = GBAApp::app()->getSaveFileName(this, tr("Export frame"),
579 tr("Portable Network Graphics (*.png)"));
580 CoreController::Interrupter interrupter(m_controller);
581 m_framebuffer.save(filename, "PNG");
582}
583
584void FrameView::reset() {
585 m_disabled.clear();
586 for (Layer& layer : m_queue) {
587 layer.enabled = true;
588 }
589 m_overrideBackdrop = QColor();
590 invalidateQueue();
591}
592
593QString FrameView::LayerId::readable() const {
594 QString typeStr;
595 switch (type) {
596 case NONE:
597 return tr("None");
598 case BACKGROUND:
599 typeStr = tr("Background");
600 break;
601 case WINDOW:
602 typeStr = tr("Window");
603#ifdef M_CORE_GBA
604 if (index == 2) {
605 return tr("Objwin");
606 }
607#endif
608 break;
609 case SPRITE:
610 typeStr = tr("Sprite");
611 break;
612 case BACKDROP:
613 typeStr = tr("Backdrop");
614 break;
615 case FRAME:
616 typeStr = tr("Frame");
617 break;
618 }
619 if (index < 0) {
620 return typeStr;
621 }
622 return tr("%1 %2").arg(typeStr).arg(index);
623}