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