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