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/memory.h>
28#endif
29
30using namespace QGBA;
31
32FrameView::FrameView(std::shared_ptr<CoreController> controller, QWidget* parent)
33 : AssetView(controller, parent)
34{
35 m_ui.setupUi(this);
36
37 m_glowTimer.setInterval(33);
38 connect(&m_glowTimer, &QTimer::timeout, this, [this]() {
39 ++m_glowFrame;
40 invalidateQueue();
41 });
42
43 m_ui.compositedView->installEventFilter(this);
44
45 connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) {
46 Layer& layer = m_queue[item->data(Qt::UserRole).toInt()];
47 layer.enabled = item->checkState() == Qt::Checked;
48 if (layer.enabled) {
49 m_disabled.remove(layer.id);
50 } else {
51 m_disabled.insert(layer.id);
52 }
53 invalidateQueue();
54 });
55 connect(m_ui.queue, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* item) {
56 if (item) {
57 m_active = m_queue[item->data(Qt::UserRole).toInt()].id;
58 } else {
59 m_active = {};
60 }
61 invalidateQueue();
62 });
63 connect(m_ui.magnification, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]() {
64 invalidateQueue();
65 });
66 connect(m_ui.exportButton, &QAbstractButton::pressed, this, &FrameView::exportFrame);
67 connect(m_ui.reset, &QAbstractButton::pressed, this, &FrameView::reset);
68
69 m_backdropPicker = ColorPicker(m_ui.backdrop, QColor(0, 0, 0, 0));
70 connect(&m_backdropPicker, &ColorPicker::colorChanged, this, [this](const QColor& color) {
71 m_overrideBackdrop = color;
72 });
73 m_controller->addFrameAction(std::bind(&FrameView::frameCallback, this, m_callbackLocker));
74
75 {
76 CoreController::Interrupter interrupter(m_controller);
77 refreshVl();
78 }
79 m_controller->frameAdvance();
80}
81
82FrameView::~FrameView() {
83 QMutexLocker locker(&m_mutex);
84 *m_callbackLocker = false;
85 if (m_vl) {
86 m_vl->deinit(m_vl);
87 }
88}
89
90bool FrameView::lookupLayer(const QPointF& coord, Layer*& out) {
91 for (Layer& layer : m_queue) {
92 if (!layer.enabled || m_disabled.contains(layer.id)) {
93 continue;
94 }
95 QPointF location = layer.location;
96 QSizeF layerDims(layer.image.width(), layer.image.height());
97 QRegion region;
98 if (layer.repeats) {
99 if (location.x() + layerDims.width() < 0) {
100 location.setX(std::fmod(location.x(), layerDims.width()));
101 }
102 if (location.y() + layerDims.height() < 0) {
103 location.setY(std::fmod(location.y(), layerDims.height()));
104 }
105
106 region += layer.mask.translated(location.x(), location.y());
107 region += layer.mask.translated(location.x() + layerDims.width(), location.y());
108 region += layer.mask.translated(location.x(), location.y() + layerDims.height());
109 region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height());
110 } else {
111 region = layer.mask.translated(location.x(), location.y());
112 }
113
114 if (region.contains(QPoint(coord.x(), coord.y()))) {
115 out = &layer;
116 return true;
117 }
118 }
119 return false;
120}
121
122void FrameView::selectLayer(const QPointF& coord) {
123 Layer* layer;
124 if (!lookupLayer(coord, layer)) {
125 return;
126 }
127 if (layer->id == m_active) {
128 m_active = {};
129 } else {
130 m_active = layer->id;
131 }
132 m_glowFrame = 0;
133}
134
135void FrameView::disableLayer(const QPointF& coord) {
136 Layer* layer;
137 if (!lookupLayer(coord, layer)) {
138 return;
139 }
140 layer->enabled = false;
141 m_disabled.insert(layer->id);
142}
143
144#ifdef M_CORE_GBA
145void FrameView::updateTilesGBA(bool) {
146 if (m_ui.freeze->checkState() == Qt::Checked) {
147 return;
148 }
149 QMutexLocker locker(&m_mutex);
150 m_queue.clear();
151 {
152 CoreController::Interrupter interrupter(m_controller);
153
154 uint16_t* io = static_cast<GBA*>(m_controller->thread()->core->board)->memory.io;
155 QRgb backdrop = M_RGB5_TO_RGB8(static_cast<GBA*>(m_controller->thread()->core->board)->video.palette[0]);
156 m_gbaDispcnt = io[REG_DISPCNT >> 1];
157 int mode = GBARegisterDISPCNTGetMode(m_gbaDispcnt);
158
159 std::array<bool, 4> enabled{
160 bool(GBARegisterDISPCNTIsBg0Enable(m_gbaDispcnt)),
161 bool(GBARegisterDISPCNTIsBg1Enable(m_gbaDispcnt)),
162 bool(GBARegisterDISPCNTIsBg2Enable(m_gbaDispcnt)),
163 bool(GBARegisterDISPCNTIsBg3Enable(m_gbaDispcnt)),
164 };
165
166 for (int priority = 0; priority < 4; ++priority) {
167 for (int sprite = 0; sprite < 128; ++sprite) {
168 ObjInfo info;
169 lookupObj(sprite, &info);
170
171 if (!info.enabled || info.priority != priority) {
172 continue;
173 }
174
175 QPointF offset(info.x, info.y);
176 QImage obj(compositeObj(info));
177 if (info.hflip || info.vflip) {
178 obj = obj.mirrored(info.hflip, info.vflip);
179 }
180 if (!info.xform.isIdentity()) {
181 offset += QPointF(obj.width(), obj.height()) / 2;
182 obj = obj.transformed(info.xform);
183 offset -= QPointF(obj.width() / 2, obj.height() / 2);
184 }
185 m_queue.append({
186 { LayerId::SPRITE, sprite },
187 !m_disabled.contains({ LayerId::SPRITE, sprite }),
188 QPixmap::fromImage(obj),
189 {}, offset, false
190 });
191 if (m_queue.back().image.hasAlpha()) {
192 m_queue.back().mask = QRegion(m_queue.back().image.mask());
193 } else {
194 m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
195 }
196 }
197
198 for (int bg = 0; bg < 4; ++bg) {
199 if (!enabled[bg]) {
200 continue;
201 }
202 if (GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + bg]) != priority) {
203 continue;
204 }
205
206 QPointF offset;
207 if (mode == 0) {
208 offset.setX(-(io[(REG_BG0HOFS >> 1) + (bg << 1)] & 0x1FF));
209 offset.setY(-(io[(REG_BG0VOFS >> 1) + (bg << 1)] & 0x1FF));
210 };
211 m_queue.append({
212 { LayerId::BACKGROUND, bg },
213 !m_disabled.contains({ LayerId::BACKGROUND, bg }),
214 QPixmap::fromImage(compositeMap(bg, m_mapStatus[bg])),
215 {}, offset, true
216 });
217 if (m_queue.back().image.hasAlpha()) {
218 m_queue.back().mask = QRegion(m_queue.back().image.mask());
219 } else {
220 m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
221 }
222 }
223 }
224 QImage backdropImage(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS), QImage::Format_Mono);
225 backdropImage.fill(1);
226 backdropImage.setColorTable({backdrop, backdrop | 0xFF000000 });
227 m_queue.append({
228 { LayerId::BACKDROP },
229 !m_disabled.contains({ LayerId::BACKDROP }),
230 QPixmap::fromImage(backdropImage),
231 {}, {0, 0}, false
232 });
233 updateRendered();
234 }
235 invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS));
236}
237
238void FrameView::injectGBA() {
239 mVideoLogger* logger = m_vl->videoLogger;
240 mVideoLoggerInjectionPoint(logger, LOGGER_INJECTION_FIRST_SCANLINE);
241 GBA* gba = static_cast<GBA*>(m_vl->board);
242 gba->video.renderer->highlightBG[0] = false;
243 gba->video.renderer->highlightBG[1] = false;
244 gba->video.renderer->highlightBG[2] = false;
245 gba->video.renderer->highlightBG[3] = false;
246 for (int i = 0; i < 128; ++i) {
247 gba->video.renderer->highlightOBJ[i] = false;
248 }
249 QPalette palette;
250 gba->video.renderer->highlightColor = palette.color(QPalette::HighlightedText).rgb();
251 gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 48 + 64;
252 if (!m_overrideBackdrop.isValid()) {
253 QRgb backdrop = M_RGB5_TO_RGB8(gba->video.palette[0]) | 0xFF000000;
254 m_backdropPicker.setColor(backdrop);
255 }
256
257 m_vl->reset(m_vl);
258 for (const Layer& layer : m_queue) {
259 switch (layer.id.type) {
260 case LayerId::SPRITE:
261 if (!layer.enabled) {
262 mVideoLoggerInjectOAM(logger, layer.id.index << 2, 0x200);
263 }
264 if (layer.id == m_active) {
265 gba->video.renderer->highlightOBJ[layer.id.index] = true;
266 }
267 break;
268 case LayerId::BACKGROUND:
269 m_vl->enableVideoLayer(m_vl, layer.id.index, layer.enabled);
270 if (layer.id == m_active) {
271 gba->video.renderer->highlightBG[layer.id.index] = true;
272 }
273 break;
274 }
275 }
276 if (m_overrideBackdrop.isValid()) {
277 mVideoLoggerInjectPalette(logger, 0, M_RGB8_TO_RGB5(m_overrideBackdrop.rgb()));
278 }
279 if (m_ui.disableScanline->checkState() == Qt::Checked) {
280 mVideoLoggerIgnoreAfterInjection(logger, (1 << DIRTY_PALETTE) | (1 << DIRTY_OAM) | (1 << DIRTY_REGISTER));
281 } else {
282 mVideoLoggerIgnoreAfterInjection(logger, 0);
283 }
284}
285#endif
286
287#ifdef M_CORE_GB
288void FrameView::updateTilesGB(bool) {
289 if (m_ui.freeze->checkState() == Qt::Checked) {
290 return;
291 }
292 m_queue.clear();
293 {
294 CoreController::Interrupter interrupter(m_controller);
295 updateRendered();
296 }
297 invalidateQueue(m_controller->screenDimensions());
298}
299
300void FrameView::injectGB() {
301 for (const Layer& layer : m_queue) {
302 }
303}
304#endif
305
306void FrameView::invalidateQueue(const QSize& dims) {
307 if (dims.isValid()) {
308 m_dims = dims;
309 }
310 bool blockSignals = m_ui.queue->blockSignals(true);
311 QMutexLocker locker(&m_mutex);
312 if (m_vl) {
313 switch (m_controller->platform()) {
314#ifdef M_CORE_GBA
315 case PLATFORM_GBA:
316 injectGBA();
317 break;
318#endif
319#ifdef M_CORE_GB
320 case PLATFORM_GB:
321 injectGB();
322 break;
323#endif
324 }
325 m_vl->runFrame(m_vl);
326 }
327
328 for (int i = 0; i < m_queue.count(); ++i) {
329 const Layer& layer = m_queue[i];
330 QListWidgetItem* item;
331 if (i >= m_ui.queue->count()) {
332 item = new QListWidgetItem;
333 m_ui.queue->addItem(item);
334 } else {
335 item = m_ui.queue->item(i);
336 }
337 item->setText(layer.id.readable());
338 item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
339 item->setCheckState(layer.enabled ? Qt::Checked : Qt::Unchecked);
340 item->setData(Qt::UserRole, i);
341 item->setSelected(layer.id == m_active);
342 }
343
344 while (m_ui.queue->count() > m_queue.count()) {
345 delete m_ui.queue->takeItem(m_queue.count());
346 }
347 m_ui.queue->blockSignals(blockSignals);
348
349 QPixmap composited;
350 if (m_framebuffer.isNull()) {
351 updateRendered();
352 composited = m_rendered;
353 } else {
354 m_ui.exportButton->setEnabled(true);
355 composited.convertFromImage(m_framebuffer);
356 }
357 m_composited = composited.scaled(m_dims * m_ui.magnification->value());
358 m_ui.compositedView->setPixmap(m_composited);
359}
360
361void FrameView::updateRendered() {
362 if (m_ui.freeze->checkState() == Qt::Checked) {
363 return;
364 }
365 m_rendered.convertFromImage(m_controller->getPixels());
366}
367
368bool FrameView::eventFilter(QObject* obj, QEvent* event) {
369 QPointF pos;
370 switch (event->type()) {
371 case QEvent::MouseButtonPress:
372 pos = static_cast<QMouseEvent*>(event)->localPos();
373 pos /= m_ui.magnification->value();
374 selectLayer(pos);
375 return true;
376 case QEvent::MouseButtonDblClick:
377 pos = static_cast<QMouseEvent*>(event)->localPos();
378 pos /= m_ui.magnification->value();
379 disableLayer(pos);
380 return true;
381 default:
382 break;
383 }
384 return false;
385}
386
387void FrameView::refreshVl() {
388 QMutexLocker locker(&m_mutex);
389 m_currentFrame = m_nextFrame;
390 m_nextFrame = VFileMemChunk(nullptr, 0);
391 if (m_currentFrame) {
392 m_controller->endVideoLog(false);
393 QMetaObject::invokeMethod(this, "newVl");
394 }
395 m_controller->endVideoLog();
396 m_controller->startVideoLog(m_nextFrame, false);
397}
398
399void FrameView::newVl() {
400 if (!m_glowTimer.isActive()) {
401 m_glowTimer.start();
402 }
403 QMutexLocker locker(&m_mutex);
404 if (!m_currentFrame) {
405 return;
406 }
407 if (m_vl) {
408 m_vl->deinit(m_vl);
409 }
410 m_vl = mCoreFindVF(m_currentFrame);
411 m_vl->init(m_vl);
412 m_vl->loadROM(m_vl, m_currentFrame);
413 m_currentFrame = nullptr;
414 mCoreInitConfig(m_vl, nullptr);
415 unsigned width, height;
416 m_vl->desiredVideoDimensions(m_vl, &width, &height);
417 m_framebuffer = QImage(width, height, QImage::Format_RGBX8888);
418 m_vl->setVideoBuffer(m_vl, reinterpret_cast<color_t*>(m_framebuffer.bits()), width);
419 m_vl->reset(m_vl);
420}
421
422void FrameView::frameCallback(FrameView* viewer, std::shared_ptr<bool> lock) {
423 if (!*lock) {
424 return;
425 }
426 CoreController::Interrupter interrupter(viewer->m_controller, true);
427 viewer->refreshVl();
428 viewer->m_controller->addFrameAction(std::bind(&FrameView::frameCallback, viewer, lock));
429}
430
431void FrameView::exportFrame() {
432 QString filename = GBAApp::app()->getSaveFileName(this, tr("Export frame"),
433 tr("Portable Network Graphics (*.png)"));
434 CoreController::Interrupter interrupter(m_controller);
435 m_framebuffer.save(filename, "PNG");
436}
437
438void FrameView::reset() {
439 m_disabled.clear();
440 for (Layer& layer : m_queue) {
441 layer.enabled = true;
442 }
443 m_overrideBackdrop = QColor();
444 invalidateQueue();
445}
446
447QString FrameView::LayerId::readable() const {
448 QString typeStr;
449 switch (type) {
450 case NONE:
451 return tr("None");
452 case BACKGROUND:
453 typeStr = tr("Background");
454 break;
455 case WINDOW:
456 typeStr = tr("Window");
457 break;
458 case SPRITE:
459 typeStr = tr("Sprite");
460 break;
461 case BACKDROP:
462 typeStr = tr("Backdrop");
463 break;
464 }
465 if (index < 0) {
466 return typeStr;
467 }
468 return tr("%1 %2").arg(typeStr).arg(index);
469}