diff --git a/CHANGES b/CHANGES index 2fb47290a..139ed6443 100644 --- a/CHANGES +++ b/CHANGES @@ -56,12 +56,17 @@ Emulation fixes: - GB Video: Draw SGB border pieces that overlap GB graphics (fixes mgba.io/i/1339) - GBA: Improve timing when not booting from BIOS - GBA BIOS: Work around IRQ handling hiccup in Mario & Luigi (fixes mgba.io/i/1059) + - GBA DMA: Fix DMA source direction bits being cleared (fixes mgba.io/i/2410) - GBA I/O: Redo internal key input, enabling edge-based key IRQs - GBA I/O: Disable open bus behavior on invalid register 06A - GBA Memory: Fix misaligned 32-bit I/O loads (fixes mgba.io/i/2307) + - GBA Video: Fix OpenGL rendering on M1 Macs Other fixes: - Core: Don't attempt to restore rewind diffs past start of rewind - FFmpeg: Fix crash when encoding audio with some containers + - FFmpeg: Fix GIF recording (fixes mgba.io/i/2393) + - GB: Fix temporary saves + - GB, GBA: Save writeback-pending masked saves on unload (fixes mgba.io/i/2396) Misc: - Core: Suspend runloop when a core crashes - GB Video: Add default SGB border @@ -71,6 +76,9 @@ Misc: - Qt: Only set default controller bindings if loading fails (fixes mgba.io/i/799) - Qt: Save converter now supports importing GameShark Advance saves - Qt: Save positions of multiplayer windows (closes mgba.io/i/2128) + - Qt: Add optional frame counter to OSD (closes mgba.io/i/1728) + - Qt: Add optional emulation-related information on reset (closes mgba.io/i/1780) + - Qt: Add QOpenGLWidget cross-thread codepath for macOS (fixes mgba.io/i/1754) - Windows: Attach to console if present 0.9.3: (2021-12-17) diff --git a/include/mgba/internal/debugger/gdb-stub.h b/include/mgba/internal/debugger/gdb-stub.h index 5743834a2..a617bc558 100644 --- a/include/mgba/internal/debugger/gdb-stub.h +++ b/include/mgba/internal/debugger/gdb-stub.h @@ -29,6 +29,7 @@ struct GDBStub { char line[GDB_STUB_MAX_LINE]; char outgoing[GDB_STUB_MAX_LINE]; + char memoryMapXml[GDB_STUB_MAX_LINE]; enum GDBStubAckState lineAck; Socket socket; diff --git a/include/mgba/internal/defines.h b/include/mgba/internal/defines.h new file mode 100644 index 000000000..8af603736 --- /dev/null +++ b/include/mgba/internal/defines.h @@ -0,0 +1,31 @@ +/* Copyright (c) 2013-2022 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef M_INTERNAL_DEFINES_H +#define M_INTERNAL_DEFINES_H + +#define mSAVEDATA_CLEANUP_THRESHOLD 15 + +enum { + mSAVEDATA_DIRT_NONE = 0, + mSAVEDATA_DIRT_NEW = 1, + mSAVEDATA_DIRT_SEEN = 2, +}; + +static inline bool mSavedataClean(int* dirty, uint32_t* dirtAge, uint32_t frameCount) { + if (*dirty & mSAVEDATA_DIRT_NEW) { + *dirtAge = frameCount; + *dirty &= ~mSAVEDATA_DIRT_NEW; + if (!(*dirty & mSAVEDATA_DIRT_SEEN)) { + *dirty |= mSAVEDATA_DIRT_SEEN; + } + } else if ((*dirty & mSAVEDATA_DIRT_SEEN) && frameCount - *dirtAge > mSAVEDATA_CLEANUP_THRESHOLD) { + *dirty = 0; + return true; + } + return false; +} + +#endif diff --git a/include/mgba/internal/gb/gb.h b/include/mgba/internal/gb/gb.h index d0e59e07e..a0196c162 100644 --- a/include/mgba/internal/gb/gb.h +++ b/include/mgba/internal/gb/gb.h @@ -102,7 +102,7 @@ struct GB { struct VFile* sramRealVf; uint32_t sramSize; int sramDirty; - int32_t sramDirtAge; + uint32_t sramDirtAge; bool sramMaskWriteback; int sgbBit; diff --git a/include/mgba/internal/gb/memory.h b/include/mgba/internal/gb/memory.h index a9dc8b7e9..29ffb6960 100644 --- a/include/mgba/internal/gb/memory.h +++ b/include/mgba/internal/gb/memory.h @@ -65,11 +65,6 @@ enum { GB_SIZE_MBC6_FLASH = 0x100000, }; -enum { - GB_SRAM_DIRT_NEW = 1, - GB_SRAM_DIRT_SEEN = 2 -}; - struct GBMemory; typedef void (*GBMemoryBankControllerWrite)(struct GB*, uint16_t address, uint8_t value); typedef uint8_t (*GBMemoryBankControllerRead)(struct GBMemory*, uint16_t address); diff --git a/include/mgba/internal/gba/savedata.h b/include/mgba/internal/gba/savedata.h index 395e06b7a..7f0b84c83 100644 --- a/include/mgba/internal/gba/savedata.h +++ b/include/mgba/internal/gba/savedata.h @@ -60,11 +60,6 @@ enum FlashManufacturer { FLASH_MFG_SANYO = 0x1362 }; -enum SavedataDirty { - SAVEDATA_DIRT_NEW = 1, - SAVEDATA_DIRT_SEEN = 2 -}; - enum { SAVEDATA_FLASH_BASE = 0x0E005555, @@ -92,7 +87,7 @@ struct GBASavedata { unsigned settling; struct mTimingEvent dust; - enum SavedataDirty dirty; + int dirty; uint32_t dirtAge; enum FlashStateMachine flashState; diff --git a/src/debugger/gdb-stub.c b/src/debugger/gdb-stub.c index 23cc52396..7069c7f75 100644 --- a/src/debugger/gdb-stub.c +++ b/src/debugger/gdb-stub.c @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -29,6 +30,30 @@ enum { MACH_O_ARM_V4T = 5 }; +static const char* TARGET_XML = "" + "armv4t" + "none" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + static void _sendMessage(struct GDBStub* stub); static void _gdbStubDeinit(struct mDebugger* debugger) { @@ -336,16 +361,6 @@ static void _readGPRs(struct GDBStub* stub, const char* message) { _int2hex32(cpu->gprs[ARM_PC] - (cpu->cpsr.t ? WORD_SIZE_THUMB : WORD_SIZE_ARM), &stub->outgoing[i]); i += 8; - // Floating point registers, unused on the GBA (8 of them, 24 bits each) - for (r = 0; r < 8 * 3; ++r) { - _int2hex32(0, &stub->outgoing[i]); - i += 8; - } - - // Floating point status, unused on the GBA (32 bits) - _int2hex32(0, &stub->outgoing[i]); - i += 8; - // CPU status _int2hex32(cpu->cpsr.packed, &stub->outgoing[i]); i += 8; @@ -433,7 +448,58 @@ static void _processQSupportedCommand(struct GDBStub* stub, const char* message) } message = end + 1; } - strncpy(stub->outgoing, "swbreak+;hwbreak+", GDB_STUB_MAX_LINE - 4); + strncpy(stub->outgoing, "swbreak+;hwbreak+;qXfer:features:read+;qXfer:memory-map:read+", GDB_STUB_MAX_LINE - 4); +} + +static void _processQXferCommand(struct GDBStub* stub, const char* params, const char* data) { + unsigned offset = 0; + unsigned length = 0; + + unsigned index = 0; + for (index = 0; params[index] != ','; ++index) { + offset <<= 4; + offset |= _hex2int(¶ms[index], 1); + } + + ++index; + unsigned int paramsLength = strlen(params); + for (; index + 3 < paramsLength; ++index) { + length <<= 4; + length |= _hex2int(¶ms[index], 1); + } + + length += 1; + + if (length + 4 > GDB_STUB_MAX_LINE) { + length = GDB_STUB_MAX_LINE - 4; + } + + if (strlen(data) < length + offset) { + length = strlen(data) - offset + 1; + stub->outgoing[0] = 'l'; + } else { + stub->outgoing[0] = 'm'; + } + strlcpy(&stub->outgoing[1], &data[offset], length); +} + +static void _generateMemoryMapXml(struct GDBStub* stub, char* memoryMap) { + size_t index = 0; + strncpy(memoryMap, "", 27); + index += strlen(memoryMap); + const struct mCoreMemoryBlock* blocks; + size_t nBlocks = stub->d.core->listMemoryBlocks(stub->d.core, &blocks); + size_t i; + for (i = 0; i < nBlocks; ++i) { + if (!(blocks[i].flags & mCORE_MEMORY_MAPPED)) { + continue; + } + + const char* type = blocks[i].flags & (mCORE_MEMORY_WRITE | mCORE_MEMORY_WORM) ? "ram" : "rom"; + index += snprintf(&memoryMap[index], GDB_STUB_MAX_LINE - index, "", type, blocks[i].start, blocks[i].size); + } + int amountLeft = GDB_STUB_MAX_LINE - index; + strncpy(&memoryMap[index], "", amountLeft); } static void _processQReadCommand(struct GDBStub* stub, const char* message) { @@ -452,6 +518,13 @@ static void _processQReadCommand(struct GDBStub* stub, const char* message) { strncpy(stub->outgoing, "m1", GDB_STUB_MAX_LINE - 4); } else if (!strncmp("sThreadInfo#", message, 12)) { strncpy(stub->outgoing, "l", GDB_STUB_MAX_LINE - 4); + } else if (!strncmp("Xfer:features:read:target.xml:", message, 30)) { + _processQXferCommand(stub, message + 30, TARGET_XML); + } else if (!strncmp("Xfer:memory-map:read::", message, 22)) { + if (strlen(stub->memoryMapXml) == 0) { + _generateMemoryMapXml(stub, stub->memoryMapXml); + } + _processQXferCommand(stub, message + 22, stub->memoryMapXml); } else if (!strncmp("Supported:", message, 10)) { _processQSupportedCommand(stub, message + 10); } @@ -487,7 +560,6 @@ static void _setBreakpoint(struct GDBStub* stub, const char* message) { unsigned i = 0; uint32_t address = _readHex(readAddress, &i); readAddress += i + 1; - uint32_t kind = _readHex(readAddress, &i); struct mBreakpoint breakpoint = { .address = address, @@ -499,8 +571,6 @@ static void _setBreakpoint(struct GDBStub* stub, const char* message) { switch (message[0]) { case '0': - ARMDebuggerSetSoftwareBreakpoint(stub->d.platform, address, kind == 2 ? MODE_THUMB : MODE_ARM); - break; case '1': stub->d.platform->setBreakpoint(stub->d.platform, &breakpoint); break; @@ -710,6 +780,8 @@ bool GDBStubListen(struct GDBStub* stub, int port, const struct Address* bindAdd goto cleanup; } + memset(stub->memoryMapXml, 0, GDB_STUB_MAX_LINE); + return true; cleanup: diff --git a/src/feature/commandline.c b/src/feature/commandline.c index 2d3f3e357..6cab35094 100644 --- a/src/feature/commandline.c +++ b/src/feature/commandline.c @@ -165,6 +165,7 @@ void applyArguments(const struct mArguments* args, struct mSubParser* subparser, } if (args->bios) { mCoreConfigSetOverrideValue(config, "bios", args->bios); + mCoreConfigSetOverrideIntValue(config, "useBios", true); } HashTableEnumerate(&args->configOverrides, _tableApply, config); if (subparser) { diff --git a/src/feature/ffmpeg/ffmpeg-encoder.c b/src/feature/ffmpeg/ffmpeg-encoder.c index 590a6db92..38ca867ec 100644 --- a/src/feature/ffmpeg/ffmpeg-encoder.c +++ b/src/feature/ffmpeg/ffmpeg-encoder.c @@ -789,7 +789,7 @@ void _ffmpegPostVideoFrame(struct mAVStream* stream, const color_t* pixels, size sws_scale(encoder->scaleContext, (const uint8_t* const*) &pixels, (const int*) &stride, 0, encoder->iheight, encoder->videoFrame->data, encoder->videoFrame->linesize); if (encoder->graph) { - if (av_buffersrc_add_frame(encoder->source, encoder->videoFrame) < 0) { + if (av_buffersrc_write_frame(encoder->source, encoder->videoFrame) < 0) { return; } while (true) { diff --git a/src/gb/gb.c b/src/gb/gb.c index b94d7f482..43f5d93a0 100644 --- a/src/gb/gb.c +++ b/src/gb/gb.c @@ -5,6 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include +#include #include #include #include @@ -17,8 +18,6 @@ #include #include -#define CLEANUP_THRESHOLD 15 - const uint32_t CGB_SM83_FREQUENCY = 0x800000; const uint32_t SGB_SM83_FREQUENCY = 0x418B1E; @@ -203,6 +202,14 @@ void GBResizeSram(struct GB* gb, size_t size) { if (gb->memory.sram) { vf->unmap(vf, gb->memory.sram, gb->sramSize); } + if (vf->size(vf) < gb->sramSize) { + void* sram = vf->map(vf, vf->size(vf), MAP_READ); + struct VFile* newVf = VFileMemChunk(sram, vf->size(vf)); + vf->unmap(vf, sram,vf->size(vf)); + vf = newVf; + gb->sramVf = newVf; + vf->truncate(vf, size); + } gb->memory.sram = vf->map(vf, size, MAP_READ); } if (gb->memory.sram == (void*) -1) { @@ -233,24 +240,19 @@ void GBSramClean(struct GB* gb, uint32_t frameCount) { if (!gb->sramVf) { return; } - if (gb->sramDirty & GB_SRAM_DIRT_NEW) { - gb->sramDirtAge = frameCount; - gb->sramDirty &= ~GB_SRAM_DIRT_NEW; - if (!(gb->sramDirty & GB_SRAM_DIRT_SEEN)) { - gb->sramDirty |= GB_SRAM_DIRT_SEEN; - } - } else if ((gb->sramDirty & GB_SRAM_DIRT_SEEN) && frameCount - gb->sramDirtAge > CLEANUP_THRESHOLD) { + if (mSavedataClean(&gb->sramDirty, &gb->sramDirtAge, frameCount)) { if (gb->sramMaskWriteback) { GBSavedataUnmask(gb); } if (gb->memory.mbcType == GB_MBC3_RTC) { GBMBCRTCWrite(gb); } - gb->sramDirty = 0; - if (gb->memory.sram && gb->sramVf->sync(gb->sramVf, gb->memory.sram, gb->sramSize)) { - mLOG(GB_MEM, INFO, "Savedata synced"); - } else { - mLOG(GB_MEM, INFO, "Savedata failed to sync!"); + if (gb->sramVf == gb->sramRealVf) { + if (gb->memory.sram && gb->sramVf->sync(gb->sramVf, gb->memory.sram, gb->sramSize)) { + mLOG(GB_MEM, INFO, "Savedata synced"); + } else { + mLOG(GB_MEM, INFO, "Savedata failed to sync!"); + } } size_t c; @@ -271,7 +273,7 @@ void GBSavedataMask(struct GB* gb, struct VFile* vf, bool writeback) { } gb->sramVf = vf; gb->sramMaskWriteback = writeback; - gb->memory.sram = vf->map(vf, gb->sramSize, MAP_READ); + GBResizeSram(gb, gb->sramSize); GBMBCSwitchSramBank(gb, gb->memory.sramCurrentBank); } @@ -316,7 +318,9 @@ void GBUnloadROM(struct GB* gb) { gb->memory.mbcType = GB_MBC_AUTODETECT; gb->isPristine = false; - gb->sramMaskWriteback = false; + if (!gb->sramDirty) { + gb->sramMaskWriteback = false; + } GBSavedataUnmask(gb); GBSramDeinit(gb); if (gb->sramRealVf) { diff --git a/src/gb/mbc.c b/src/gb/mbc.c index bd8ad46f0..de6f7802d 100644 --- a/src/gb/mbc.c +++ b/src/gb/mbc.c @@ -6,9 +6,10 @@ #include #include -#include +#include #include #include +#include #include #include @@ -615,6 +616,7 @@ void _GBMBC2(struct GB* gb, uint16_t address, uint8_t value) { address &= 0x1FF; memory->sramBank[(address >> 1)] &= 0xF0 >> shift; memory->sramBank[(address >> 1)] |= (value & 0xF) << shift; + gb->sramDirty |= mSAVEDATA_DIRT_NEW; break; default: // TODO @@ -776,6 +778,7 @@ void _GBMBC6(struct GB* gb, uint16_t address, uint8_t value) { case 0x2B: if (memory->sramAccess) { memory->sramBank[address & (GB_SIZE_EXTERNAL_RAM_HALFBANK - 1)] = value; + gb->sramDirty |= mSAVEDATA_DIRT_NEW; } break; case 0x2C: @@ -841,6 +844,7 @@ void _GBMBC7(struct GB* gb, uint16_t address, uint8_t value) { break; case 0x5: _GBMBC7Write(&gb->memory, address, value); + gb->sramDirty |= mSAVEDATA_DIRT_NEW; break; default: // TODO @@ -1163,6 +1167,7 @@ void _GBPocketCam(struct GB* gb, uint16_t address, uint8_t value) { address &= 0x7F; if (address == 0 && value & 1) { value &= 6; // TODO: Timing + gb->sramDirty |= mSAVEDATA_DIRT_NEW; _GBPocketCamCapture(memory); } if (address < sizeof(memory->mbcState.pocketCam.registers)) { @@ -1287,6 +1292,7 @@ void _GBTAMA5(struct GB* gb, uint16_t address, uint8_t value) { switch (tama5->registers[GBTAMA5_CS] >> 1) { case 0x0: // RAM write memory->sram[address] = out; + gb->sramDirty |= mSAVEDATA_DIRT_NEW; break; case 0x1: // RAM read break; diff --git a/src/gb/memory.c b/src/gb/memory.c index 0ec280aa1..175950e82 100644 --- a/src/gb/memory.c +++ b/src/gb/memory.c @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -356,11 +357,13 @@ void GBStore8(struct SM83Core* cpu, uint16_t address, int8_t value) { if (memory->rtcAccess) { memory->rtcRegs[memory->activeRtcReg] = value; } else if (memory->sramAccess && memory->sram && memory->directSramAccess) { - memory->sramBank[address & (GB_SIZE_EXTERNAL_RAM - 1)] = value; + if (memory->sramBank[address & (GB_SIZE_EXTERNAL_RAM - 1)] != value) { + memory->sramBank[address & (GB_SIZE_EXTERNAL_RAM - 1)] = value; + gb->sramDirty |= mSAVEDATA_DIRT_NEW; + } } else { memory->mbcWrite(gb, address, value); } - gb->sramDirty |= GB_SRAM_DIRT_NEW; return; case GB_REGION_WORKING_RAM_BANK0: case GB_REGION_WORKING_RAM_BANK0 + 2: @@ -648,7 +651,7 @@ void GBPatch8(struct SM83Core* cpu, uint16_t address, int8_t value, int8_t* old, } else { memory->mbcWrite(gb, address, value); } - gb->sramDirty |= GB_SRAM_DIRT_NEW; + gb->sramDirty |= mSAVEDATA_DIRT_NEW; return; case GB_REGION_WORKING_RAM_BANK0: case GB_REGION_WORKING_RAM_BANK0 + 2: diff --git a/src/gba/dma.c b/src/gba/dma.c index d30f0291a..5807e506f 100644 --- a/src/gba/dma.c +++ b/src/gba/dma.c @@ -83,9 +83,6 @@ uint16_t GBADMAWriteCNT_HI(struct GBA* gba, int dma, uint16_t control) { if (!wasEnabled && GBADMARegisterIsEnable(currentDma->reg)) { currentDma->nextSource = currentDma->source; - if (currentDma->nextSource >= BASE_CART0 && currentDma->nextSource < BASE_CART_SRAM && GBADMARegisterGetSrcControl(currentDma->reg) < 3) { - currentDma->reg = GBADMARegisterClearSrcControl(currentDma->reg); - } currentDma->nextDest = currentDma->dest; uint32_t width = 2 << GBADMARegisterGetWidth(currentDma->reg); @@ -291,7 +288,13 @@ void GBADMAService(struct GBA* gba, int number, struct GBADMA* info) { } gba->bus = memory->dmaTransferRegister; } - int sourceOffset = DMA_OFFSET[GBADMARegisterGetSrcControl(info->reg)] * width; + + int sourceOffset; + if (info->nextSource >= BASE_CART0 && info->nextSource < BASE_CART_SRAM && GBADMARegisterGetSrcControl(info->reg) < 3) { + sourceOffset = width; + } else { + sourceOffset = DMA_OFFSET[GBADMARegisterGetSrcControl(info->reg)] * width; + } int destOffset = DMA_OFFSET[GBADMARegisterGetDestControl(info->reg)] * width; if (source) { source += sourceOffset; diff --git a/src/gba/gba.c b/src/gba/gba.c index d278bdd9c..97664ce54 100644 --- a/src/gba/gba.c +++ b/src/gba/gba.c @@ -149,7 +149,9 @@ void GBAUnloadROM(struct GBA* gba) { gba->memory.rom = NULL; gba->isPristine = false; - gba->memory.savedata.maskWriteback = false; + if (!gba->memory.savedata.dirty) { + gba->memory.savedata.maskWriteback = false; + } GBASavedataUnmask(&gba->memory.savedata); GBASavedataDeinit(&gba->memory.savedata); if (gba->memory.savedata.realVf) { diff --git a/src/gba/memory.c b/src/gba/memory.c index 0842afc00..3ada70999 100644 --- a/src/gba/memory.c +++ b/src/gba/memory.c @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -1075,12 +1076,12 @@ void GBAStore8(struct ARMCore* cpu, uint32_t address, int8_t value, int* cycleCo } else { memory->savedata.data[address & (SIZE_CART_SRAM - 1)] = value; } - memory->savedata.dirty |= SAVEDATA_DIRT_NEW; + memory->savedata.dirty |= mSAVEDATA_DIRT_NEW; } else if (memory->hw.devices & HW_TILT) { GBAHardwareTiltWrite(&memory->hw, address & OFFSET_MASK, value); } else if (memory->savedata.type == SAVEDATA_SRAM512) { memory->savedata.data[address & (SIZE_CART_SRAM512 - 1)] = value; - memory->savedata.dirty |= SAVEDATA_DIRT_NEW; + memory->savedata.dirty |= mSAVEDATA_DIRT_NEW; } else { mLOG(GBA_MEM, GAME_ERROR, "Writing to non-existent SRAM: 0x%08X", address); } diff --git a/src/gba/renderers/gl.c b/src/gba/renderers/gl.c index 22b6fe756..74c455798 100644 --- a/src/gba/renderers/gl.c +++ b/src/gba/renderers/gl.c @@ -67,8 +67,8 @@ static const GLchar* const _gles3Header = "precision highp isampler2D;\n"; static const GLchar* const _gl3Header = - "#version 150 core\n" - "#define OUT(n)\n" + "#version 330 core\n" + "#define OUT(n) layout(location = n)\n" PALETTE_ENTRY "precision highp float;\n"; @@ -684,7 +684,7 @@ void GBAVideoGLRendererCreate(struct GBAVideoGLRenderer* renderer) { renderer->scale = 1; } -static void _compileShader(struct GBAVideoGLRenderer* glRenderer, struct GBAVideoGLShader* shader, const char** shaderBuffer, int shaderBufferLines, GLuint vs, const struct GBAVideoGLUniform* uniforms, const char* const* outFrags, char* log) { +static void _compileShader(struct GBAVideoGLRenderer* glRenderer, struct GBAVideoGLShader* shader, const char** shaderBuffer, int shaderBufferLines, GLuint vs, const struct GBAVideoGLUniform* uniforms, char* log) { GLuint program = glCreateProgram(); shader->program = program; @@ -697,14 +697,6 @@ static void _compileShader(struct GBAVideoGLRenderer* glRenderer, struct GBAVide if (log[0]) { mLOG(GBA_VIDEO, ERROR, "Fragment shader compilation failure: %s", log); } - size_t i; -#ifndef BUILD_GLES3 - for (i = 0; outFrags[i]; ++i) { - glBindFragDataLocation(program, i, outFrags[i]); - } -#else - UNUSED(outFrags); -#endif glLinkProgram(program); glGetProgramInfoLog(program, 2048, 0, log); if (log[0]) { @@ -719,6 +711,7 @@ static void _compileShader(struct GBAVideoGLRenderer* glRenderer, struct GBAVide glEnableVertexAttribArray(positionLocation); glVertexAttribPointer(positionLocation, 2, GL_INT, GL_FALSE, 0, NULL); + size_t i; for (i = 0; uniforms[i].name; ++i) { shader->uniforms[uniforms[i].type] = glGetUniformLocation(program, uniforms[i].name); } @@ -824,7 +817,7 @@ void GBAVideoGLRendererInit(struct GBAVideoRenderer* renderer) { char log[2048]; const GLchar* shaderBuffer[4]; const GLubyte* version = glGetString(GL_VERSION); - if (strncmp((const char*) version, "OpenGL ES ", strlen("OpenGL ES "))) { + if (strncmp((const char*) version, "OpenGL ES ", strlen("OpenGL ES ")) != 0) { shaderBuffer[0] = _gl3Header; } else { shaderBuffer[0] = _gles3Header; @@ -838,53 +831,47 @@ void GBAVideoGLRendererInit(struct GBAVideoRenderer* renderer) { if (log[0]) { mLOG(GBA_VIDEO, ERROR, "Vertex shader compilation failure: %s", log); } - - const char* const noWindow[] = {"color", "flags", NULL}; - const char* const window[] = {"color", "flags", "window", NULL}; - const char* const onlyWindow[] = {"window", NULL}; - const char* const onlyColor[] = {"color", NULL}; - shaderBuffer[1] = _renderMode0; shaderBuffer[2] = _renderTile16; - _compileShader(glRenderer, &glRenderer->bgShader[0], shaderBuffer, 3, vs, _uniformsMode0, noWindow, log); + _compileShader(glRenderer, &glRenderer->bgShader[0], shaderBuffer, 3, vs, _uniformsMode0, log); shaderBuffer[2] = _renderTile256; - _compileShader(glRenderer, &glRenderer->bgShader[1], shaderBuffer, 3, vs, _uniformsMode0, noWindow, log); + _compileShader(glRenderer, &glRenderer->bgShader[1], shaderBuffer, 3, vs, _uniformsMode0, log); shaderBuffer[1] = _renderMode2; shaderBuffer[2] = _interpolate; shaderBuffer[3] = _fetchTileOverflow; - _compileShader(glRenderer, &glRenderer->bgShader[2], shaderBuffer, 4, vs, _uniformsMode2, noWindow, log); + _compileShader(glRenderer, &glRenderer->bgShader[2], shaderBuffer, 4, vs, _uniformsMode2, log); shaderBuffer[3] = _fetchTileNoOverflow; - _compileShader(glRenderer, &glRenderer->bgShader[3], shaderBuffer, 4, vs, _uniformsMode2, noWindow, log); + _compileShader(glRenderer, &glRenderer->bgShader[3], shaderBuffer, 4, vs, _uniformsMode2, log); shaderBuffer[1] = _renderMode4; shaderBuffer[2] = _interpolate; - _compileShader(glRenderer, &glRenderer->bgShader[4], shaderBuffer, 3, vs, _uniformsMode4, noWindow, log); + _compileShader(glRenderer, &glRenderer->bgShader[4], shaderBuffer, 3, vs, _uniformsMode4, log); shaderBuffer[1] = _renderMode35; shaderBuffer[2] = _interpolate; - _compileShader(glRenderer, &glRenderer->bgShader[5], shaderBuffer, 3, vs, _uniformsMode35, noWindow, log); + _compileShader(glRenderer, &glRenderer->bgShader[5], shaderBuffer, 3, vs, _uniformsMode35, log); shaderBuffer[1] = _renderObj; shaderBuffer[2] = _renderTile16; - _compileShader(glRenderer, &glRenderer->objShader[0], shaderBuffer, 3, vs, _uniformsObj, window, log); + _compileShader(glRenderer, &glRenderer->objShader[0], shaderBuffer, 3, vs, _uniformsObj, log); shaderBuffer[2] = _renderTile256; - _compileShader(glRenderer, &glRenderer->objShader[1], shaderBuffer, 3, vs, _uniformsObj, window, log); + _compileShader(glRenderer, &glRenderer->objShader[1], shaderBuffer, 3, vs, _uniformsObj, log); shaderBuffer[1] = _renderObjPriority; - _compileShader(glRenderer, &glRenderer->objShader[2], shaderBuffer, 2, vs, _uniformsObjPriority, noWindow, log); + _compileShader(glRenderer, &glRenderer->objShader[2], shaderBuffer, 2, vs, _uniformsObjPriority, log); shaderBuffer[1] = _renderWindow; - _compileShader(glRenderer, &glRenderer->windowShader, shaderBuffer, 2, vs, _uniformsWindow, onlyWindow, log); + _compileShader(glRenderer, &glRenderer->windowShader, shaderBuffer, 2, vs, _uniformsWindow, log); shaderBuffer[1] = _finalize; - _compileShader(glRenderer, &glRenderer->finalizeShader, shaderBuffer, 2, vs, _uniformsFinalize, onlyColor, log); + _compileShader(glRenderer, &glRenderer->finalizeShader, shaderBuffer, 2, vs, _uniformsFinalize, log); glBindVertexArray(0); glDeleteShader(vs); diff --git a/src/gba/savedata.c b/src/gba/savedata.c index fbae0a554..182a6cc30 100644 --- a/src/gba/savedata.c +++ b/src/gba/savedata.c @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -24,7 +25,6 @@ #define FLASH_PROGRAM_CYCLES 650 // This needs real testing, and is only an estimation currently #define EEPROM_SETTLE_CYCLES 115000 -#define CLEANUP_THRESHOLD 15 mLOG_DEFINE_CATEGORY(GBA_SAVE, "GBA Savedata", "gba.savedata"); @@ -379,7 +379,7 @@ void GBASavedataWriteFlash(struct GBASavedata* savedata, uint16_t address, uint8 case FLASH_STATE_RAW: switch (savedata->command) { case FLASH_COMMAND_PROGRAM: - savedata->dirty |= SAVEDATA_DIRT_NEW; + savedata->dirty |= mSAVEDATA_DIRT_NEW; savedata->currentBank[address] = value; savedata->command = FLASH_COMMAND_NONE; mTimingDeschedule(savedata->timing, &savedata->dust); @@ -511,7 +511,7 @@ void GBASavedataWriteEEPROM(struct GBASavedata* savedata, uint16_t value, uint32 uint8_t current = savedata->data[savedata->writeAddress >> 3]; current &= ~(1 << (0x7 - (savedata->writeAddress & 0x7))); current |= (value & 0x1) << (0x7 - (savedata->writeAddress & 0x7)); - savedata->dirty |= SAVEDATA_DIRT_NEW; + savedata->dirty |= mSAVEDATA_DIRT_NEW; savedata->data[savedata->writeAddress >> 3] = current; mTimingDeschedule(savedata->timing, &savedata->dust); mTimingSchedule(savedata->timing, &savedata->dust, EEPROM_SETTLE_CYCLES); @@ -565,15 +565,10 @@ void GBASavedataClean(struct GBASavedata* savedata, uint32_t frameCount) { if (!savedata->vf) { return; } - if (savedata->dirty & SAVEDATA_DIRT_NEW) { - savedata->dirtAge = frameCount; - savedata->dirty &= ~SAVEDATA_DIRT_NEW; - savedata->dirty |= SAVEDATA_DIRT_SEEN; - } else if ((savedata->dirty & SAVEDATA_DIRT_SEEN) && frameCount - savedata->dirtAge > CLEANUP_THRESHOLD) { + if (mSavedataClean(&savedata->dirty, &savedata->dirtAge, frameCount)) { if (savedata->maskWriteback) { GBASavedataUnmask(savedata); } - savedata->dirty = 0; if (savedata->mapMode & MAP_WRITE) { size_t size = GBASavedataSize(savedata); if (savedata->data && savedata->vf->sync(savedata->vf, savedata->data, size)) { @@ -650,7 +645,7 @@ void _flashSwitchBank(struct GBASavedata* savedata, int bank) { void _flashErase(struct GBASavedata* savedata) { mLOG(GBA_SAVE, DEBUG, "Performing flash chip erase"); - savedata->dirty |= SAVEDATA_DIRT_NEW; + savedata->dirty |= mSAVEDATA_DIRT_NEW; size_t size = SIZE_CART_FLASH512; if (savedata->type == SAVEDATA_FLASH1M) { size = SIZE_CART_FLASH1M; @@ -660,7 +655,7 @@ void _flashErase(struct GBASavedata* savedata) { void _flashEraseSector(struct GBASavedata* savedata, uint16_t sectorStart) { mLOG(GBA_SAVE, DEBUG, "Performing flash sector erase at 0x%04x", sectorStart); - savedata->dirty |= SAVEDATA_DIRT_NEW; + savedata->dirty |= mSAVEDATA_DIRT_NEW; size_t size = 0x1000; if (savedata->type == SAVEDATA_FLASH1M) { mLOG(GBA_SAVE, DEBUG, "Performing unknown sector-size erase at 0x%04x", sectorStart); diff --git a/src/platform/opengl/gles2.c b/src/platform/opengl/gles2.c index 4c01fd3f0..4277b7b76 100644 --- a/src/platform/opengl/gles2.c +++ b/src/platform/opengl/gles2.c @@ -235,6 +235,7 @@ static void mGLES2ContextResized(struct VideoBackend* v, unsigned w, unsigned h) context->shaders[n].dirty = true; } } + context->finalShader.dirty = true; glBindTexture(GL_TEXTURE_2D, context->finalShader.tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, drawW, drawH, 0, GL_RGB, GL_UNSIGNED_BYTE, 0); glBindFramebuffer(GL_FRAMEBUFFER, context->finalShader.fbo); @@ -375,7 +376,6 @@ void mGLES2ContextDrawFrame(struct VideoBackend* v) { glGetIntegerv(GL_VIEWPORT, viewport); context->finalShader.filter = v->filter; - context->finalShader.dirty = true; _drawShader(context, &context->initialShader); if (v->interframeBlending) { context->interframeShader.blend = true; @@ -437,15 +437,19 @@ void mGLES2ContextCreate(struct mGLES2Context* context) { } void mGLES2ContextUseFramebuffer(struct mGLES2Context* context) { - glGenFramebuffers(1, &context->finalShader.fbo); - glGenTextures(1, &context->finalShader.tex); + if (!context->finalShader.fbo) { + glGenFramebuffers(1, &context->finalShader.fbo); + } + if (!context->finalShader.tex) { + glGenTextures(1, &context->finalShader.tex); - glBindTexture(GL_TEXTURE_2D, context->finalShader.tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glBindTexture(GL_TEXTURE_2D, 0); + glBindTexture(GL_TEXTURE_2D, context->finalShader.tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + } glBindFramebuffer(GL_FRAMEBUFFER, context->finalShader.fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, context->finalShader.tex, 0); diff --git a/src/platform/qt/AboutScreen.cpp b/src/platform/qt/AboutScreen.cpp index b1c1716ce..c0baa4b47 100644 --- a/src/platform/qt/AboutScreen.cpp +++ b/src/platform/qt/AboutScreen.cpp @@ -74,7 +74,7 @@ AboutScreen::AboutScreen(QWidget* parent) { QString copyright = m_ui.copyright->text(); - copyright.replace("{year}", tr("2021")); + copyright.replace("{year}", QLatin1String("2022")); m_ui.copyright->setText(copyright); } } diff --git a/src/platform/qt/Action.h b/src/platform/qt/Action.h index 3d1e24c5f..9fe2f6aa2 100644 --- a/src/platform/qt/Action.h +++ b/src/platform/qt/Action.h @@ -17,6 +17,12 @@ Q_OBJECT public: typedef std::function Function; typedef std::function BooleanFunction; + enum class Role { + NO_ROLE = 0, + ABOUT, + SETTINGS, + QUIT, + }; Action(Function, const QString& name, const QString& visibleName, QObject* parent = nullptr); Action(BooleanFunction, const QString& name, const QString& visibleName, QObject* parent = nullptr); @@ -44,8 +50,10 @@ public: bool isEnabled() const { return m_enabled; } bool isActive() const { return m_active; } bool isExclusive() const { return m_exclusive; } + Role role() const { return m_role; } void setExclusive(bool exclusive = true) { m_exclusive = exclusive; } + void setRole(Role role) { m_role = role; } Action& operator=(const Action&); @@ -62,6 +70,7 @@ private: bool m_enabled = true; bool m_active = false; bool m_exclusive = false; + Role m_role = Role::NO_ROLE; Function m_function; BooleanFunction m_booleanFunction; diff --git a/src/platform/qt/ActionMapper.cpp b/src/platform/qt/ActionMapper.cpp index a0d80a0d1..e0166d5aa 100644 --- a/src/platform/qt/ActionMapper.cpp +++ b/src/platform/qt/ActionMapper.cpp @@ -79,6 +79,20 @@ void ActionMapper::rebuildMenu(const QString& menu, QMenu* qmenu, QWidget* conte } else if (!m_defaultShortcuts[actionName].isEmpty()) { qaction->setShortcut(m_defaultShortcuts[actionName][0]); } + switch (action->role()) { + case Action::Role::NO_ROLE: + qaction->setMenuRole(QAction::NoRole); + break; + case Action::Role::SETTINGS: + qaction->setMenuRole(QAction::PreferencesRole); + break; + case Action::Role::ABOUT: + qaction->setMenuRole(QAction::AboutRole); + break; + case Action::Role::QUIT: + qaction->setMenuRole(QAction::QuitRole); + break; + } QObject::connect(qaction, &QAction::triggered, [qaction, action](bool enabled) { if (qaction->isCheckable()) { action->trigger(enabled); @@ -172,4 +186,4 @@ Action* ActionMapper::getAction(const QString& itemName) { QKeySequence ActionMapper::defaultShortcut(const QString& itemName) { return m_defaultShortcuts[itemName]; -} \ No newline at end of file +} diff --git a/src/platform/qt/ActionMapper.h b/src/platform/qt/ActionMapper.h index f99290bd2..b71f1fbda 100644 --- a/src/platform/qt/ActionMapper.h +++ b/src/platform/qt/ActionMapper.h @@ -73,7 +73,9 @@ private: template Action* ActionMapper::addAction(const QString& visibleName, const QString& name, T* obj, V (T::*method)(), const QString& menu, const QKeySequence& shortcut) { - return addAction(visibleName, name, std::bind(method, obj), menu, shortcut); + return addAction(visibleName, name, [method, obj]() -> void { + (obj->*method)(); + }, menu, shortcut); } template diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 79e79fdc8..97c1d4b99 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -4,6 +4,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(PLATFORM_SRC) set(QT_STATIC OFF) +set(QT_DEFINES) if(BUILD_SDL) add_definitions(-DBUILD_SDL) @@ -36,6 +37,11 @@ if(NOT Qt5Widgets_FOUND) endif() if(APPLE) + execute_process(COMMAND xcrun --show-sdk-version OUTPUT_VARIABLE MACOSX_SDK) + if(MACOSX_SDK VERSION_GREATER 10.14) + list(APPEND QT_DEFINES USE_SHARE_WIDGET) + endif() + if(Qt5Widgets_VERSION MATCHES "^5.1[0-9]") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mmacosx-version-min=10.8") else() @@ -186,7 +192,6 @@ if(M_CORE_GB) list(APPEND PLATFORM_SRC ${GB_SRC}) endif() -set(QT_DEFINES) if(Qt5Multimedia_FOUND) list(APPEND AUDIO_SRC AudioProcessorQt.cpp diff --git a/src/platform/qt/CoreController.cpp b/src/platform/qt/CoreController.cpp index a72c7baaa..03b79d2ef 100644 --- a/src/platform/qt/CoreController.cpp +++ b/src/platform/qt/CoreController.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #ifdef M_CORE_GBA #include @@ -86,12 +87,17 @@ CoreController::CoreController(mCore* core, QObject* parent) } controller->m_resetActions.clear(); + controller->m_frameCounter = -1; if (!controller->m_hwaccel) { context->core->setVideoBuffer(context->core, reinterpret_cast(controller->m_activeBuffer.data()), controller->screenDimensions().width()); } + QString message(tr("Reset r%1-%2 %3").arg(gitRevision).arg(QLatin1String(gitCommitShort)).arg(controller->m_crc32, 8, 16, QLatin1Char('0'))); QMetaObject::invokeMethod(controller, "didReset"); + if (controller->m_showResetInfo) { + QMetaObject::invokeMethod(controller, "statusPosted", Q_ARG(const QString&, message)); + } controller->finishFrame(); }; @@ -483,6 +489,10 @@ void CoreController::setSync(bool sync) { } } +void CoreController::showResetInfo(bool enable) { + m_showResetInfo = enable; +} + void CoreController::setRewinding(bool rewind) { if (!m_threadContext.core->opts.rewindEnable) { return; @@ -1053,6 +1063,7 @@ void CoreController::finishFrame() { mCoreThreadPauseFromThread(&m_threadContext); } } + ++m_frameCounter; } updateKeys(); diff --git a/src/platform/qt/CoreController.h b/src/platform/qt/CoreController.h index 372f01406..5d4d5914c 100644 --- a/src/platform/qt/CoreController.h +++ b/src/platform/qt/CoreController.h @@ -126,6 +126,7 @@ public: bool videoSync() const { return m_videoSync; } void addFrameAction(std::function callback); + uint64_t frameCounter() const { return m_frameCounter; } public slots: void start(); @@ -134,6 +135,7 @@ public slots: void setPaused(bool paused); void frameAdvance(); void setSync(bool enable); + void showResetInfo(bool enable); void setRewinding(bool); void rewind(int count = 0); @@ -232,6 +234,7 @@ private: uint32_t m_crc32; QString m_internalTitle; QString m_dbTitle; + bool m_showResetInfo = false; QByteArray m_activeBuffer; QByteArray m_completeBuffer; @@ -240,6 +243,7 @@ private: std::unique_ptr m_cacheSet; std::unique_ptr m_override; + uint64_t m_frameCounter; QList> m_resetActions; QList> m_frameActions; QMutex m_actionMutex{QMutex::Recursive}; diff --git a/src/platform/qt/Display.cpp b/src/platform/qt/Display.cpp index f85c43d75..1eec61d96 100644 --- a/src/platform/qt/Display.cpp +++ b/src/platform/qt/Display.cpp @@ -35,10 +35,12 @@ Display* Display::create(QWidget* parent) { if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) { format.setVersion(2, 0); } else { - format.setVersion(3, 2); + format.setVersion(3, 3); } format.setProfile(QSurfaceFormat::CoreProfile); - if (!DisplayGL::supportsFormat(format)) { + if (DisplayGL::supportsFormat(format)) { + QSurfaceFormat::setDefaultFormat(format); + } else { #ifdef BUILD_GL LOG(QT, WARN) << ("Failed to create an OpenGL Core context, trying old-style..."); format.setVersion(1, 4); @@ -102,14 +104,20 @@ QSize Display::viewportSize() { } void Display::attach(std::shared_ptr controller) { - connect(controller.get(), &CoreController::stateLoaded, this, &Display::resizeContext); - connect(controller.get(), &CoreController::stateLoaded, this, &Display::forceDraw); - connect(controller.get(), &CoreController::rewound, this, &Display::forceDraw); - connect(controller.get(), &CoreController::paused, this, &Display::pauseDrawing); - connect(controller.get(), &CoreController::unpaused, this, &Display::unpauseDrawing); - connect(controller.get(), &CoreController::frameAvailable, this, &Display::framePosted); - connect(controller.get(), &CoreController::statusPosted, this, &Display::showMessage); - connect(controller.get(), &CoreController::didReset, this, &Display::resizeContext); + CoreController* controllerP = controller.get(); + connect(controllerP, &CoreController::stateLoaded, this, &Display::resizeContext); + connect(controllerP, &CoreController::stateLoaded, this, &Display::forceDraw); + connect(controllerP, &CoreController::rewound, this, &Display::forceDraw); + connect(controllerP, &CoreController::paused, this, &Display::pauseDrawing); + connect(controllerP, &CoreController::unpaused, this, &Display::unpauseDrawing); + connect(controllerP, &CoreController::frameAvailable, this, &Display::framePosted); + connect(controllerP, &CoreController::frameAvailable, this, [controllerP, this]() { + if (m_showFrameCounter) { + m_messagePainter.showFrameCounter(controllerP->frameCounter()); + } + }); + connect(controllerP, &CoreController::statusPosted, this, &Display::showMessage); + connect(controllerP, &CoreController::didReset, this, &Display::resizeContext); } void Display::configure(ConfigController* config) { @@ -119,6 +127,7 @@ void Display::configure(ConfigController* config) { interframeBlending(opts->interframeBlending); filter(opts->resampleVideo); config->updateOption("showOSD"); + config->updateOption("showFrameCounter"); #if defined(BUILD_GL) || defined(BUILD_GLES2) || defined(BUILD_GLES3) if (opts->shader) { struct VDir* shader = VDirOpen(opts->shader); @@ -131,12 +140,15 @@ void Display::configure(ConfigController* config) { } void Display::resizeEvent(QResizeEvent*) { - m_messagePainter.resize(size(), m_lockAspectRatio, devicePixelRatio()); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + m_messagePainter.resize(size(), devicePixelRatioF()); +#else + m_messagePainter.resize(size(), devicePixelRatio()); +#endif } void Display::lockAspectRatio(bool lock) { m_lockAspectRatio = lock; - m_messagePainter.resize(size(), m_lockAspectRatio, devicePixelRatio()); } void Display::lockIntegerScaling(bool lock) { @@ -151,6 +163,13 @@ void Display::showOSDMessages(bool enable) { m_showOSD = enable; } +void Display::showFrameCounter(bool enable) { + m_showFrameCounter = enable; + if (!enable) { + m_messagePainter.clearFrameCounter(); + } +} + void Display::filter(bool filter) { m_filter = filter; } diff --git a/src/platform/qt/Display.h b/src/platform/qt/Display.h index 5babf8d59..9e20c092f 100644 --- a/src/platform/qt/Display.h +++ b/src/platform/qt/Display.h @@ -42,6 +42,7 @@ public: bool hasInterframeBlending() const { return m_interframeBlending; } bool isFiltered() const { return m_filter; } bool isShowOSD() const { return m_showOSD; } + bool isShowFrameCounter() const { return m_showFrameCounter; } virtual void attach(std::shared_ptr); virtual void configure(ConfigController*); @@ -71,6 +72,7 @@ public slots: virtual void lockIntegerScaling(bool lock); virtual void interframeBlending(bool enable); virtual void showOSDMessages(bool enable); + virtual void showFrameCounter(bool enable); virtual void filter(bool filter); virtual void framePosted() = 0; virtual void setShaders(struct VDir*) = 0; @@ -93,6 +95,7 @@ private: MessagePainter m_messagePainter; bool m_showOSD = true; + bool m_showFrameCounter = false; bool m_lockAspectRatio = false; bool m_lockIntegerScaling = false; bool m_interframeBlending = false; diff --git a/src/platform/qt/DisplayGL.cpp b/src/platform/qt/DisplayGL.cpp index be69e3ac7..2f71b7745 100644 --- a/src/platform/qt/DisplayGL.cpp +++ b/src/platform/qt/DisplayGL.cpp @@ -9,14 +9,14 @@ #include #include -#include -#include #include +#include #include #include #include #include #include +#include #include @@ -32,6 +32,12 @@ #endif #endif +#ifdef _WIN32 +#define OVERHEAD_NSEC 1000000 +#else +#define OVERHEAD_NSEC 300000 +#endif + using namespace QGBA; QHash DisplayGL::s_supports; @@ -43,13 +49,96 @@ uint qHash(const QSurfaceFormat& format, uint seed) { return qHash(representation, seed); } +void mGLWidget::initializeGL() { + m_vao.create(); + m_program.create(); + + m_program.addShaderFromSourceCode(QOpenGLShader::Vertex, R"(#version 150 core + in vec4 position; + out vec2 texCoord; + void main() { + gl_Position = position; + texCoord = (position.st + 1.0) * 0.5; + })"); + + m_program.addShaderFromSourceCode(QOpenGLShader::Fragment, R"(#version 150 core + in vec2 texCoord; + out vec4 color; + uniform sampler2D tex; + void main() { + color = vec4(texture(tex, texCoord).rgb, 1.0); + })"); + + m_program.link(); + m_program.setUniformValue("tex", 0); + m_positionLocation = m_program.attributeLocation("position"); + + connect(&m_refresh, &QTimer::timeout, this, static_cast(&QWidget::update)); +} + +void mGLWidget::finalizeVAO() { + QOpenGLFunctions_3_2_Core* fn = context()->versionFunctions(); + fn->glGetError(); // Clear the error + m_vao.bind(); + fn->glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + fn->glEnableVertexAttribArray(m_positionLocation); + fn->glVertexAttribPointer(m_positionLocation, 2, GL_FLOAT, GL_FALSE, 0, NULL); + m_vao.release(); + if (fn->glGetError() == GL_NO_ERROR) { + m_vaoDone = true; + } +} + +void mGLWidget::paintGL() { + if (!m_vaoDone) { + finalizeVAO(); + } + QOpenGLFunctions_3_2_Core* fn = context()->versionFunctions(); + m_program.bind(); + m_vao.bind(); + fn->glBindTexture(GL_TEXTURE_2D, m_tex); + fn->glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + fn->glBindTexture(GL_TEXTURE_2D, 0); + m_vao.release(); + m_program.release(); + + // TODO: Better timing + ++m_refreshResidue; + if (m_refreshResidue == 3) { + m_refresh.start(16); + m_refreshResidue = 0; + } else { + m_refresh.start(17); + } +} + DisplayGL::DisplayGL(const QSurfaceFormat& format, QWidget* parent) : Display(parent) { setAttribute(Qt::WA_NativeWindow); + window()->windowHandle()->setFormat(format); windowHandle()->create(); - m_painter = std::make_unique(windowHandle(), format); +#ifdef USE_SHARE_WIDGET + bool useShareWidget = true; +#else + // TODO: Does using this on Wayland help? + bool useShareWidget = false; +#endif + + if (useShareWidget) { + m_gl = new mGLWidget; + m_gl->setAttribute(Qt::WA_NativeWindow); + m_gl->setFormat(format); + QBoxLayout* layout = new QVBoxLayout; + layout->addWidget(m_gl); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + } else { + m_gl = nullptr; + } + + m_painter = std::make_unique(windowHandle(), m_gl, format); m_drawThread.setObjectName("Painter Thread"); m_painter->setThread(&m_drawThread); @@ -98,16 +187,20 @@ void DisplayGL::startDrawing(std::shared_ptr controller) { lockIntegerScaling(isIntegerScalingLocked()); interframeBlending(hasInterframeBlending()); showOSDMessages(isShowOSD()); + showFrameCounter(isShowFrameCounter()); filter(isFiltered()); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - messagePainter()->resize(size(), isAspectRatioLocked(), devicePixelRatioF()); + messagePainter()->resize(size(), devicePixelRatioF()); #else - messagePainter()->resize(size(), isAspectRatioLocked(), devicePixelRatio()); + messagePainter()->resize(size(), devicePixelRatio()); #endif + CoreController::Interrupter interrupter(controller); QMetaObject::invokeMethod(m_painter.get(), "start"); - setUpdatesEnabled(false); + if (!m_gl) { + setUpdatesEnabled(false); + } } bool DisplayGL::supportsFormat(const QSurfaceFormat& format) { @@ -189,7 +282,9 @@ void DisplayGL::unpauseDrawing() { m_isDrawing = true; QMetaObject::invokeMethod(m_painter.get(), "unpause", Qt::BlockingQueuedConnection); #ifndef Q_OS_MAC - setUpdatesEnabled(false); + if (!m_gl) { + setUpdatesEnabled(false); + } #endif } } @@ -220,6 +315,11 @@ void DisplayGL::showOSDMessages(bool enable) { QMetaObject::invokeMethod(m_painter.get(), "showOSD", Q_ARG(bool, enable)); } +void DisplayGL::showFrameCounter(bool enable) { + Display::showFrameCounter(enable); + QMetaObject::invokeMethod(m_painter.get(), "showFrameCounter", Q_ARG(bool, enable)); +} + void DisplayGL::filter(bool filter) { Display::filter(filter); QMetaObject::invokeMethod(m_painter.get(), "filter", Q_ARG(bool, filter)); @@ -274,10 +374,21 @@ int DisplayGL::framebufferHandle() { return m_painter->glTex(); } -PainterGL::PainterGL(QWindow* surface, const QSurfaceFormat& format) - : m_surface(surface) +PainterGL::PainterGL(QWindow* window, mGLWidget* widget, const QSurfaceFormat& format) + : m_window(window) , m_format(format) + , m_widget(widget) { + if (widget) { + m_format = widget->format(); + QOffscreenSurface* surface = new QOffscreenSurface; + surface->setScreen(window->screen()); + surface->setFormat(m_format); + surface->create(); + m_surface = surface; + } else { + m_surface = m_window; + } m_supportsShaders = m_format.version() >= qMakePair(2, 0); for (auto& buf : m_buffers) { m_free.append(&buf.front()); @@ -307,6 +418,9 @@ void PainterGL::makeCurrent() { void PainterGL::create() { m_gl = std::make_unique(); m_gl->setFormat(m_format); + if (m_widget) { + m_gl->setShareContext(m_widget->context()); + } m_gl->create(); makeCurrent(); @@ -317,7 +431,7 @@ void PainterGL::create() { mGLES2Context* gl2Backend; #endif - m_window = std::make_unique(); + m_paintDev = std::make_unique(); #if defined(BUILD_GLES2) || defined(BUILD_GLES3) auto version = m_format.version(); @@ -342,11 +456,36 @@ void PainterGL::create() { } painter->m_gl->swapBuffers(painter->m_surface); painter->makeCurrent(); + + if (painter->m_widget && painter->supportsShaders()) { + QOpenGLFunctions_3_2_Core* fn = painter->m_gl->versionFunctions(); + fn->glFinish(); + mGLES2Context* gl2Backend = reinterpret_cast(painter->m_backend); + painter->m_widget->setTex(painter->m_finalTex[painter->m_finalTexIdx]); + painter->m_finalTexIdx ^= 1; + gl2Backend->finalShader.tex = painter->m_finalTex[painter->m_finalTexIdx]; + mGLES2ContextUseFramebuffer(gl2Backend); + } }; m_backend->init(m_backend, 0); #if defined(BUILD_GLES2) || defined(BUILD_GLES3) if (m_supportsShaders) { + if (m_widget) { + m_widget->setVBO(gl2Backend->vbo); + + gl2Backend->finalShader.tex = 0; + mGLES2ContextUseFramebuffer(gl2Backend); + m_finalTex[0] = gl2Backend->finalShader.tex; + + gl2Backend->finalShader.tex = 0; + mGLES2ContextUseFramebuffer(gl2Backend); + m_finalTex[1] = gl2Backend->finalShader.tex; + + m_finalTexIdx = 0; + gl2Backend->finalShader.tex = m_finalTex[m_finalTexIdx]; + m_widget->setTex(m_finalTex[m_finalTexIdx]); + } m_shader.preprocessShader = static_cast(&reinterpret_cast(m_backend)->initialShader); } #endif @@ -369,7 +508,7 @@ void PainterGL::destroy() { #endif m_backend->deinit(m_backend); m_gl->doneCurrent(); - m_window.reset(); + m_paintDev.reset(); m_gl.reset(); free(m_backend); @@ -404,8 +543,10 @@ void PainterGL::setMessagePainter(MessagePainter* messagePainter) { } void PainterGL::resize(const QSize& size) { + qreal r = m_window->devicePixelRatio(); m_size = size; - m_window->setSize(m_size); + m_paintDev->setSize(m_size * r); + m_paintDev->setDevicePixelRatio(r); if (m_started && !m_active) { forceDraw(); } @@ -429,6 +570,10 @@ void PainterGL::showOSD(bool enable) { m_showOSD = enable; } +void PainterGL::showFrameCounter(bool enable) { + m_showFrameCounter = enable; +} + void PainterGL::filter(bool filter) { m_backend->filter = filter; if (m_started && !m_active) { @@ -462,7 +607,7 @@ void PainterGL::draw() { if (!sync->audioWait && !sync->videoFrameWait) { return; } - if (m_delayTimer.elapsed() >= 1000 / m_surface->screen()->refreshRate()) { + if (m_delayTimer.elapsed() >= 1000 / m_window->screen()->refreshRate()) { return; } if (!m_drawTimer.isActive()) { @@ -476,13 +621,13 @@ void PainterGL::draw() { m_delayTimer.start(); } else { if (sync->audioWait || sync->videoFrameWait) { - while (m_delayTimer.nsecsElapsed() + 1000000 < 1000000000 / sync->fpsTarget) { + while (m_delayTimer.nsecsElapsed() + OVERHEAD_NSEC < 1000000000 / sync->fpsTarget) { QThread::usleep(500); } forceRedraw = sync->videoFrameWait; } if (!forceRedraw) { - forceRedraw = m_delayTimer.nsecsElapsed() + 1000000 >= 1000000000 / m_surface->screen()->refreshRate(); + forceRedraw = m_delayTimer.nsecsElapsed() + OVERHEAD_NSEC >= 1000000000 / m_window->screen()->refreshRate(); } } mCoreSyncWaitFrameEnd(sync); @@ -497,7 +642,7 @@ void PainterGL::draw() { void PainterGL::forceDraw() { performDraw(); if (!m_context->thread()->impl->sync.audioWait && !m_context->thread()->impl->sync.videoFrameWait) { - if (m_delayTimer.elapsed() < 1000 / m_surface->screen()->refreshRate()) { + if (m_delayTimer.elapsed() < 1000 / m_window->screen()->refreshRate()) { return; } m_delayTimer.restart(); @@ -522,7 +667,7 @@ void PainterGL::stop() { } if (m_videoProxy) { m_videoProxy->reset(); - m_videoProxy->moveToThread(m_surface->thread()); + m_videoProxy->moveToThread(m_window->thread()); m_videoProxy.reset(); } m_backend->clear(m_backend); @@ -540,14 +685,14 @@ void PainterGL::unpause() { } void PainterGL::performDraw() { - float r = m_surface->devicePixelRatio(); + float r = m_window->devicePixelRatio(); m_backend->resized(m_backend, m_size.width() * r, m_size.height() * r); if (m_buffer) { m_backend->postFrame(m_backend, m_buffer); } m_backend->drawFrame(m_backend); if (m_showOSD && m_messagePainter) { - m_painter.begin(m_window.get()); + m_painter.begin(m_paintDev.get()); m_messagePainter->paint(&m_painter); m_painter.end(); } diff --git a/src/platform/qt/DisplayGL.h b/src/platform/qt/DisplayGL.h index d31921151..54256ed30 100644 --- a/src/platform/qt/DisplayGL.h +++ b/src/platform/qt/DisplayGL.h @@ -21,7 +21,11 @@ #include #include #include +#include #include +#include +#include +#include #include #include #include @@ -35,11 +39,37 @@ #include "platform/video-backend.h" class QOpenGLPaintDevice; +class QOpenGLWidget; uint qHash(const QSurfaceFormat&, uint seed = 0); namespace QGBA { +class mGLWidget : public QOpenGLWidget { +Q_OBJECT + +public: + void setTex(GLuint tex) { m_tex = tex; } + void setVBO(GLuint vbo) { m_vbo = vbo; } + void finalizeVAO(); + +protected: + void initializeGL() override; + void paintGL() override; + +private: + GLuint m_tex; + GLuint m_vbo; + + bool m_vaoDone = false; + QOpenGLVertexArrayObject m_vao; + QOpenGLShaderProgram m_program; + GLuint m_positionLocation; + + QTimer m_refresh; + int m_refreshResidue = 0; +}; + class PainterGL; class DisplayGL : public Display { Q_OBJECT @@ -66,6 +96,7 @@ public slots: void lockIntegerScaling(bool lock) override; void interframeBlending(bool enable) override; void showOSDMessages(bool enable) override; + void showFrameCounter(bool enable) override; void filter(bool filter) override; void framePosted() override; void setShaders(struct VDir*) override; @@ -87,13 +118,14 @@ private: std::unique_ptr m_painter; QThread m_drawThread; std::shared_ptr m_context; + mGLWidget* m_gl; }; class PainterGL : public QObject { Q_OBJECT public: - PainterGL(QWindow* surface, const QSurfaceFormat& format); + PainterGL(QWindow* surface, mGLWidget* widget, const QSurfaceFormat& format); ~PainterGL(); void setThread(QThread*); @@ -122,6 +154,7 @@ public slots: void lockIntegerScaling(bool lock); void interframeBlending(bool enable); void showOSD(bool enable); + void showFrameCounter(bool enable); void filter(bool filter); void resizeContext(); @@ -144,10 +177,14 @@ private: uint32_t* m_buffer = nullptr; QPainter m_painter; QMutex m_mutex; - QWindow* m_surface; + QWindow* m_window; + QSurface* m_surface; QSurfaceFormat m_format; - std::unique_ptr m_window; + std::unique_ptr m_paintDev; std::unique_ptr m_gl; + int m_finalTexIdx = 0; + GLuint m_finalTex[2]; + mGLWidget* m_widget; bool m_active = false; bool m_started = false; QTimer m_drawTimer; @@ -155,6 +192,7 @@ private: CoreController::Interrupter m_interrupter; bool m_supportsShaders; bool m_showOSD; + bool m_showFrameCounter; VideoShader m_shader{}; VideoBackend* m_backend = nullptr; QSize m_size; diff --git a/src/platform/qt/DisplayQt.cpp b/src/platform/qt/DisplayQt.cpp index e21a87ded..500e5b888 100644 --- a/src/platform/qt/DisplayQt.cpp +++ b/src/platform/qt/DisplayQt.cpp @@ -107,7 +107,7 @@ void DisplayQt::paintEvent(QPaintEvent*) { } painter.drawImage(full, m_backing, QRect(0, 0, m_width, m_height)); painter.setOpacity(1); - if (isShowOSD()) { + if (isShowOSD() || isShowFrameCounter()) { messagePainter()->paint(&painter); } } diff --git a/src/platform/qt/MessagePainter.cpp b/src/platform/qt/MessagePainter.cpp index 15ab748aa..cf72051ff 100644 --- a/src/platform/qt/MessagePainter.cpp +++ b/src/platform/qt/MessagePainter.cpp @@ -18,6 +18,8 @@ MessagePainter::MessagePainter(QObject* parent) { m_messageFont = GBAApp::app()->monospaceFont(); m_messageFont.setPixelSize(13); + m_frameFont = GBAApp::app()->monospaceFont(); + m_frameFont.setPixelSize(10); connect(&m_messageTimer, &QTimer::timeout, this, &MessagePainter::clearMessage); m_messageTimer.setSingleShot(true); m_messageTimer.setInterval(5000); @@ -25,40 +27,35 @@ MessagePainter::MessagePainter(QObject* parent) clearMessage(); } -void MessagePainter::resize(const QSize& size, bool lockAspectRatio, qreal scaleFactor) { - int w = size.width(); - int h = size.height(); - int drawW = w; - int drawH = h; - if (lockAspectRatio) { - if (w * 2 > h * 3) { - drawW = h * 3 / 2; - } else if (w * 2 < h * 3) { - drawH = w * 2 / 3; - } - } - m_world.reset(); - m_world.scale(qreal(drawW) / GBA_VIDEO_HORIZONTAL_PIXELS, qreal(drawH) / GBA_VIDEO_VERTICAL_PIXELS); +void MessagePainter::resize(const QSize& size, qreal scaleFactor) { + double drawW = size.width(); + double drawH = size.height(); + double area = pow(drawW * drawW * drawW * drawH * drawH, 0.2); m_scaleFactor = scaleFactor; - m_local = QPoint(1, GBA_VIDEO_VERTICAL_PIXELS - m_messageFont.pixelSize() - 1); - m_local = m_world.map(m_local); - m_local += QPoint((w - drawW) / 2, (h - drawH) / 2); - m_pixmapBuffer = QPixmap(drawW * m_scaleFactor, - (m_messageFont.pixelSize() + 2) * m_world.m22() * m_scaleFactor); - m_pixmapBuffer.setDevicePixelRatio(m_scaleFactor); + m_world.reset(); + m_world.scale(area / 220., area / 220.); + m_local = QPoint(area / 100., drawH - m_messageFont.pixelSize() * m_world.m22() * 1.3); + + QFontMetrics metrics(m_frameFont); + m_framePoint = QPoint(drawW / m_world.m11() - metrics.height() * 0.1, metrics.height() * 0.75); + m_mutex.lock(); - m_message.prepare(m_world, m_messageFont); redraw(); m_mutex.unlock(); } void MessagePainter::redraw() { - m_pixmapBuffer.fill(Qt::transparent); if (m_message.text().isEmpty()) { + m_pixmapBuffer.fill(Qt::transparent); m_pixmap = m_pixmapBuffer; - m_pixmap.setDevicePixelRatio(m_scaleFactor); return; } + m_message.prepare(m_world, m_messageFont); + QSizeF sizef = m_message.size() * m_scaleFactor; + m_pixmapBuffer = QPixmap(sizef.width() * m_world.m11(), sizef.height() * m_world.m22()); + m_pixmapBuffer.setDevicePixelRatio(m_scaleFactor); + m_pixmapBuffer.fill(Qt::transparent); + QPainter painter(&m_pixmapBuffer); painter.setWorldTransform(m_world); painter.setRenderHint(QPainter::Antialiasing); @@ -74,16 +71,32 @@ void MessagePainter::redraw() { painter.setPen(Qt::white); painter.drawStaticText(0, 0, m_message); m_pixmap = m_pixmapBuffer; - m_pixmap.setDevicePixelRatio(m_scaleFactor); } void MessagePainter::paint(QPainter* painter) { if (!m_message.text().isEmpty()) { painter->drawPixmap(m_local, m_pixmap); } + if (m_drawFrameCounter) { + QString frame(tr("Frame %1").arg(m_frameCounter)); + QFontMetrics metrics(m_frameFont); + painter->setWorldTransform(m_world); + painter->setRenderHint(QPainter::Antialiasing); + painter->setFont(m_frameFont); + painter->setPen(Qt::black); + painter->translate(-metrics.width(frame), 0); + const static int ITERATIONS = 11; + for (int i = 0; i < ITERATIONS; ++i) { + painter->save(); + painter->translate(cos(i * 2.0 * M_PI / ITERATIONS) * 0.8, sin(i * 2.0 * M_PI / ITERATIONS) * 0.8); + painter->drawText(m_framePoint, frame); + painter->restore(); + } + painter->setPen(Qt::white); + painter->drawText(m_framePoint, frame); + } } - void MessagePainter::showMessage(const QString& message) { m_mutex.lock(); m_message.setText(message); @@ -100,3 +113,16 @@ void MessagePainter::clearMessage() { m_mutex.unlock(); m_messageTimer.stop(); } + +void MessagePainter::showFrameCounter(uint64_t frameCounter) { + m_mutex.lock(); + m_frameCounter = frameCounter; + m_drawFrameCounter = true; + m_mutex.unlock(); +} + +void MessagePainter::clearFrameCounter() { + m_mutex.lock(); + m_drawFrameCounter = false; + m_mutex.unlock(); +} diff --git a/src/platform/qt/MessagePainter.h b/src/platform/qt/MessagePainter.h index 37f24c766..8b40afdc4 100644 --- a/src/platform/qt/MessagePainter.h +++ b/src/platform/qt/MessagePainter.h @@ -19,7 +19,7 @@ Q_OBJECT public: MessagePainter(QObject* parent = nullptr); - void resize(const QSize& size, bool lockAspectRatio, qreal scaleFactor); + void resize(const QSize& size, qreal scaleFactor); void paint(QPainter* painter); void setScaleFactor(qreal factor); @@ -27,18 +27,28 @@ public slots: void showMessage(const QString& message); void clearMessage(); + void showFrameCounter(uint64_t); + void clearFrameCounter(); + private: void redraw(); QMutex m_mutex; QStaticText m_message; + qreal m_scaleFactor = 1; + uint64_t m_frameCounter; + bool m_drawFrameCounter = false; + + QPoint m_local; QPixmap m_pixmap; QPixmap m_pixmapBuffer; + + QPointF m_framePoint = QPointF(0, 0); + QFont m_frameFont; + QTimer m_messageTimer{this}; - QPoint m_local; QTransform m_world; QFont m_messageFont; - qreal m_scaleFactor = 1; }; } diff --git a/src/platform/qt/SettingsView.cpp b/src/platform/qt/SettingsView.cpp index 8ce54caa0..67e2009d8 100644 --- a/src/platform/qt/SettingsView.cpp +++ b/src/platform/qt/SettingsView.cpp @@ -452,6 +452,8 @@ void SettingsView::updateConfig() { saveSetting("lockIntegerScaling", m_ui.lockIntegerScaling); saveSetting("interframeBlending", m_ui.interframeBlending); saveSetting("showOSD", m_ui.showOSD); + saveSetting("showFrameCounter", m_ui.showFrameCounter); + saveSetting("showResetInfo", m_ui.showResetInfo); saveSetting("volume", m_ui.volume); saveSetting("mute", m_ui.mute); saveSetting("fastForwardVolume", m_ui.volumeFf); @@ -675,6 +677,8 @@ void SettingsView::reloadConfig() { loadSetting("lockIntegerScaling", m_ui.lockIntegerScaling); loadSetting("interframeBlending", m_ui.interframeBlending); loadSetting("showOSD", m_ui.showOSD, true); + loadSetting("showFrameCounter", m_ui.showFrameCounter); + loadSetting("showResetInfo", m_ui.showResetInfo); loadSetting("volume", m_ui.volume, 0x100); loadSetting("mute", m_ui.mute, false); loadSetting("fastForwardVolume", m_ui.volumeFf, m_ui.volume->value()); diff --git a/src/platform/qt/SettingsView.ui b/src/platform/qt/SettingsView.ui index d5bfd2942..7ba8bb3e1 100644 --- a/src/platform/qt/SettingsView.ui +++ b/src/platform/qt/SettingsView.ui @@ -594,6 +594,13 @@ + + + + When inactive: + + + @@ -612,6 +619,31 @@ + + + + When minimized: + + + + + + + + + Pause + + + + + + + Mute + + + + + @@ -667,20 +699,41 @@ + + + 20 + + + + + Show frame count in OSD + + + + + + + Show emulation info on reset + + + + + + Enable Discord Rich Presence - + Qt::Horizontal - + Automatically save state @@ -690,7 +743,7 @@ - + Automatically load state @@ -700,14 +753,14 @@ - + Qt::Horizontal - + Automatically save cheats @@ -717,7 +770,7 @@ - + Automatically load cheats @@ -727,38 +780,6 @@ - - - - - - Pause - - - - - - - Mute - - - - - - - - - When inactive: - - - - - - - When minimized: - - - @@ -2394,6 +2415,38 @@ + + showOSD + toggled(bool) + showFrameCounter + setEnabled(bool) + + + 374 + 391 + + + 418 + 431 + + + + + showOSD + toggled(bool) + showResetInfo + setEnabled(bool) + + + 374 + 391 + + + 418 + 470 + + + diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index 2b79d6755..7ff3a3c87 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -771,7 +771,9 @@ void Window::focusInEvent(QFocusEvent*) { updateMultiplayerActive(true); } } - m_display->forceDraw(); + if (m_display) { + m_display->forceDraw(); + } } void Window::focusOutEvent(QFocusEvent*) { @@ -899,7 +901,6 @@ void Window::gameStarted() { m_config->updateOption("lockAspectRatio"); m_config->updateOption("interframeBlending"); m_config->updateOption("resampleVideo"); - m_config->updateOption("showOSD"); if (m_savedScale > 0) { resizeFrame(size * m_savedScale); } @@ -1262,12 +1263,25 @@ void Window::setupMenu(QMenuBar* menubar) { m_actions.addAction(tr("Add folder to library..."), "addDirToLibrary", this, &Window::addDirToLibrary, "file"); #endif + m_actions.addMenu(tr("Save games"), "saves", "file"); addGameAction(tr("Load alternate save game..."), "loadAlternateSave", [this]() { this->selectSave(false); - }, "file"); + }, "saves"); addGameAction(tr("Load temporary save game..."), "loadTemporarySave", [this]() { this->selectSave(true); - }, "file"); + }, "saves"); + + m_actions.addSeparator("saves"); + + m_actions.addAction(tr("Convert save game..."), "convertSave", openControllerTView(), "saves"); + +#ifdef M_CORE_GBA + Action* importShark = addGameAction(tr("Import GameShark Save..."), "importShark", this, &Window::importSharkport, "saves"); + m_platformActions.insert(mPLATFORM_GBA, importShark); + + Action* exportShark = addGameAction(tr("Export GameShark Save..."), "exportShark", this, &Window::exportSharkport, "saves"); + m_platformActions.insert(mPLATFORM_GBA, exportShark); +#endif m_actions.addAction(tr("Load &patch..."), "loadPatch", this, &Window::selectPatch, "file"); @@ -1367,17 +1381,6 @@ void Window::setupMenu(QMenuBar* menubar) { m_platformActions.insert(mPLATFORM_GB, quickSave); } -#ifdef M_CORE_GBA - m_actions.addSeparator("file"); - m_actions.addAction(tr("Convert save game..."), "convertSave", openControllerTView(), "file"); - - Action* importShark = addGameAction(tr("Import GameShark Save..."), "importShark", this, &Window::importSharkport, "file"); - m_platformActions.insert(mPLATFORM_GBA, importShark); - - Action* exportShark = addGameAction(tr("Export GameShark Save..."), "exportShark", this, &Window::exportSharkport, "file"); - m_platformActions.insert(mPLATFORM_GBA, exportShark); -#endif - m_actions.addSeparator("file"); m_multiWindow = m_actions.addAction(tr("New multiplayer window"), "multiWindow", [this]() { GBAApp::app()->newWindow(); @@ -1396,11 +1399,8 @@ void Window::setupMenu(QMenuBar* menubar) { m_actions.addSeparator("file"); #endif - m_actions.addAction(tr("About..."), "about", openTView(), "file"); - -#ifndef Q_OS_MAC - m_actions.addAction(tr("E&xit"), "quit", static_cast(this), &QWidget::close, "file", QKeySequence::Quit); -#endif + m_actions.addAction(tr("About..."), "about", openTView(), "file")->setRole(Action::Role::ABOUT); + m_actions.addAction(tr("E&xit"), "quit", static_cast(this), &QWidget::close, "file", QKeySequence::Quit)->setRole(Action::Role::SETTINGS); m_actions.addMenu(tr("&Emulation"), "emu"); @@ -1671,7 +1671,7 @@ void Window::setupMenu(QMenuBar* menubar) { addGameAction(tr("&Cheats..."), "cheatsWindow", openControllerTView(), "tools"); m_actions.addSeparator("tools"); - m_actions.addAction(tr("Settings..."), "settings", this, &Window::openSettingsWindow, "tools"); + m_actions.addAction(tr("Settings..."), "settings", this, &Window::openSettingsWindow, "tools")->setRole(Action::Role::SETTINGS); m_actions.addAction(tr("Make portable"), "makePortable", this, &Window::tryMakePortable, "tools"); #ifdef USE_DEBUGGERS @@ -1799,6 +1799,20 @@ void Window::setupMenu(QMenuBar* menubar) { } }, this); + ConfigOption* showFrameCounter = m_config->addOption("showFrameCounter"); + showFrameCounter->connect([this](const QVariant& value) { + if (m_display) { + m_display->showFrameCounter(value.toBool()); + } + }, this); + + ConfigOption* showResetInfo = m_config->addOption("showResetInfo"); + showResetInfo->connect([this](const QVariant& value) { + if (m_controller) { + m_controller->showResetInfo(value.toBool()); + } + }, this); + ConfigOption* videoScale = m_config->addOption("videoScale"); videoScale->connect([this](const QVariant& value) { if (m_display) { @@ -2045,6 +2059,9 @@ void Window::setController(CoreController* controller, const QString& fname) { attachDisplay(); m_controller->loadConfig(m_config); + m_config->updateOption("showOSD"); + m_config->updateOption("showFrameCounter"); + m_config->updateOption("showResetInfo"); m_controller->start(); if (!m_pendingState.isEmpty()) { diff --git a/src/platform/test/CMakeLists.txt b/src/platform/test/CMakeLists.txt index 2517e83bf..5e20cb41e 100644 --- a/src/platform/test/CMakeLists.txt +++ b/src/platform/test/CMakeLists.txt @@ -46,3 +46,10 @@ if(BUILD_CINEMA) add_test(cinema ${BINARY_NAME}-cinema -v) install(TARGETS ${BINARY_NAME}-cinema DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${BINARY_NAME}-test) endif() + +if(BUILD_ROM_TEST) + add_executable(${BINARY_NAME}-rom-test ${CMAKE_CURRENT_SOURCE_DIR}/rom-test-main.c) + target_link_libraries(${BINARY_NAME}-rom-test ${BINARY_NAME}) + target_compile_definitions(${BINARY_NAME}-rom-test PRIVATE "${OS_DEFINES};${FEATURE_DEFINES};${FUNCTION_DEFINES}") + install(TARGETS ${BINARY_NAME}-rom-test DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${BINARY_NAME}-test) +endif() diff --git a/src/platform/test/rom-test-main.c b/src/platform/test/rom-test-main.c new file mode 100644 index 000000000..d015f40c0 --- /dev/null +++ b/src/platform/test/rom-test-main.c @@ -0,0 +1,220 @@ +/* Copyright (c) 2013-2022 Jeffrey Pfau +* Copyright (c) 2022 Felix Jones +* +* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define ROM_TEST_OPTIONS "S:R:" +#define ROM_TEST_USAGE \ + "\nAdditional options:\n" \ + " -S SWI Run until specified SWI call before exiting\n" \ + " -R REGISTER General purpose register to return as exit code\n" \ + +struct RomTestOpts { + int exitSwiImmediate; + unsigned int returnCodeRegister; +}; + +static void _romTestShutdown(int signal); +static bool _parseRomTestOpts(struct mSubParser* parser, int option, const char* arg); +static bool _parseSwi(const char* regStr, int* oSwi); +static bool _parseNamedRegister(const char* regStr, unsigned int* oRegister); + +static bool _dispatchExiting = false; +static int _exitCode = 0; + +#ifdef M_CORE_GBA +static void _romTestSwi3Callback(void* context); + +static void _romTestSwi16(struct ARMCore* cpu, int immediate); +static void _romTestSwi32(struct ARMCore* cpu, int immediate); + +static int _exitSwiImmediate; +static unsigned int _returnCodeRegister; + +void (*_armSwi16)(struct ARMCore* cpu, int immediate); +void (*_armSwi32)(struct ARMCore* cpu, int immediate); +#endif + +int main(int argc, char * argv[]) { + signal(SIGINT, _romTestShutdown); + + struct RomTestOpts romTestOpts = { 3, 0 }; + struct mSubParser subparser = { + .usage = ROM_TEST_USAGE, + .parse = _parseRomTestOpts, + .extraOptions = ROM_TEST_OPTIONS, + .opts = &romTestOpts + }; + + struct mArguments args; + bool parsed = parseArguments(&args, argc, argv, &subparser); + if (!args.fname) { + parsed = false; + } + if (!parsed || args.showHelp) { + usage(argv[0], ROM_TEST_USAGE); + return !parsed; + } + if (args.showVersion) { + version(argv[0]); + return 0; + } + struct mCore* core = mCoreFind(args.fname); + if (!core) { + return 1; + } + core->init(core); + mCoreInitConfig(core, "romTest"); + applyArguments(&args, NULL, &core->config); + + mCoreConfigSetDefaultValue(&core->config, "idleOptimization", "remove"); + +#ifdef M_CORE_GBA + if (core->platform(core) == mPLATFORM_GBA) { + ((struct GBA*) core->board)->hardCrash = false; + + _exitSwiImmediate = romTestOpts.exitSwiImmediate; + _returnCodeRegister = romTestOpts.returnCodeRegister; + + if (_exitSwiImmediate == 3) { + // Hook into SWI 3 (shutdown) + struct mCoreCallbacks callbacks = {0}; + callbacks.context = core; + callbacks.shutdown = _romTestSwi3Callback; + core->addCoreCallbacks(core, &callbacks); + } else { + // Custom SWI hooks + _armSwi16 = ((struct GBA*) core->board)->cpu->irqh.swi16; + ((struct GBA*) core->board)->cpu->irqh.swi16 = _romTestSwi16; + _armSwi32 = ((struct GBA*) core->board)->cpu->irqh.swi32; + ((struct GBA*) core->board)->cpu->irqh.swi32 = _romTestSwi32; + } + } +#endif + + bool cleanExit = true; + if (!mCoreLoadFile(core, args.fname)) { + cleanExit = false; + goto loadError; + } + if (args.patch) { + core->loadPatch(core, VFileOpen(args.patch, O_RDONLY)); + } + + struct VFile* savestate = NULL; + + if (args.savestate) { + savestate = VFileOpen(args.savestate, O_RDONLY); + } + + core->reset(core); + + struct mCheatDevice* device; + if (args.cheatsFile && (device = core->cheatDevice(core))) { + struct VFile* vf = VFileOpen(args.cheatsFile, O_RDONLY); + if (vf) { + mCheatDeviceClear(device); + mCheatParseFile(device, vf); + vf->close(vf); + } + } + + if (savestate) { + mCoreLoadStateNamed(core, savestate, 0); + savestate->close(savestate); + } + + do { + core->runLoop(core); + } while (!_dispatchExiting); + + core->unloadROM(core); + +loadError: + freeArguments(&args); + mCoreConfigDeinit(&core->config); + core->deinit(core); + + return cleanExit ? _exitCode : 1; +} + +static void _romTestShutdown(int signal) { + UNUSED(signal); + _dispatchExiting = true; +} + +#ifdef M_CORE_GBA +static void _romTestSwi3Callback(void* context) { + struct mCore* core = context; + _exitCode = ((struct GBA*) core->board)->cpu->regs.gprs[_returnCodeRegister]; + _dispatchExiting = true; +} + +static void _romTestSwi16(struct ARMCore* cpu, int immediate) { + if (immediate == _exitSwiImmediate) { + _exitCode = cpu->regs.gprs[_returnCodeRegister]; + _dispatchExiting = true; + return; + } + _armSwi16(cpu, immediate); +} + +static void _romTestSwi32(struct ARMCore* cpu, int immediate) { + if (immediate == _exitSwiImmediate) { + _exitCode = cpu->regs.gprs[_returnCodeRegister]; + _dispatchExiting = true; + return; + } + _armSwi32(cpu, immediate); +} +#endif + +static bool _parseRomTestOpts(struct mSubParser* parser, int option, const char* arg) { + struct RomTestOpts* opts = parser->opts; + errno = 0; + switch (option) { + case 'S': + return _parseSwi(arg, &opts->exitSwiImmediate); + case 'R': + return _parseNamedRegister(arg, &opts->returnCodeRegister); + default: + return false; + } +} + +static bool _parseSwi(const char* swiStr, int* oSwi) { + char* parseEnd; + long swi = strtol(swiStr, &parseEnd, 0); + if (errno || swi > UINT8_MAX || *parseEnd) { + return false; + } + *oSwi = swi; + return true; +} + +static bool _parseNamedRegister(const char* regStr, unsigned int* oRegister) { + if (regStr[0] == 'r' || regStr[0] == 'R') { + ++regStr; + } + + char* parseEnd; + unsigned long regId = strtoul(regStr, &parseEnd, 10); + if (errno || regId > 15 || *parseEnd) { + return false; + } + *oRegister = regId; + return true; +}