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