From 9ce234daaca53093448c1b133c9e291385ee229e Mon Sep 17 00:00:00 2001 From: Lothar Serra Mari Date: Thu, 30 May 2019 09:44:16 +0200 Subject: [PATCH 01/50] Qt: Update German GUI translation --- src/platform/qt/ts/mgba-de.ts | 442 +++++++++++++++++----------------- 1 file changed, 226 insertions(+), 216 deletions(-) diff --git a/src/platform/qt/ts/mgba-de.ts b/src/platform/qt/ts/mgba-de.ts index b4a54aa7a..28f090df8 100644 --- a/src/platform/qt/ts/mgba-de.ts +++ b/src/platform/qt/ts/mgba-de.ts @@ -1289,22 +1289,22 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. QGBA::CoreController - + Failed to open save file: %1 Fehler beim Öffnen der Speicherdatei: %1 - + Failed to open game file: %1 Fehler beim Öffnen der Spieldatei: %1 - + Failed to open snapshot file for reading: %1 Konnte Snapshot-Datei %1 nicht zum Lesen öffnen - + Failed to open snapshot file for writing: %1 Konnte Snapshot-Datei %1 nicht zum Schreiben öffnen @@ -3381,12 +3381,12 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd.Video-Logs (*.mvl) - + Crash Absturz - + The game has crashed with the following error: %1 @@ -3395,428 +3395,433 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. - + Couldn't Load Konnte nicht geladen werden - + Could not load game. Are you sure it's in the correct format? Konnte das Spiel nicht laden. Sind Sie sicher, dass es im korrekten Format vorliegt? - + Unimplemented BIOS call Nicht implementierter BIOS-Aufruf - + This game uses a BIOS call that is not implemented. Please use the official BIOS for best experience. Dieses Spiel verwendet einen BIOS-Aufruf, der nicht implementiert ist. Bitte verwenden Sie für die beste Spielerfahrung das offizielle BIOS. - + Really make portable? Portablen Modus wirklich aktivieren? - + This will make the emulator load its configuration from the same directory as the executable. Do you want to continue? Diese Einstellung wird den Emulator so konfigurieren, dass er seine Konfiguration aus dem gleichen Verzeichnis wie die Programmdatei lädt. Möchten Sie fortfahren? - + Restart needed Neustart benötigt - + Some changes will not take effect until the emulator is restarted. Einige Änderungen werden erst übernommen, wenn der Emulator neu gestartet wurde. - + - Player %1 of %2 - Spieler %1 von %2 - + %1 - %2 %1 - %2 - + %1 - %2 - %3 %1 - %2 - %3 - + %1 - %2 (%3 fps) - %4 %1 - %2 (%3 Bilder/Sekunde) - %4 - + &File &Datei - + Load &ROM... &ROM laden... - + Load ROM in archive... ROM aus Archiv laden... - + Load alternate save... Alternative Speicherdatei laden... - + Load temporary save... Temporäre Speicherdatei laden... - + Load &patch... &Patch laden... - + Boot BIOS BIOS booten - + Replace ROM... ROM ersetzen... - + ROM &info... ROM-&Informationen... - + Recent Zuletzt verwendet - + Make portable Portablen Modus aktivieren - + &Load state Savestate (aktueller Zustand) &laden - + Load state file... Ssavestate-Datei laden... - + &Save state Savestate (aktueller Zustand) &speichern - + Save state file... Savestate-Datei speichern... - + Quick load Schnell laden - + Quick save Schnell speichern - + Load recent Lade zuletzt gespeicherten Savestate - + Save recent Speichere aktuellen Zustand - + Undo load state Laden des Savestate rückgängig machen - + Undo save state Speichern des Savestate rückgängig machen - - + + State &%1 Savestate &%1 - + Load camera image... Lade Kamerabild... - + Import GameShark Save Importiere GameShark-Speicherstand - + Export GameShark Save Exportiere GameShark-Speicherstand - + New multiplayer window Neues Multiplayer-Fenster - + E&xit &Beenden - + &Emulation &Emulation - + &Reset Zu&rücksetzen - + Sh&utdown Schli&eßen - + Yank game pak Spielmodul herausziehen - + &Pause &Pause - + &Next frame &Nächstes Bild - + Fast forward (held) Schneller Vorlauf (gehalten) - + &Fast forward Schneller &Vorlauf - + Fast forward speed Vorlauf-Geschwindigkeit - + Unbounded Unbegrenzt - + %0x %0x - + Rewind (held) Zurückspulen (gehalten) - + Re&wind Zur&ückspulen - + Step backwards Schrittweiser Rücklauf - + Sync to &video Mit &Video synchronisieren - + Sync to &audio Mit &Audio synchronisieren - + Solar sensor Solar-Sensor - + Increase solar level Sonnen-Level erhöhen - + Decrease solar level Sonnen-Level verringern - + Brightest solar level Hellster Sonnen-Level - + Darkest solar level Dunkelster Sonnen-Level - + Brightness %1 Helligkeit %1 - + BattleChip Gate... BattleChip Gate... - + Audio/&Video Audio/&Video - + Frame size Bildgröße - + Toggle fullscreen Vollbildmodus umschalten - + Lock aspect ratio Seitenverhältnis korrigieren - + Force integer scaling Pixelgenaue Skalierung (Integer scaling) - + + Interframe blending + Interframe-Überblendung + + + Frame&skip Frame&skip - + Mute Stummschalten - + FPS target Bildwiederholrate - + Take &screenshot &Screenshot erstellen - + F12 F12 - + Record GIF... GIF aufzeichen... - + Game Boy Printer... Game Boy Printer... - + Video layers Video-Ebenen - + Audio channels Audio-Kanäle - + Adjust layer placement... Lage der Bildebenen anpassen... - + &Tools &Werkzeuge - + View &logs... &Logs ansehen... - + Game &overrides... Spiel-&Überschreibungen... - + Game &Pak sensors... Game &Pak-Sensoren... - + &Cheats... &Cheats... - + Open debugger console... Debugger-Konsole öffnen... - + Start &GDB server... &GDB-Server starten... - + Settings... Einstellungen... @@ -3826,142 +3831,142 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd.Ordner auswählen - + Add folder to library... Ordner zur Bibliothek hinzufügen... - + About... Über... - + %1× %1x - + Bilinear filtering Bilineare Filterung - + Native (59.7275) Nativ (59.7275) - + Record A/V... Audio/Video aufzeichnen... - + View &palette... &Palette betrachten... - + View &sprites... &Sprites betrachten... - + View &tiles... &Tiles betrachten... - + View &map... &Map betrachten... - + View memory... Speicher betrachten... - + Search memory... Speicher durchsuchen... - + View &I/O registers... &I/O-Register betrachten... - + Record debug video log... Video-Protokoll aufzeichnen... - + Stop debug video log Aufzeichnen des Video-Protokolls beenden - + Exit fullscreen Vollbildmodus beenden - + GameShark Button (held) GameShark-Taste (gehalten) - + Autofire Autofeuer - + Autofire A Autofeuer A - + Autofire B Autofeuer B - + Autofire L Autofeuer L - + Autofire R Autofeuer R - + Autofire Start Autofeuer Start - + Autofire Select Autofeuer Select - + Autofire Up Autofeuer nach oben - + Autofire Right Autofeuer rechts - + Autofire Down Autofeuer nach unten - + Autofire Left Autofeuer links @@ -4274,7 +4279,7 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. - + frames Bild(er) @@ -4320,253 +4325,258 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd.Nativ (59.7275) - + + Interframe blending + Interframe-Überblendung + + + Language Sprache - + English Englisch - + List view Listenansicht - + Tree view Baumansicht - + Show FPS in title bar Bildwiederholrate in der Titelleiste anzeigen - + Automatically save cheats Cheats automatisch speichern - + Automatically load cheats Cheats automatisch laden - + Automatically save state Zustand (Savestate) automatisch speichern - + Automatically load state Zustand (Savestate) automatisch laden - + Enable Discord Rich Presence Discord-Integration aktivieren - + Video renderer: Video-Renderer: - + Software Software - + OpenGL OpenGL - + OpenGL enhancements OpenGL-Verbesserungen - + High-resolution scale: Hochauflösende Skalierung: - + XQ GBA audio (experimental) XQ GBA-Audio (experimentell) - + Cheats Cheats - + Log to file In Datei protokollieren - + Log to console Auf die Konsole protokollieren - + Select Log File Protokoll-Datei auswählen - + Camera: Kamera: - - - + + + Autodetect Automatisch erkennen - - - + + + Game Boy (DMG) Game Boy (DMG) - - - + + + Super Game Boy (SGB) Super Game Boy (SGB) - - - + + + Game Boy Color (CGB) Game Boy Color (CGB) - - - + + + Game Boy Advance (AGB) Game Boy Advance (AGB) - + Default BG colors: Standard-Hintergrundfarben: - + Default sprite colors 1: Standard-Sprite-Farben 1: - + Default sprite colors 2: Standard-Sprite-Farben 2: - + Use GBC colors in GB games Verwende GBC-Farben in GB-Spielen - + Super Game Boy borders Super Game Boy-Rahmen - + Game Boy model: Game Boy-Modell: - + Super Game Boy model: Super Game Boy-Modell: - + Game Boy Color model: Game Boy Color-Modell: - + Camera driver: Kamera-Treiber: - + Library: Bibliothek: - + Show when no game open Anzeigen, wenn kein Spiel geöffnet ist - + Clear cache Cache leeren - + Fast forward speed: Vorlauf-Geschwindigkeit: - + Preload entire ROM into memory ROM-Datei vollständig in Arbeitsspeicher vorladen - - - - - - - - - + + + + + + + + + Browse Durchsuchen - + Use BIOS file if found BIOS-Datei verwenden, wenn vorhanden - + Skip BIOS intro BIOS-Intro überspringen - - + + × × - + Unbounded unbegrenzt - + Suspend screensaver Bildschirmschoner deaktivieren @@ -4576,50 +4586,50 @@ wenn vorhanden BIOS - + Pause when inactive Pause, wenn inaktiv - + Run all Alle ausführen - + Remove known Bekannte entfernen - + Detect and remove Erkennen und entfernen - + Allow opposing input directions Gegensätzliche Eingaberichtungen erlauben - - + + Screenshot Screenshot - - + + Save data Speicherdaten - - + + Cheat codes Cheat-Codes - + Enable rewind Rücklauf aktivieren @@ -4629,76 +4639,76 @@ wenn vorhanden Bilineare Filterung - + Rewind history: Rücklauf-Verlauf: - + Idle loops: Leerlaufprozesse: - + Savestate extra data: Zusätzliche Savestate-Daten: - + Load extra data: Lade zusätzliche Daten: - + Autofire interval: Autofeuer-Intervall: - + GB BIOS file: Datei mit GB-BIOS: - + GBA BIOS file: Datei mit GBA-BIOS: - + GBC BIOS file: Datei mit GBC-BIOS: - + SGB BIOS file: Datei mit SGB-BIOS: - + Save games Spielstände - - - - - + + + + + Same directory as the ROM Verzeichnis der ROM-Datei - + Save states Savestates - + Screenshots Screenshots - + Patches Patches From 0cace151e1148cfd12d4175f816a044871d8e113 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 30 May 2019 11:59:07 -0700 Subject: [PATCH 02/50] GBA Video: Fix wrapped sprite mosaic clamping (fixes #1432) --- CHANGES | 1 + .../gba/obj/sma2-mosaic-clamp/baseline_0000.png | Bin 0 -> 5996 bytes .../gba/obj/sma2-mosaic-clamp/baseline_0001.png | Bin 0 -> 5950 bytes .../gba/obj/sma2-mosaic-clamp/baseline_0002.png | Bin 0 -> 6056 bytes .../gba/obj/sma2-mosaic-clamp/baseline_0003.png | Bin 0 -> 6061 bytes .../gba/obj/sma2-mosaic-clamp/baseline_0004.png | Bin 0 -> 5954 bytes .../gba/obj/sma2-mosaic-clamp/baseline_0005.png | Bin 0 -> 6010 bytes .../gba/obj/sma2-mosaic-clamp/baseline_0006.png | Bin 0 -> 6015 bytes .../gba/obj/sma2-mosaic-clamp/baseline_0007.png | Bin 0 -> 5987 bytes cinema/gba/obj/sma2-mosaic-clamp/test.mvl | Bin 0 -> 37919 bytes src/gba/renderers/video-software.c | 6 +++--- 11 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/baseline_0000.png create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/baseline_0001.png create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/baseline_0002.png create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/baseline_0003.png create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/baseline_0004.png create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/baseline_0005.png create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/baseline_0006.png create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/baseline_0007.png create mode 100644 cinema/gba/obj/sma2-mosaic-clamp/test.mvl diff --git a/CHANGES b/CHANGES index be5d78206..84dbb27fa 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,7 @@ Emulation fixes: - GB Video: Delay LYC STAT check (fixes mgba.io/i/1331) - GB Video: Fix window being enabled mid-scanline (fixes mgba.io/i/1328) - GB I/O: Filter IE top bits properly (fixes mgba.io/i/1329) + - GBA Video: Fix wrapped sprite mosaic clamping (fixes mgba.io/i/1432) Other fixes: - Qt: Fix some Qt display driver race conditions - Core: Improved lockstep driver reliability (Le Hoang Quyen) diff --git a/cinema/gba/obj/sma2-mosaic-clamp/baseline_0000.png b/cinema/gba/obj/sma2-mosaic-clamp/baseline_0000.png new file mode 100644 index 0000000000000000000000000000000000000000..d05b02e1689c81e153cfde56dc2beab12674eb92 GIT binary patch literal 5996 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 aP)mA|y4Dp*9dX4iGIi8}%O@U8 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 g)~om`Ud{Ob0|JX›SzyJUM07*qoM6N<$g7RX=z5oCK literal 0 HcmV?d00001 diff --git a/cinema/gba/obj/sma2-mosaic-clamp/baseline_0002.png b/cinema/gba/obj/sma2-mosaic-clamp/baseline_0002.png new file mode 100644 index 0000000000000000000000000000000000000000..92763de4a81591cf97032c56d590a25b383b6afa GIT binary patch literal 6056 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 literal 0 HcmV?d00001 diff --git a/cinema/gba/obj/sma2-mosaic-clamp/baseline_0004.png b/cinema/gba/obj/sma2-mosaic-clamp/baseline_0004.png new file mode 100644 index 0000000000000000000000000000000000000000..11cb7a1f44e5d845d47b293caf38510083014b4d GIT binary patch literal 5954 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 literal 0 HcmV?d00001 diff --git a/cinema/gba/obj/sma2-mosaic-clamp/baseline_0005.png b/cinema/gba/obj/sma2-mosaic-clamp/baseline_0005.png new file mode 100644 index 0000000000000000000000000000000000000000..0796c9e35bee034acab52075ef8530bd580b2ab1 GIT binary patch literal 6010 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 literal 0 HcmV?d00001 diff --git a/cinema/gba/obj/sma2-mosaic-clamp/baseline_0006.png b/cinema/gba/obj/sma2-mosaic-clamp/baseline_0006.png new file mode 100644 index 0000000000000000000000000000000000000000..8172f46705f9d734c6498423cc8b264406c231dd GIT binary patch literal 6015 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 literal 0 HcmV?d00001 diff --git a/cinema/gba/obj/sma2-mosaic-clamp/baseline_0007.png b/cinema/gba/obj/sma2-mosaic-clamp/baseline_0007.png new file mode 100644 index 0000000000000000000000000000000000000000..3ccd126c3f591781080ee40cb5ddaabb8ff754f0 GIT binary patch literal 5987 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 literal 0 HcmV?d00001 diff --git a/cinema/gba/obj/sma2-mosaic-clamp/test.mvl b/cinema/gba/obj/sma2-mosaic-clamp/test.mvl new file mode 100644 index 0000000000000000000000000000000000000000..9fece4fdb3267c352bcc7e0767841aaef1fe9a92 GIT binary patch literal 37919 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%0D3Q)vAOh3I)f z!v0?YN4FY&f|5#-U-mFx3u)&wgm-00Zdug44r=2qVO5cF@76JUEo_->|6gqp-ai%> z)_1O~@CQa_D~+?&F=$=eys4I=m3jL`SVF~AVa5`PR{e1>;$pq%fBLK8v^7OIsdjg% zc1>-B#gWZYZWrmQs8aAKIuX8K?EMjDI6yy~W@S$g01kl>G0%pB;Cj}JX%F!ATc_7^{p04zsb zVQIi1K=2ac94ghJ#tL*L^zRi&+$0sUZ3u!Tq&!C)De`b9G7}mR#^Tx(5erqAM|fiy z@(npp0D9~E!s4wJvVjn_-*3EHc8+7BR6W=~S&Vb81ZE}|O0~+jNN~yww7x_Q>C1db zMS2wW@mbnqirkk1G+r8IQ_>oE4%sMj`r7T*3h)AjBtK|Nh~j7}z#9*R#rqG}>VMO% zQvTV@hA$AsB^{QPC?d)$koo^F_TD-wjv(q24UnKgg9RHTxCM6z5FkKscM0yn2X_cA z!QI{6WpHvUIj)xEc>Zg>6ey<@OgpAYmPNA7Pf z@WgfUKg73VlIsJ0k{GRySRs|bKkyPr!#4P&wq)s_6cz6w@UpIwI?ZK5`S9z%lbmx z{5fMUXwR2dD!aFr(%BnPPr=Ko6opga`KE)|QGVej3{$1Vkf4DNHPh$awc=_F2!0OD zZCH}ptucg6_B|*v%QbR`wi)o3o}@>CzF{gM&itbl7W>`x~Wnt?1dW^?1>b0CkwZ6ofdXx2oaM$q|`VuZfLXjR!$IEG!7iw1eq6eqT=v+8H z_x)Ix6!}@fscj6WkmDHtpkL&vdFp1opZy)*5aLD_1u2(EXO>ZjyL1yGK>SDcKXUV; zl&e}y_#2qgq1Irm7f-4?1n>>e#qdQu^?RM z#VW|rsuWr(JCmFF8I|Hh`xd+?mpE^#-U~~LAyhCA(YLPRiP2?THf}P*RE8p!$g*+x zfa2K;-6y6_d;)N?rB-PsgR1rYDoOjav!utY&mhfDXbI|H6b$UIUgh*1W^AT@(zHrU z(SKj#R=XlhickGAp7bR<$tA=M`D*w?%!w|eM!)l!8NTf18bIS@h7=@~< zgUeKA7bf30(XJ~zIPIT&dPmjK7UG5UCR^z>-T7iuX--n$lIfz@xH3mf%+NtO#rN+u zCNEX>_IK*6wnqg6$aT=a=Vn}8FW9hA%XQZ|zp0U#_L}7ySeO*AnzM8PmK|42)Cj5>lx+l?8W%vbnnxJycGQouMye(b1oSN z*f>8lVbfVZ8f2HIBaezLPI?>f9CE5&)Z8X%f){Fuxz>@F5z+W;chG#mvh@oih|SahmKOo6Q5=2 z@QA`&*iXH)$l~Ii02^|yIF^;+cv;fFW5c`O*5$7O_X(E>+0F$L-V_0K8B+OBoT|7? zmX*&9F^H9Qv1e|au*ZUOAx64ZP6O0WQ^{b;lrq?~tmG;Odhr*9cptk(h*9&ihjVC! z3K{iZojk7hbZZS|c{XN0)!vzSr7dn3&pK8}HHbPz9{Y-0ybNdbr5IgHMBFAW=)Yi9 z!3t{^hf>DgsH(ON9a`1JqAEzeD_^qRDfS~a>}7h!k$<2B*ZS|9im8iZNSH(sPPa4_ zW<3F^KTdyOyFg`R)ud0%B@40M2G(B>FgD(QLEo^Xv-y=5bn}D_`_}U4REWLZWUF3x zL*DM3DyY%=Cm^VpnNFK(*kTZQ!+xJsTo~m1>(bhJ!G(Cklwh`UM&*{Fog7Zw&w_GH z!;WCojBX8iEW~m`KNEUB$PLC0*?R{>;ZVdafi+l9v;f&M-Etl>ElXcWkH|mUlxELM=5`Nm%&|7?9R8_p5_4yNbBdz0?MkBax4a0hLum34mrlLpY-O%~`rqMUG(oj!u>i$Ed-1QyX z;p@{>-3vFFu{jSn@}$%QyYSuGaj3$@!7FTKteE?OAJ7@dwfImQDi1z`LVHLj4lgER zwtf$hf*mg%NNAyN{tDYi?b$t4XQj%6Lc5!8n^weaOsTi(1#|-1qh1 z?{Iz^bB61K$%5?lZ#`RgT)bh~SxeeG&x7mJlQ--lHegsc1+nxXB9xo|T=a^7xMjY! zF>*g@bB$Z6J$z|}nD@2R=Za#=MwfbdP%`k^x_@sEtR)hj9ZIz8LZ0Y#v}LaF1g_~% z(%1B+a_-%?C%-P0*ui|-Jrn<&^XdjRg{sT!o`JcR@gEPn2J_L+dIwK3HkOl}$kxsI zJedEQIeR?7W(L<*RwO4|Fmtf~A@MY=Pwz3}uiW68rFu3C{g(xDj59j1z5c#?gDB1F z#6At58RhemCSu}B-HjDGSH30T6+cW#-dW%0{~YtW)oWB7c6&Kgu6}@tZ<+@_z}&oG zFg35GeO9b*WKd&-Uc>VL%<_Aj4;NrQ+&Moz` zn<|Me-~-<=@x!dk(TeO7Ku|W2YK6y_cMuG>xFvM&=?kq-sIjg<0&LI~9W2n z6O~ed-(tE}A7EVGRT{Ck6uc!qxlfSuqm82~-^l^Yg;kgEr&sf>x0gvgm0wEFEf&Bl zkLgg08xosvC(LQb7yS-)?1zppI$;P3N%gC5q~aa50B@(#E-zZ-^K);K1g zviCV&e@`=a8&L3WHsfH~QXT}TxyM2dNABffY-}EdcjD5wx7G`?Y46)f-RvI)_LqN0 zOah)LA%GdkRQo2Db4f@@zhqb_QEpe*X=qY3^4)tqam?3CJ2yB})2I1o=F*71 zEGOwWQ0xbDpE2JpZMI>7rIu`dcWa1#e6NBijmU?UCtUZ-bJ@sDxNzS4tOTa%)CUfh z0|h}|vP-^Ta9eC%7qriatK*X)D^To}cY4fd=c%war_wq<2#Z3gZ zW8YHW8{WWpi$i0n<#4-+*;l@A&5Pm99hkRI-uA)ADk#Dz&t5pUt;DyF zf-}f;`lI_}q;Q?jSL1g}uuHZ_sv8s0iHCl;7*5N-s|| z)r%o2P*3h#7nnAOspJQDl*4AHYH5f$`N-71bTMvwH1U1HdWG%&&hqT*wGXHeW*UaW7?VNV;6);atP5{q8pAdFq%3#%7AK`GZSI z1Pj4=v{C%8+T5Zf+c9{Mp~@Z&E`xVdE%8Vz99znazsCH#m316Ri-(6EmzU@H0qI2P z9!rGTl%GG6GdXbzBM^LlU6%dgNoDUUbnQbE-%OI9UEt8(@khktK<)`DMBSx|moqe) z7Y~dtPGT{<9OZ#GBzt0Q)qoiLc)x8Zc+(*ZlhMUSdh)~QZDE@wTXg??f@JQ)y!J#7 ztA+Oh770BYClj5QZh^CC?2TI`Y5~TZzXywW0Xnbe?U+~eP>Kw%<7wKbQR>r;9xoHJ zo$kF=X)HQ&%39hJ(l%=CaPf$l#!NAwZ-(D_VC!f{%=rfW{@Bu==jEq)Da@;cULNwj z?vj6qd_9~evjWV?_P5yIVZH_#u*2p9l`)Gjz2Q?TC~?b10oOO3Zb6*N&5&7P0Z-&L z(Gei%7>PN;f$}Wd1#Tyy&qNVaAi!O=g}}L3P3m+XxMJyyz}^-NXSK zFApZC$Zr%hXtV6?JW#9bW^Wg|)uwMxh0QK2k zb@7fcqH5p~SET55%i#&UnRfwXpI4z!^=xViV1BUH(J{!RCK{@BIoqmVsq8lIer%N8 zggj^4cf{XFaS`F1i~f;9ITASBYN*c;nWcA%ao#Tax?7~EY>ygxlK$5fWlkh-{}CJS zGwP)ro4?%&MN|pv^px=Nk+0w^A5`kx=K|^5v*(z%$*b?pt5c?kceUiCIk>p>!skh# zn%dX=cvbpRSA>u2K%Oj%AunYq7rNL~Hb_2vJzjrR-H$O#TbnHN%zI4aQkzriX^1?n1cY(4#i$kAQ zfkz_TjR0Q%jtF+J`QVRka_}z6HMjz1-&OP!?2|`9ZKJ`y$zdwn=G$|S{aSjqcJpax zefq9FBm14L2#R*oAuUVPghFhFIApDck~|7RSNk9-av6PwR7kM@7Ngt&h;AQEzu$WZ z-E+aMzUj~Fu9d^i#fj&#i8{JK4BdOYXYblDYdC!V63Jb)x*`I5bcH^lVfeZVz+S;> zP)5!e!c>)<*KWKCU+R*HIgijWzU($%Z*RS|^H8ivB`B2o)KmK9?!j~LmZPjWt{y6R z)SVD?@nuUu_MvLOquT3If3Z^ktE)Q=vJ#A8_*7WjA=^o-{`0JQ=hlL`&+gu2G4B(j z-pwao1E{BvNbV~LiOXY_VsZl0N|Q*yo|t%TC}i$9#_qhppeY8N3=^+bvNO1GO$=$c z0|8;S&sr}BxiQcy48WV(6Kw1BekKvHc0VNEE%F@#e+{I_#?+_7Z;x*Dm51{F!zsJ@ zv;%D!ivQ)I&qoGfN8O#e+G~4)QV)8OQ-jv=FVV^2&0AUL2(8{FT(mP;>5tPq>(v|2 zQPq>eK$NqL^mjtdSd*I`e=^byb=v8KE(wNRavg2yN)sJ3p7!ohYbp$-$@1^?Za+S=e)D`IGt%2}^Y;+ag)K~9-FXZqKl;%v zc`HQ^4ZTz|Lfy{p@K1+>cU`vTu8=OT-!Rwj;6$j9T~f)EL?jfSmd=>TV_H~RmGh$$ zFdJ4`HweY6;$%^s{-Ozw$_ZPg6=7?w&0SrXig(h!AddSEHeb80vdfH>==B`Gl{)%r zogP@ju=<+^U7l|-DeA4IUv^?)(79jUH$3{XcT`WwT%Nu~ors0ONEDJT#`(?SO=JP! zX|8svO{i-Xbi>J;)-YW>-nd3)BD*C9uq z&wZ~8T)tbU>8s>7{yq%=$K%I35n^H%GW4a5Zqmn6ve+&9vNT2?2C*Bd0x(}JnI{;*R6XKTH)Ngjc&s(jDE zC93rBB_|h+(#5z}H*g<6r8`^v^j{FPwW)uy5yubV(#cSWImkH0Nt=Mf>) zLzjr-frEj}g7eqiL|2VFj+@#q`It?6fKzv$IamDZyvK4%j14vH8O)od8{HV!XYW1> zU22zr+5~*4sIPDf+S_lR`z_2buuGpu@BBM(Oq=fuXc7jS-*dw)0#H1G6FqtWBFk5& zu<-`BIL;H#qg$A1p=f0{aoYS=Lyo3LB#H7U38z=Edx;UVI>CeEv{v;qk1V zII#9g+yx_i8jQex>x%G!;avNj^)Kb!ynKzjmEkt&=NE4b@s1f*CM|;uu z!oC^vN%2XY5b&v~rsh*F#&)0lbi*+Z_a+1WRwH_=(ydDUzNxI*gE_po- z5KMMvEOXS^gEu?>Bu`~}o+N7M15eg(ri5XA5fefn&xhT`Wc|;po;$(f6HZ$|vcmdY zqrC&LFf0??kwUoY)T@~HG|D&_{Bn<8pWJnGE+YKUr$2vOm~7&`B0J^%0?4*qc%W9> z()f3;p0xva@!=r3q)YsqWPvC5>}B>r)!X2kx53PvH{1u;c>?O1a)p(tmzF&d1}Zdxbv%OcDXyRd`kUu&3eilu} zHxE>8HuTw=5f2rZsd{|W{BTw=*gvT0ys{@X-CicK;OW6sM$IzF;30qsxI9%e{#A+J zNuc$DH((_iQ2bM&B7xJ9OZH$WTrIshNjD{p8{#+NxS@ zq1{upfnZ}jLb=UjdXsQv)2>0IQCq^z+}xaC)f%N~$-?A8sX;>`StsCPK>r6=m}V_K zHkQcBKHCOtw6d8J)UW8dhq!9}$HqP*YygU>cHD_u^IH%)ec_2J+tkfg9P=Ok)L8cSM2uVF>V+$NoKctuccg=m!;~W9blt^p)>7Z@~vs;#pn5MZ%NBKU(b+R`|6obX0TDv zX01O|E$_Km3JjR(@^!20hZ=Jijb=FZx3-0LNS3{}P(tDx24UqfZ6J;$k>unNE2yGAnHb)AGHy@Pyr> zd^n(f?3UZEe6sPu2TdinG&_EMjITZ&NleC{2B=?AhN^k2V0Q9!;98 zr-^EHVn5A=h=jmk+{)MKjslhB0boAQ$#_HL1aaTMeH1qve5RFt}( zYF-9Xso751?i*C2xZFlh>N31?WWgkT)h5Wb>+8amyM-Hkk^)p?$jMsBJRcdlNAchE z9V>qSBDp>4C|Wa3mG%;sMCRLrgwNs<9@t*@OZ~S zk-HCcHI-8VPt6lnc9t)=YmPK{dUP&lXS+gNli~f9oQyZ8A<3n`k>&yv_oxx-rK*;4xH;g4!hc%{Y|sq7#8u+boLBXr&t=% z?S5!`ZC4r_dt?Y>eKs=~6dbR%T4NlOE^BMtyDD46us*vW)p)Hlt!BE}i=}CfG04Xh zTa%j23PJm1RFRvs;gE}ULZ}cZQ{@?xA)mN6&92cR$)(8vrqj3&6XKQn`wy1J+aypQ z=_TV}!F8QFwUNDQvY!NOH`Ooyfx}{*yI7@OhPR3X4EQZDX|U(ecRykM)7Lu*IA>zN z&*yv%S<2@K6tUAJ@5-z8f5OoVjJxwt z39Z(6i^rc$BlKBMMg|XC8%~UVH=m^G}t3l!~!5<^QlU1>NqHIcRmr|7G!6TJG@3 zLLnPZI6sRZ&c~;pZ+8x*C`;5lIEw|G&X?8!@3^Pa>!G3u29HnWLp89IM6&^gDzMPGMFlAWdRj45UrY{0vHh^cLs1P}d%{FYS_&d_`Ad#VpVGT>i()DO2Mk$1uW zC@gtFH^8^Ws-v$q)$N__;Pd8^EPjC{|C4t4VWZ@4)bWrfclphscX$nWjplG9Ta{?e z=jYM;p-#dnjOcR0Db#2kwye)2OJB3dNSck-@k!i_1^IZZzN|b@2aWc+>st8yGj9HH z^h+0ILW&qb(7pZ=d6B!MwkAg^w*J4e zuEo7n3J+!AhiZdMD*6Ovs}Avk*=(u47IGr$x>nD|vh%o1dP@>3Ge`>5ovIvpP2_Sx z$4qam(6#QQ@vc%F+iXm=#Z|T$^*&F&ASgJ$TxG0rfGosIHdBxPvhzy2POi@+Mv&zB zs1_cQs;%gz%~kPbmH?g85nSj?tP7MhijGja3=%&KZOo^LDheKtYloc|AT$7=xS}yC&`M!DAjaGU{VAmA!8x5<4fsm-Cofwc`zvjW%OV@NK!%Rvy zpVwUXrT-?4ROV*lxG7yG^CFuuTP0fgb!=>*6#BYUF=-aF1$KTO8V9J9ni4$g=snOi zosT+BbkjTNWSdSE_4qz4BuAJr9f%BpEnS~E7^Da**_W(UwuwE`GA7lCWR&VQs(CLa z1{Y_t1f@nAKko_I{z)WS+}pgj|8RNM{Q{`l;K}bCSa#F0$2aD9mcM`7NTFRiSW;Z! ze@l%z*kbW*5H#$kg_uZ4NEB-L@G6!xYylpPo2&Cy5{uS%9gL$3FQ7fkWIN8><0;@w z&uI#4mDTUtXWlYC%AhASvXNOLyqHBuX#|Fe%RY4Rl+|4Z*W4b}`A!~W{ezP~>H;Vu zfi}DU+U!BAj{EsaCCXMP_kx5qC|vTd6W=IieHjG&o@^&Qc!7SZMniaHo~m z1<6}vYT!^e@sNO9)m1Dde_-{HQa2$swUWGCZqm*F*?mSn z49qGi*_2kqW79CCedRiLC}RW5Qy9M{;Hlli5FAs#P_Q#dGPTg7F_+>62eYCiFS-Lx zHFPO?3MmIPdGzFuR_hKtA}yB~cB6*ZHQaxD>IRqig4CE_B@2|-62B=M0BdaP&QF%z z2L7#_wh|uNMuM~U=W6gOFEmp7GYoqKocT##Y@U7-Tzrf1NS{B^1mnu)Hp!y2ybKYp zK{aOdTUlhbssF~!BrjsCVE?kCI;PSwVv>mgN1P!imcP#u7oIuFGBCVkt@YUjCwflY zS)UFk+D^6WHW1l%m)P2vP6kNT@1IF96FJT3X#HZK;U&3y5zK4NS;?k8O0zudcr`N` zua_5yrT!}7JxMB`8##niO~KW*a#r)C!l+Walvqmdq+F}UfBJw~(AmjT7vsQG3q?I^ zMqr6YlKc6~&|5SiTNvex<)f0MY1w(US-JtUd}=$$D4Kd?lVhkTSUEukQMtIul@PYv z%W#IaaD|kj9!C~gHQQ9{;MHy`FH>lnGe7^~T5x;DA5(|xcY+G?)>v1vQ;^EBaZ0uL zZJ&v6{10~f{ySztpz*jb1NI+oND~vywTvIsmcp@bNgVfyYk2#?WD+M(c!wil{7j^m zLS1R!&~7g$jR*;vI>45`n07@Ha>l4#&nDQ6I@jGht-dog2$@rhUPLF|6IK)7OO|J}3cu9$b zWvkP-$2$v6Qk}bG6jdO_PIHTA$F)!TXF{~Bp&(!Iw7qqu484g+#)&wP>NC%lKaLpk zlY`PatTr&hN1MQ$v|=w`X-LnVU^xf5`>8yj{cp&&=RnEWp>+6yRY!8ayI2WEI*(`x z^Vz_NaJC(uBq@SQ?iH8Jd}!jLVx<#R*%VA7_u+dXcr~lcd-s?8@QGke_Tlr|?HR7b z7o(*H8EKqDSUN0x7w>F71ZtE{Y^}7n^@~(zUKAX4cdYR^^3v0R!2|AtkQ6IuvRefL zs2O%LvpNtTgSQ?2;LHp?9opwo9c9xojOIKJo8P^cMSokAyN$jxt6hBzRf{KPRanFc z5?Q3z-rH{wnJjHUB#3pIz%)8o_XRHj^>0vRmpJ+E!;STsN!P9FH9{vv@K6UVD13<@ zuS`%Y!c)p{BuiK@Y+38NF-~`EcXF=IM4rTD2G(&U)cO~{6ekBY)kC}%*0F@G$K#fU z$`c`j)Wru;S`rZq;NKK~K5PDr3|noU&$u#BoE26?n^-DW>hI4jErV!hF8U5>%x~7F zgIZc@W%4%*V{nykK>DQy?gL-gFrA__8gRnLQ-k@BExw4H922J8A@4r+u+X(7aG|f}An+Se-1eMIqcIxd>Ap|? z@0RjV6loAea_$p5_MgioQ14-AL1yv4tAXPd6R`<7+mT#`=K)nZ5ZeR`7!t345ZHzz z^wU$k&GWGc#5fKLo<=AZXRYv4bHLVxKQltG3G!L~xCDx-`Cu2Kv%|fN>ej>Eg67(T z@Rfr`f*1{L`wK7Lq~C!9Tt%=(AAbu;RfzQtzdP;vJ3tU7h#5lv$mE3~jOo_5;UMBg z?Aqr;NSvNm*z=PYAHPT1fguBKYzJNwtvblXfln8Gsm}!>-vTF$3<%NLR)J_el6a9_ z2DR__JaV50y?y1zxC8yaDyS~YMwaTp#N?N-t;&m#1o}{eua9!J?bt$86@0dx>44yc zCH%3ur+B*_!uKe+j^YM-gV-*kycpRCT7~{!7euVta2A~jwZ;Dr_UvdER0q2Cp+iI; zzple)2Tb)H8B$z(Y&#zZQMw)k&cL;qp_e@p*&?K#s*|CQ5I`K+%0jmYVKguVYAQWfV5{|~YG z-)pew(-3W^;WBHm{%87xdK$`o>?f~@Eabp{+lQ+8fuNVVhK##M@;G$DL1-OWXWOg= z&n~di0m&w?-7u8NoOAR)q|}PxH1vN;4*xZ$kGWcb?9K!X;zm5rm!|Vx!pg+=hp+P@&p~F5PKu8~11m9zV zsA@iu1?xcw&qIW22=04*O2+OQiY!o7b3bSseT)qLtr&Rq6^^FwOUg$TLxu)XfuDgV zUlF4HQXRfH_b~CIMu)68d?Z10%K3CCMlc@uG>wk7&8vw#1ghWu=U+MtownC02MNE| zSQDNWME`H(AQpq1c7PD0b3hjo4@hb|9|GHc*!ksv_6oO->Wd5r6$w!2q1{n|Shk=; z38C+yxWWj&h}pqwMvSj~uOD8iU-4eKyfC1Mmww&B-GSYqzJ9(zQy`IE?qv+vM;N?; zpKq|h&ek@|Bc&G#l(g%kfq(lB*)rTipEpG9k;semG8E>Av@QCG==G`V0}N#=1Qr1I zh~Y&*77)6Fy^Ipm$Kw9c6-2g-_F^b9pZlYq`-7T=zd9kpFC&t9L&2YZf4-v7fN)b# zZKpBj4Ee|Xs0?XHP)NU`MF->?ag!hn^^qJ0wN8JyF%)(N!SiC^_gz7lc;V!O%t6u+ zl4E~~8bTk`wTUMPJTJmzkUp=ho&AsOkHtNu4n!FkQ^7|graMnHxWKU-%*l`gh?piE z;|>Wg!DQgg4!k>dMF0WFss&3Q-rA741D7QrJ0!VxYzGg*0g-7TWbw}qmfcaSfg5_k zbD-A!P#tIxRKC;r1IGFYY$5RxJq%^smT5uNN9sn_0CCk|>tlAmxA)`eV+A$Vpgix$ zLChZURzi;>K0DCd_G#7G8-Bj+U8y0_M`kpn&OkWo13C!nzGDqggBWX~6ZFX%qE`eb z_vt8ZRXUKqGT4L|1UC1=LYhGW+nErS7P3uru)jg*L!W&QIwakJssqypZ4(pVm%bec zp;(6RMu#E!AVFDQtJaTtz`YX?>_@s+kw@=G!gYAxcg=l(zM-9%?N1QtM_ex=yFif; z5r2{3sh;d!^Bvb^R9_5VIA0Xs55DNxf!+QB+x9!)ZSW3Y8?bW&QK&(00*QdSdkD4- zcH|#{*s?(hDgNP2j7bRgLf+ecPH zKX8xR4uv}*nSUmP?mS?~kmuaL^PhJLaZ=QdE#++li#eJ{axdgTWBvadaWIa&@hi#Z z4ynRB_wG&xle2Q%`EcvIt>PYR z6ZdjoP{wxNmijH(Va?^^xlE_UE5BDmP!1onX;9=?2pIltw2PSHX?u*1_|B8Ki->qZ z!^mMvPLdrp>*1XtTKseaoOZcr$edX+awV(i3T>)%ho+=RtMKqiET5miEOj+I^3R0jsEa^kie#KI!;Qk7FA#}RzhTML_E1sT$k$4P@n0FGVvJ4 zZF0Bp{c);W7=pfHC=3QiyXV9@Li}oXb_8Un*zeqRZq=35h-uI;sQ1`+pju+VC4ZKVscoHtNW`Q&J-r>8f}S?|?YGBif!I_jzMEJRKFbT?Be9cUYrsc>8?HJ&(U1tB9y2_i zNlOammr?wDUXZ^l68dB^Thi(UBjV>Y1Ka*qmUAvFI5)8sH_41M%F5qV(q*HzAg;0G z7{w#jU`B72rO{VBYxSbaQk_;ut&Pz_7cRy@y4{peZ3MGrg9! zz`%!_3>Ply8p#~_&k(h8T}c2M+pG^|Ci@_=#WYS!-=C#?CoMg=CS(`#>Cg3EpYQXW zosY}f3}3%D>hM}uX?ashQEt=X>{JQGAvb$aBJBc9EdW^IP7S%# z0)=e!(&D07SXpik2wuD_2fDmGH4RorUx|2j^*d+7x~B+cgDs0QG|pWA$;D}v^El0o53wLNhXtH?KB+=cp$vTDR3tn&vwP)DL1D~n91W!N24&=y(47ZW9q&3)Yz zf6cX8xPs*2&TpC=o1gC+v_9u9)zy+3rVmRvMG^@m;l#|7@SsOY{c|NCYN2o8iTt4B z&hNY(n;zSG88-@yv#g>M2L@P9(ji^HFV<5Exqd&Fp@flazI>pd4U=B={TD`V;*Av( z{i#scz_N=KzV+cI5#wi)q~O?V7sX%6eGJpDSqiRDr?$B(Y$h5D2!)vG6hyUhNC>o(QJHRu?mD5VeYIl%HQ;i-2vR{~t zWmiBG8euJK&1h20*<)@UOMY?ObmFR6+t*Lx2K0kY``4huKV>5P3jNohZWi$|z)>Ty zw*GK_ct=fCeMDWGW5n-#c|OuM1`?T2Q;ip<+~3&11>Te)r@Q@J%Q)aD6R23?5ZIkj z@~UFFCA~!lGF~9TNJh*YsPex$Zi`v#o^;6CEvGFxT?K5W(Ko@!wAv;5ChuCv;Jm_-C% zW_*SAt`m~H(Txdo+{-Nt{%3iWAQND6&ynCuI}Qjqnc~~ai8cR#x1mDKb!xC#Sg13; z;(7zrHF9tbc@pfJw!wyH?0*y+YN?N`*qbGFq$#2$)QU4MP*D98w?Guw^Q*KzW91i4 zT){}suSy!jlrV{jo~#R*NCg>rk{DAHL*|6_KUevZF<;?R4@VnyQXcr%gJ!?hyH%-kJ{rIiQDR zYq0A_66_7Qv;B{pg4P(H92D~mW4`L|zkM+I;YC=@A?Pe#{I7EPoDj)~D}DsmP;s}I zAwC}oR*!hgf4^-svuB+|h93-qu8ceWJc)#v-^_u*X)vF2wNHPI&AV?}zAf z-@x2$@RS1ZgHxC0*PFgj^K*Jn6Wv`B(z%#O&z0ICHmCXB8IS0nzBV#N#-QE{jMLLolv(EO7SUg_2MJM!s%MQ{`UZen-MKO!XZ%vXcu!B|)6{Ah6 zy-kKz#$Qe9=WV}yMobl!glc5$Jq{34g)6snXDlY(d*5cozbwAqhBzfJRJ1uS>=gb= z=2{rN)#&iVe>>wSSHINtynG~JZRt3^j+Z-%uGiQ2668}_VgD7=l!dG`=I<|!=q;SY z=V>iZ8t0g0{;!?B4bTrxKfA3wSEivGA|5Y96#B!fr}|zO;tS$)&77IM!m!I*K-F{< zG@*80JEJfL zwJ>zox<~|epk^IY74;Kypt(R=9yW1>I+}2TCdEXQ{tKmf>L<@H*10LC$_DeO)l@Z8 zgZ4)^Y3UI&V6a5#LHTICaO2Btu5}3yX~%Zc2m8fIo!8|$p@@+Cm`%>h-BfCik$d`Q zwP&uiC3(gcUz;)Q6?=sUyb+x=%9mnGQK0%!E5)!XN>`&A zM_ahT8)KbN zr!HlKSg**$!xu_=^y1NvJLdP>Na6&{=Rpcz^)@l@Ei5!w9Lk^qrv@`MWjm^-4WTEn zho@l;XSPQ+?GYtMJkNQM5x9}`(@_71QKkb>#a7g_P&loy>I*Wp&Iz74K+AQ)C+`{M zKj)bJy>OgAT=6X}K|%W_>Czq^9%g1{Nz$&a(ftRGfqi`_pJ;x@-loZ_iLGY`C-g6_45iiBrnE>PvjIbe|fEO#we7L2PydACDh~<6~$l<<*2>B zy!4i1zCmTGg$(l4zws2S{yLkEsbQ2F-*jfBuC*2~WZHaA4X`^|yR`QxDlRs7IJ-RI z@dk;7DPgyw!5|AT!jNn@b!D%N0ekZ+dy^}Bcqqrf7JP?o$ML zeP2o5lXhcq_em;}zW=ThMeb16RgEaRwe|4!eu5ks$0YBTdCN^T;GHxLGAIOxw>RnG zITe@z_+IiJ_z1k9(k9npMD{{j`d)HJvB;~l(~T?R;W~J8t~Dh)#2}0O2VmcglXawN zW!HVs2BItuzf55&3G9vh&g-D3(OT?_@+u#(u}+extoFm38g!DT$QyH<xMM1OgQ6oKwW$}?fvu1|uLNZn= zsz&`^jJ>m{u|7i7TScrf1yvHGm%Bxe6u+J&gnm6eOswWcGWCP0*kix%lO%sIrY`_} zLF+fMzKD0C_t+-Rg5e#S_cRe%$@Crxb(pMk8MmavowM`T4;}AO*Nc9s4VAt)$$zNJ z{w_59bAoSTkTfBxtQy|8o4Tl;FRC_AHf6HSkzWAu2cLRBtkp==!HF9EWD>R3h z*VK6*$`-$3B>GYI&M8VL$mMAYY5fax`KS+6F3X_&7u0vIC1p~yo`Ion-Q+!~-j_Sx zpX~ByFjPv$-`R%N3mR&cZ`m!pHWKU#CVi>IKI}bjxwcx{Z*uD@8NBy3( z&f?uW7s`n9V*z>0_PHQB5(<=JQzjJ_zNGn3pVm8|GB6MNsf`z))+QrKS@ENJv;?+p zBudVgT6u_-KGsi8*38kkdU)#35P!aVJ+WBUs;xL-`s-WgK`fbbe)(NBZ;e*iS6ocE zC`s`KZDtRe-#Pl-cJ_^JC*SgGKmSf=C5Rc43rw(Rh!x&I zn0m(Y!baE$<=#8u@`0M=b1hWJ;i>5#SdC&@;T5Y}ZxSqGY|*Ay0!tU$q@DNO81A5w z`{g5Mywy|IyRX`KOKNe0^Uxf8hHm^ttG1W{k;Dn*Z!c*>@~;g7@vzs(^i)xUb4fa@ z?l51z|A^|#$4EAJ=TDdMO}j!fvRe8Z?XrhMu4=SFaok)RBa4qfR^2;S{1mYwzck8N zctw;7!m=2j;gGJzh1|iKw5mzYV9y#1^qs1#@@g0CmDN-cO+eLTOCPDv#hS<+Hp?C9 zo^sLwHJu{B--2#I>$b=d{p&GHlBrQ0(^$Rfu||-dH|ea*Gk`~UDzU{3Piq57X;nkO zw`KLp+yHtH@5iQx=wn8CB@HdXH74^%tce{eVcmLodiu=|k%*c-Jfc0BE?kTWgd*7( zXeVB<>%`5D36O zYVg~62G5w_^eP26;(ZW7JIgd)#8I1_=XYE(U)ABg9yc<)U~|d-nS1=16B}%Ezs9N! z{0n{X53pwYM%Rk1N~R3o`cJ`->|`=fDr*$-!|^ZcswDAz<>46$lx?`spnH`oWhMGx;8XAfSs38W`~4j06uv4tQ}b2Fp$k%@EW z@_K!6Vf=K4hoUf>GRtDDtnHwdyM4&nByEj<@mtsIPGX*Qo5j7%D2f|m&m+D_g-wQs zEvYXlls`27=lCh1PI0mE?BJ?90=B^g_Lxu5BhlVdr&ZnPwaYAF439}@#`1bO*iGP{ z>y-Xu*DQYG;x6dP`x`^VoWD9mSe#fStoF9PSHQ+a|Ky1tBWKr}Fm7tH+>lpsi*yp9 z5!NO5{>X`Ft8)V9$i-GLLNH5+v7f!BA&6fax2c{=Mk;J_gwWztmS@nGlJ>c*Mb7rs zZ2Jf5?jd2A?c86NFTwfmk3;hdnx_vn(kZxBa%Yqp#+0m1%7@|~vP;(CnYEj(t|itF zU!L=~h7YC_mu_r6Ml5*8Ss}EM_O}`4@ z9v`U?Dy?XJ)K}IHNaO3^0+Q^PY@6nx;rKPHco>?zNj5iimNy>MQn7?;UcxpVt)f4apw^p7i~s;`Tvnq6Kx}^v!mn z%Z`GFQ>W+a+(B0J+KtEUb+M|Wf8n*Vqi-MDH_LLY&4J3ib6Y%?IG4H2i>l;%fGGwQx|65(x8PtT* zwFOZ?nq0lo35bGGs(|z+%?g4Mq>BN9A{_(-5_&TTN{Muki-^(`0YyTn3DQEj1Ta99 zh!6sXAR&Q-0T?wU}9c09Flg#cQ5+ntna?ZNzSL^wySqzF5JiH5|x}YTcN1h^fyX z7?QWIk3P*8N?Gu-qPm@7FA8}!9hWfsT8L0MaGcY4D%_12dx{QpnCJ|#rjgz?&JZ`; z+Nt5!TaPX1~Su+aO-QIKS~O-OANxv$5=KELl- z?Bl37j7lH=m597;iSd)RaM{S9T}}!^`G(Cb>eZX>FYld=uT-pkg?`<9bT8feoV&)x zrnN`Z(uKVD^pvJtsKq%HbAK@tE2~G%db({de^cjb(*dPnnc7^L--6S*P$$}|Y57>P)vzn+-sASkzGEj+29OW!R z`~}vc=_$a8T%0fuY0&E~cDA+?wqE5n+&%aRi&9~|o<-ny%!D$6M} zkk*l@Q17S?@ye_AH51d(t*%k8O?bu70v8^}x0*v#0;8r+lTL{+d$@<(3Sm5rb$2Fc zcTo!b6C@#r6{Ur4zX9_;**=iC$bY*oCa_lxoh}8FBhF}bWl~4sIhs%tqMIZtky_!+ zqQSX+13c%vx)fH^6wS~l;w1@V3_fD84+|m%J4OulW2wP8vczCtRv;V?he$cYS0pQZ z=Dm%vjN?QzEJH&RszlV&Vn0N~o>HSU*>>n!DGg0epLyTTeugL-B1!nn7=;sh8R}>* zhX)*0Lgh{|%GiKo&zoYRi$Zg#Mv|yJs@GkXI-26gGJzBN7_m|uUoGI0EERy zgnec}Bsu5MX2T339QK5&BFPCrbJvKV$!-UB6=ieMBw-%)6>HV;c6&cUvTic>2mJ}` zZP(eAFKF(c^efU#cW_0(dU$qx<6Erm&d2`*k%mlnU;`omQPQvUiLc+H=`*m%<+-Db zCxFidPw-n0&ue`*!Dc4dt$cS}`94=O7QV2Yx59yvKK@#$Stv=vZqNE3b-!S`lo66B}a{XqMfD@Exe%jCP&(h-h8udk9joGliv}Rt;E5Hc8c4*by z!5jxl)0drR3NAHSqAc&tEkASH*6cY4bWn9UaR<}91lHQ4ewI{~f_Y8y2i)|xt?W1g za#oahU1+dJs60Pi6K#+p)mKICUsY7>woMe2##;V9 z)cZ>I3vhYW&VUaFT9=jn&B)=Qh8iZv#du#nuOT}^>&ws(xXqNY3_eL?|`E_J2JV{YwC z%W>sfC8R8iZ1@H<`QcieD^%xc6|062S(A2X4=I7LuenbX*B@$ht|Ds@5G_DzoYNrTe>t_G6pAU@l$p(7n`h`lLcB8GSV#04&rq};ZMWzQN|tll}UBp zi`3*!01xuK;#-;2WUgY~`3=;)#cSO5=2N);*uHzFweo0|8{w%Anh?*!Ral za_{OG42ttoOkH8yLAd@Gj- z((i6fkoK6IM5J~kSJ5HacVe~2bN!>lP6&q9?$dxRI2b11I2An~K zzn`7r1GSy949uy*7Y+=&Aew?=K_&5TCN*}ts@o0yj~80%Wo_Uli3-;A;ea`n^I{C6 zhJFX=885?}x1t$~O4Hl^^5ut7>yYq;H>iE=6 zp0m8j>_myfZy$)Jdmp?j7~+yJoYY)pe?)p;)tP5R`t>FmdJ^y|!Xf=d9j;FHmwDMx zt`%M&a!aJ+SdBz?z<}o-I~!NCa%iwkzJY%idVqfG12P%U)Wt38Kbx^VgE2>d9SbkV zoD`)Ud`&-p_Ju2rWV-G5q5N4Mw91)Eq{j`tD85LUFZKCZ{AY5pWwnm$pOtgj0~0es z$q@(TPeeX07J3;gYVn(5;eiyLxD@d0$IXw!o!GZZc_%*! zW2Z)qZZ@yaIjR#2!hE|A{!mtOZ=28Cv5ho$2zV{>0OdNtAL?;w&Dzo490ZCDaq#_0 z3ti4SJ8(b3mzFR|vbkq{J3dmDgwXAPB(Ue2IRZfH@JQM`5vQ-myMC+W9zDqJq*yFua-j9uZ8J-FK zV5Qy~!5Mn210&JgS4qXqip4z-r7Y-CzgFoAiDqoO78;iT(fGCWVc*JTPlc^L)U)7S( zWD=h52amAznikl4)^w2V$J)YtW>l}up_LPlRj}*t3b7lG!(|>H2bMhy-ZHx8i=D=U z+GmD6Vw0m=IN5U?^g0Z@gVOqo#*ZWOcbFXD7e89xR7BHl>GH$PjO-P=cxjUZnjXYw zqr*7Y!fAT+-GhDc&;}eKe~yA)j{@&}(qb+=-wzsr>iMB$S_s*HgWZ>*dY_r2!n{&^ z#r|76$J44}`0l?Un7#4WkS(=%s-L+OFtO6|c2t<;X2D$0sePl(&txu~*_X>RwfxYx zOTvvw{f*F@u1OB|+aIk*718us*nT*|$X(x)XBwE)Fz9~7nXe&!(sLI_SF@@Hq- zLU9)T@gFluSrr5;X`YQ%If`=-jtK3TA$sobhq~`USqoEJ;ozyTd)Gb?porV+<>JE& z>OCfgCw;z&!-4l;r4;yJueQ?9iMCbM3AVml^v;v#`=QUZnZ(W)o{Q@_;Wa%vRn|38wNr>%ef=glFKLjgWqgOEeT{?KnF+- z(+n^ylieP|aHh4e^F!}TM#=*xdAG1$zi;Vpa!V?ID-sWVkF<_3 zblYy%*)|z@%DsAy2BO7#Qvtlu{gi{YHeWU<05a{7v_5S%72qjJD&tpom((H*7g3f+ z=wVF@^stMi*7Df$-|tG@cD*R4b_72sUZZ5S;v;|m1j0U{0OZQk2^rxn^RO|-l{X-;Y0^*EE<$ tnWOl@uuyud2WOa*QqL)(c1cl6{!3H4%Q@J^vTqJ_x)%L^uL0b_zW}W9AV&ZI literal 0 HcmV?d00001 diff --git a/src/gba/renderers/video-software.c b/src/gba/renderers/video-software.c index 4c83844ad..86c3bc409 100644 --- a/src/gba/renderers/video-software.c +++ b/src/gba/renderers/video-software.c @@ -808,12 +808,12 @@ static void _drawScanline(struct GBAVideoSoftwareRenderer* renderer, int y) { if ((y < sprite->y && (sprite->endY - 256 < 0 || y >= sprite->endY - 256)) || y >= sprite->endY) { continue; } - if (GBAObjAttributesAIsMosaic(sprite->obj.a)) { + if (GBAObjAttributesAIsMosaic(sprite->obj.a) && mosaicV > 1) { localY = mosaicY; - if (localY < sprite->y) { + if (localY < sprite->y && sprite->y < GBA_VIDEO_VERTICAL_PIXELS) { localY = sprite->y; } - if (localY >= sprite->endY) { + if (localY >= (sprite->endY & 0xFF)) { localY = sprite->endY - 1; } } From ba00cdfc024e495dc377f07d9ed6e5c57958de9c Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 30 May 2019 12:26:49 -0700 Subject: [PATCH 03/50] GBA Memory: Fix STM to VRAM (fixes #1430) --- CHANGES | 1 + src/gba/memory.c | 52 ++++++++++++++++++++++++++++++------------------ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/CHANGES b/CHANGES index 84dbb27fa..3366a9ea9 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,7 @@ Emulation fixes: - GB Video: Fix window being enabled mid-scanline (fixes mgba.io/i/1328) - GB I/O: Filter IE top bits properly (fixes mgba.io/i/1329) - GBA Video: Fix wrapped sprite mosaic clamping (fixes mgba.io/i/1432) + - GBA Memory: Fix STM to VRAM (fixes mgba.io/i/1430) Other fixes: - Qt: Fix some Qt display driver race conditions - Core: Improved lockstep driver reliability (Le Hoang Quyen) diff --git a/src/gba/memory.c b/src/gba/memory.c index 3f037567c..62fa6fda9 100644 --- a/src/gba/memory.c +++ b/src/gba/memory.c @@ -396,9 +396,10 @@ static void GBASetActiveRegion(struct ARMCore* cpu, uint32_t address) { value = 0; \ break; \ } \ - address &= 0x00017FFC; \ + LOAD_32(value, address & 0x00017FFC, gba->video.vram); \ + } else { \ + LOAD_32(value, address & 0x0001FFFC, gba->video.vram); \ } \ - LOAD_32(value, address & 0x0001FFFC, gba->video.vram); \ wait += waitstatesRegion[REGION_VRAM]; #define LOAD_OAM LOAD_32(value, address & (SIZE_OAM - 4), gba->video.oam.raw); @@ -530,9 +531,10 @@ uint32_t GBALoad16(struct ARMCore* cpu, uint32_t address, int* cycleCounter) { value = 0; break; } - address &= 0x00017FFE; + LOAD_16(value, address & 0x00017FFE, gba->video.vram); + } else { + LOAD_16(value, address & 0x0001FFFE, gba->video.vram); } - LOAD_16(value, address & 0x0001FFFE, gba->video.vram); break; case REGION_OAM: LOAD_16(value, address & (SIZE_OAM - 2), gba->video.oam.raw); @@ -645,9 +647,10 @@ uint32_t GBALoad8(struct ARMCore* cpu, uint32_t address, int* cycleCounter) { value = 0; break; } - address &= 0x00017FFF; + value = ((uint8_t*) gba->video.vram)[address & 0x00017FFF]; + } else { + value = ((uint8_t*) gba->video.vram)[address & 0x0001FFFF]; } - value = ((uint8_t*) gba->video.vram)[address & 0x0001FFFF]; break; case REGION_OAM: value = ((uint8_t*) gba->video.oam.raw)[address & (SIZE_OAM - 1)]; @@ -734,13 +737,19 @@ uint32_t GBALoad8(struct ARMCore* cpu, uint32_t address, int* cycleCounter) { mLOG(GBA_MEM, GAME_ERROR, "Bad VRAM Store32: 0x%08X", address); \ break; \ } \ - address &= 0x00017FFC; \ - } \ - LOAD_32(oldValue, address & 0x0001FFFC, gba->video.vram); \ - if (oldValue != value) { \ - STORE_32(value, address & 0x0001FFFC, gba->video.vram); \ - gba->video.renderer->writeVRAM(gba->video.renderer, (address & 0x0001FFFC) + 2); \ - gba->video.renderer->writeVRAM(gba->video.renderer, (address & 0x0001FFFC)); \ + LOAD_32(oldValue, address & 0x00017FFC, gba->video.vram); \ + if (oldValue != value) { \ + STORE_32(value, address & 0x00017FFC, gba->video.vram); \ + gba->video.renderer->writeVRAM(gba->video.renderer, (address & 0x00017FFC) + 2); \ + gba->video.renderer->writeVRAM(gba->video.renderer, (address & 0x00017FFC)); \ + } \ + } else { \ + LOAD_32(oldValue, address & 0x0001FFFC, gba->video.vram); \ + if (oldValue != value) { \ + STORE_32(value, address & 0x0001FFFC, gba->video.vram); \ + gba->video.renderer->writeVRAM(gba->video.renderer, (address & 0x0001FFFC) + 2); \ + gba->video.renderer->writeVRAM(gba->video.renderer, (address & 0x0001FFFC)); \ + } \ } \ wait += waitstatesRegion[REGION_VRAM]; @@ -855,12 +864,17 @@ void GBAStore16(struct ARMCore* cpu, uint32_t address, int16_t value, int* cycle mLOG(GBA_MEM, GAME_ERROR, "Bad VRAM Store16: 0x%08X", address); break; } - address &= 0x00017FFE; - } - LOAD_16(oldValue, address & 0x0001FFFE, gba->video.vram); - if (value != oldValue) { - STORE_16(value, address & 0x0001FFFE, gba->video.vram); - gba->video.renderer->writeVRAM(gba->video.renderer, address & 0x0001FFFE); + LOAD_16(oldValue, address & 0x00017FFE, gba->video.vram); + if (value != oldValue) { + STORE_16(value, address & 0x00017FFE, gba->video.vram); + gba->video.renderer->writeVRAM(gba->video.renderer, address & 0x00017FFE); + } + } else { + LOAD_16(oldValue, address & 0x0001FFFE, gba->video.vram); + if (value != oldValue) { + STORE_16(value, address & 0x0001FFFE, gba->video.vram); + gba->video.renderer->writeVRAM(gba->video.renderer, address & 0x0001FFFE); + } } break; case REGION_OAM: From 06657d9fde5a2e579ef55b72cfa7ed8269135b83 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 30 May 2019 17:45:34 -0700 Subject: [PATCH 04/50] Qt: Add additional info to map view --- CHANGES | 2 +- src/platform/qt/AssetInfo.cpp | 40 ++++++++++ src/platform/qt/AssetInfo.h | 34 ++++++++ src/platform/qt/AssetTile.cpp | 22 +----- src/platform/qt/AssetTile.h | 7 +- src/platform/qt/AssetTile.ui | 12 ++- src/platform/qt/CMakeLists.txt | 1 + src/platform/qt/MapView.cpp | 64 ++++++++++++++- src/platform/qt/MapView.ui | 137 ++++++++++++++++++--------------- 9 files changed, 231 insertions(+), 88 deletions(-) create mode 100644 src/platform/qt/AssetInfo.cpp create mode 100644 src/platform/qt/AssetInfo.h diff --git a/CHANGES b/CHANGES index 3366a9ea9..b6a6143fe 100644 --- a/CHANGES +++ b/CHANGES @@ -7,7 +7,7 @@ Features: - GB: Expose platform information to CLI debugger - Support Discord Rich Presence - Debugger: Add tracing to file - - Map viewer supports bitmapped GBA modes + - Enhanced map viewer, supporting bitmapped GBA modes and more displayed info - OpenGL renderer with high-resolution upscaling support - Experimental high level "XQ" audio for most GBA games - Interframe blending for games that use flicker effects diff --git a/src/platform/qt/AssetInfo.cpp b/src/platform/qt/AssetInfo.cpp new file mode 100644 index 000000000..f70d8ea0d --- /dev/null +++ b/src/platform/qt/AssetInfo.cpp @@ -0,0 +1,40 @@ +/* Copyright (c) 2013-2019 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/. */ +#include "AssetInfo.h" + +#include +#include + +using namespace QGBA; + +AssetInfo::AssetInfo(QWidget* parent) + : QGroupBox(parent) +{ +} + +void AssetInfo::addCustomProperty(const QString& id, const QString& visibleName) { + QHBoxLayout* newLayout = new QHBoxLayout; + newLayout->addWidget(new QLabel(visibleName)); + QLabel* value = new QLabel; + value->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + value->setAlignment(Qt::AlignRight); + newLayout->addWidget(value); + m_customProperties[id] = value; + int index = customLocation(); + static_cast(layout())->insertLayout(index, newLayout); +} + +void AssetInfo::setCustomProperty(const QString& id, const QVariant& value) { + QLabel* label = m_customProperties[id]; + if (!label) { + return; + } + label->setText(value.toString()); +} + +int AssetInfo::customLocation(const QString&) { + return layout()->count(); +} \ No newline at end of file diff --git a/src/platform/qt/AssetInfo.h b/src/platform/qt/AssetInfo.h new file mode 100644 index 000000000..c30a4f4c5 --- /dev/null +++ b/src/platform/qt/AssetInfo.h @@ -0,0 +1,34 @@ +/* Copyright (c) 2013-2019 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/. */ +#pragma once + +#include +#include +#include +#include + +namespace QGBA { + +class CoreController; + +class AssetInfo : public QGroupBox { +Q_OBJECT + +public: + AssetInfo(QWidget* parent = nullptr); + void addCustomProperty(const QString& id, const QString& visibleName); + +public slots: + void setCustomProperty(const QString& id, const QVariant& value); + +protected: + virtual int customLocation(const QString& id = {}); + +private: + QHash m_customProperties; +}; + +} diff --git a/src/platform/qt/AssetTile.cpp b/src/platform/qt/AssetTile.cpp index ad16d47a3..ea3bcb28c 100644 --- a/src/platform/qt/AssetTile.cpp +++ b/src/platform/qt/AssetTile.cpp @@ -22,7 +22,7 @@ using namespace QGBA; AssetTile::AssetTile(QWidget* parent) - : QGroupBox(parent) + : AssetInfo(parent) { m_ui.setupUi(this); @@ -42,16 +42,8 @@ AssetTile::AssetTile(QWidget* parent) m_ui.b->setFont(font); } -void AssetTile::addCustomProperty(const QString& id, const QString& visibleName) { - QHBoxLayout* newLayout = new QHBoxLayout; - newLayout->addWidget(new QLabel(visibleName)); - QLabel* value = new QLabel; - value->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); - value->setAlignment(Qt::AlignRight); - newLayout->addWidget(value); - m_customProperties[id] = value; - int index = layout()->indexOf(m_ui.line); - static_cast(layout())->insertLayout(index, newLayout); +int AssetTile::customLocation(const QString&) { + return layout()->indexOf(m_ui.line); } void AssetTile::setController(std::shared_ptr controller) { @@ -149,11 +141,3 @@ void AssetTile::selectColor(int index) { m_ui.g->setText(tr("0x%0 (%1)").arg(g, 2, 16, QChar('0')).arg(g, 2, 10, QChar('0'))); m_ui.b->setText(tr("0x%0 (%1)").arg(b, 2, 16, QChar('0')).arg(b, 2, 10, QChar('0'))); } - -void AssetTile::setCustomProperty(const QString& id, const QVariant& value) { - QLabel* label = m_customProperties[id]; - if (!label) { - return; - } - label->setText(value.toString()); -} diff --git a/src/platform/qt/AssetTile.h b/src/platform/qt/AssetTile.h index 35dd14e5b..676582387 100644 --- a/src/platform/qt/AssetTile.h +++ b/src/platform/qt/AssetTile.h @@ -15,13 +15,12 @@ namespace QGBA { class CoreController; -class AssetTile : public QGroupBox { +class AssetTile : public AssetInfo { Q_OBJECT public: AssetTile(QWidget* parent = nullptr); void setController(std::shared_ptr); - void addCustomProperty(const QString& id, const QString& visibleName); public slots: void setPalette(int); @@ -29,7 +28,9 @@ public slots: void selectIndex(int); void setFlip(bool h, bool v); void selectColor(int); - void setCustomProperty(const QString& id, const QVariant& value); + +protected: + int customLocation(const QString& id = {}) override; private: Ui::AssetTile m_ui; diff --git a/src/platform/qt/AssetTile.ui b/src/platform/qt/AssetTile.ui index e5557506b..403e26d60 100644 --- a/src/platform/qt/AssetTile.ui +++ b/src/platform/qt/AssetTile.ui @@ -1,13 +1,13 @@ AssetTile - + 0 0 - 171 - 355 + 241 + 406 @@ -185,6 +185,12 @@ + + QGBA::AssetInfo + QGroupBox +
AssetInfo.h
+ 1 +
QGBA::Swatch QWidget diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index e5ae84e58..0dbb75efa 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -59,6 +59,7 @@ set(SOURCE_FILES AbstractUpdater.cpp Action.cpp ActionMapper.cpp + AssetInfo.cpp AssetTile.cpp AssetView.cpp AudioProcessor.cpp diff --git a/src/platform/qt/MapView.cpp b/src/platform/qt/MapView.cpp index a0232c50b..91a283121 100644 --- a/src/platform/qt/MapView.cpp +++ b/src/platform/qt/MapView.cpp @@ -18,6 +18,7 @@ #include #endif #ifdef M_CORE_GB +#include #include #endif @@ -42,6 +43,12 @@ MapView::MapView(std::shared_ptr controller, QWidget* parent) m_boundary = 2048; m_addressBase = BASE_VRAM; m_addressWidth = 8; + m_ui.bgInfo->addCustomProperty("priority", tr("Priority")); + m_ui.bgInfo->addCustomProperty("screenBase", tr("Map base")); + m_ui.bgInfo->addCustomProperty("charBase", tr("Tile base")); + m_ui.bgInfo->addCustomProperty("size", tr("Size")); + m_ui.bgInfo->addCustomProperty("offset", tr("Offset")); + m_ui.bgInfo->addCustomProperty("transform", tr("Xform")); break; #endif #ifdef M_CORE_GB @@ -49,6 +56,9 @@ MapView::MapView(std::shared_ptr controller, QWidget* parent) m_boundary = 1024; m_addressBase = GB_BASE_VRAM; m_addressWidth = 4; + m_ui.bgInfo->addCustomProperty("screenBase", tr("Map base")); + m_ui.bgInfo->addCustomProperty("charBase", tr("Tile base")); + m_ui.bgInfo->addCustomProperty("offset", tr("Offset")); break; #endif default: @@ -143,16 +153,58 @@ void MapView::updateTilesGBA(bool force) { { CoreController::Interrupter interrupter(m_controller); int bitmap = -1; + int priority = -1; + int frame = 0; + QString offset(tr("N/A")); + QString transform(tr("N/A")); if (m_controller->platform() == PLATFORM_GBA) { - int mode = GBARegisterDISPCNTGetMode(static_cast(m_controller->thread()->core->board)->memory.io[REG_DISPCNT]); + uint16_t* io = static_cast(m_controller->thread()->core->board)->memory.io; + int mode = GBARegisterDISPCNTGetMode(io[REG_DISPCNT >> 1]); if (m_map == 2 && mode > 2) { bitmap = mode == 4 ? 1 : 0; + if (mode != 3) { + frame = GBARegisterDISPCNTGetFrameSelect(io[REG_DISPCNT >> 1]); + } } + priority = GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + m_map]); + if (mode == 0 || (mode == 1 && m_map != 2)) { + offset = QString("%1, %2") + .arg(io[(REG_BG0HOFS >> 1) + (m_map << 1)]) + .arg(io[(REG_BG0VOFS >> 1) + (m_map << 1)]); + } else if ((mode > 0 && m_map == 2) || (mode == 2 && m_map == 3)) { + int32_t refX = io[(REG_BG2X_LO >> 1) + ((m_map - 2) << 2)]; + refX |= io[(REG_BG2X_HI >> 1) + ((m_map - 2) << 2)] << 16; + int32_t refY = io[(REG_BG2Y_LO >> 1) + ((m_map - 2) << 2)]; + refY |= io[(REG_BG2Y_HI >> 1) + ((m_map - 2) << 2)] << 16; + refX <<= 4; + refY <<= 4; + refX >>= 4; + refY >>= 4; + offset = QString("%1\n%2").arg(refX / 65536., 0, 'f', 3).arg(refY / 65536., 0, 'f', 3); + transform = QString("%1 %2\n%3 %4") + .arg(io[(REG_BG2PA >> 1) + ((m_map - 2) << 2)] / 256., 3, 'f', 2) + .arg(io[(REG_BG2PB >> 1) + ((m_map - 2) << 2)] / 256., 3, 'f', 2) + .arg(io[(REG_BG2PC >> 1) + ((m_map - 2) << 2)] / 256., 3, 'f', 2) + .arg(io[(REG_BG2PD >> 1) + ((m_map - 2) << 2)] / 256., 3, 'f', 2); + + } + } + if (m_controller->platform() == PLATFORM_GB) { + uint8_t* io = static_cast(m_controller->thread()->core->board)->memory.io; + int x = io[m_map == 0 ? 0x42 : 0x4A]; + int y = io[m_map == 0 ? 0x43 : 0x4B]; + offset = QString("%1, %2").arg(x).arg(y); } if (bitmap >= 0) { mBitmapCache* bitmapCache = mBitmapCacheSetGetPointer(&m_cacheSet->bitmaps, bitmap); int width = mBitmapCacheSystemInfoGetWidth(bitmapCache->sysConfig); int height = mBitmapCacheSystemInfoGetHeight(bitmapCache->sysConfig); + m_ui.bgInfo->setCustomProperty("screenBase", QString("0x%1").arg(m_addressBase + bitmapCache->bitsStart[frame], 8, 16, QChar('0'))); + m_ui.bgInfo->setCustomProperty("charBase", tr("N/A")); + m_ui.bgInfo->setCustomProperty("size", QString("%1×%2").arg(width).arg(height)); + m_ui.bgInfo->setCustomProperty("priority", priority); + m_ui.bgInfo->setCustomProperty("offset", offset); + m_ui.bgInfo->setCustomProperty("transform", transform); m_rawMap = QImage(QSize(width, height), QImage::Format_ARGB32); uchar* bgBits = m_rawMap.bits(); for (int j = 0; j < height; ++j) { @@ -163,6 +215,16 @@ void MapView::updateTilesGBA(bool force) { mMapCache* mapCache = mMapCacheSetGetPointer(&m_cacheSet->maps, m_map); int tilesW = 1 << mMapCacheSystemInfoGetTilesWide(mapCache->sysConfig); int tilesH = 1 << mMapCacheSystemInfoGetTilesHigh(mapCache->sysConfig); + m_ui.bgInfo->setCustomProperty("screenBase", QString("%0%1") + .arg(m_addressWidth == 8 ? "0x" : "") + .arg(m_addressBase + mapCache->mapStart, m_addressWidth, 16, QChar('0'))); + m_ui.bgInfo->setCustomProperty("charBase", QString("%0%1") + .arg(m_addressWidth == 8 ? "0x" : "") + .arg(m_addressBase + mapCache->tileCache->tileBase, m_addressWidth, 16, QChar('0'))); + m_ui.bgInfo->setCustomProperty("size", QString("%1×%2").arg(tilesW * 8).arg(tilesH * 8)); + m_ui.bgInfo->setCustomProperty("priority", priority); + m_ui.bgInfo->setCustomProperty("offset", offset); + m_ui.bgInfo->setCustomProperty("transform", transform); m_rawMap = QImage(QSize(tilesW * 8, tilesH * 8), QImage::Format_ARGB32); uchar* bgBits = m_rawMap.bits(); for (int j = 0; j < tilesH; ++j) { diff --git a/src/platform/qt/MapView.ui b/src/platform/qt/MapView.ui index d07a832b5..b9d5418f3 100644 --- a/src/platform/qt/MapView.ui +++ b/src/platform/qt/MapView.ui @@ -6,18 +6,80 @@ 0 0 - 641 - 489 + 1273 + 736 Maps - - - + + + - + + + + + + + + + + + + Export + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + + + + × + + + 1 + + + 8 + + + + + + + Magnification + + + + + + + + true @@ -30,8 +92,8 @@ 0 0 - 457 - 463 + 835 + 720 @@ -71,62 +133,15 @@
- - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - - - 0 - 0 - - - - × - - - 1 - - - 8 - - - - - - - Magnification - - - - - - - - - Export - - - + + QGBA::AssetInfo + QGroupBox +
AssetInfo.h
+ 1 +
QGBA::AssetTile QGroupBox From db2b56f418359e03f0505bac5e894cf821989cc3 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 30 May 2019 20:56:19 -0700 Subject: [PATCH 05/50] Qt: Add getPixels call for a finished context --- src/platform/qt/CoreController.cpp | 20 ++++++++++++++++++++ src/platform/qt/CoreController.h | 1 + src/platform/qt/Window.cpp | 5 +---- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/platform/qt/CoreController.cpp b/src/platform/qt/CoreController.cpp index 579f75d04..e1d4bc7ea 100644 --- a/src/platform/qt/CoreController.cpp +++ b/src/platform/qt/CoreController.cpp @@ -210,6 +210,26 @@ const color_t* CoreController::drawContext() { return reinterpret_cast(m_completeBuffer.constData()); } +QImage CoreController::getPixels() { + QByteArray buffer; + QSize size = screenDimensions(); + size_t stride = size.width() * BYTES_PER_PIXEL; + + if (!m_hwaccel) { + buffer = m_completeBuffer; + } else { + Interrupter interrupter(this); + const void* pixels; + m_threadContext.core->getPixels(m_threadContext.core, &pixels, &stride); + stride *= BYTES_PER_PIXEL; + buffer.resize(stride * size.height()); + memcpy(buffer.data(), pixels, buffer.size()); + } + + return QImage(reinterpret_cast(buffer.constData()), + size.width(), size.height(), stride, QImage::Format_RGBX8888); +} + bool CoreController::isPaused() { return mCoreThreadIsPaused(&m_threadContext); } diff --git a/src/platform/qt/CoreController.h b/src/platform/qt/CoreController.h index 978421249..ac1e327ba 100644 --- a/src/platform/qt/CoreController.h +++ b/src/platform/qt/CoreController.h @@ -67,6 +67,7 @@ public: mCoreThread* thread() { return &m_threadContext; } const color_t* drawContext(); + QImage getPixels(); bool isPaused(); bool hasStarted(); diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index 00a7f5b00..d87674d55 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -1706,11 +1706,8 @@ void Window::focusCheck() { } void Window::updateFrame() { - QSize size = m_controller->screenDimensions(); - QImage currentImage(reinterpret_cast(m_controller->drawContext()), size.width(), size.height(), - size.width() * BYTES_PER_PIXEL, QImage::Format_RGBX8888); QPixmap pixmap; - pixmap.convertFromImage(currentImage); + pixmap.convertFromImage(m_controller->getPixels()); m_screenWidget->setPixmap(pixmap); emit paused(true); } From 86efc6cc9f138ef54e4478b76cc91c0bb173362d Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 31 May 2019 15:32:22 -0700 Subject: [PATCH 06/50] Qt: Add frame inspector for GBA games --- CHANGES | 1 + src/platform/qt/AssetView.cpp | 175 ++++++++++++++++++- src/platform/qt/AssetView.h | 39 ++++- src/platform/qt/CMakeLists.txt | 2 + src/platform/qt/FrameView.cpp | 309 +++++++++++++++++++++++++++++++++ src/platform/qt/FrameView.h | 85 +++++++++ src/platform/qt/FrameView.ui | 156 +++++++++++++++++ src/platform/qt/MapView.cpp | 13 +- src/platform/qt/MapView.ui | 8 +- src/platform/qt/ObjView.cpp | 194 ++++++--------------- src/platform/qt/ObjView.h | 14 +- src/platform/qt/Window.cpp | 9 +- 12 files changed, 829 insertions(+), 176 deletions(-) create mode 100644 src/platform/qt/FrameView.cpp create mode 100644 src/platform/qt/FrameView.h create mode 100644 src/platform/qt/FrameView.ui diff --git a/CHANGES b/CHANGES index b6a6143fe..8b449be32 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,7 @@ Features: - OpenGL renderer with high-resolution upscaling support - Experimental high level "XQ" audio for most GBA games - Interframe blending for games that use flicker effects + - Frame inspector for dissecting and debugging rendering Emulation fixes: - GBA: All IRQs have 7 cycle delay (fixes mgba.io/i/539, mgba.io/i/1208) - GBA: Reset now reloads multiboot ROMs diff --git a/src/platform/qt/AssetView.cpp b/src/platform/qt/AssetView.cpp index 2f48cce32..eef257025 100644 --- a/src/platform/qt/AssetView.cpp +++ b/src/platform/qt/AssetView.cpp @@ -1,4 +1,4 @@ -/* Copyright (c) 2013-2016 Jeffrey Pfau +/* Copyright (c) 2013-2019 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 @@ -9,6 +9,16 @@ #include +#ifdef M_CORE_GBA +#include +#endif +#ifdef M_CORE_GB +#include +#include +#endif + +#include + using namespace QGBA; AssetView::AssetView(std::shared_ptr controller, QWidget* parent) @@ -98,3 +108,166 @@ void AssetView::compositeTile(const void* tBuffer, void* buffer, size_t stride, break; } } + +QImage AssetView::compositeMap(int map, mMapCacheEntry* mapStatus) { + mMapCache* mapCache = mMapCacheSetGetPointer(&m_cacheSet->maps, map); + int tilesW = 1 << mMapCacheSystemInfoGetTilesWide(mapCache->sysConfig); + int tilesH = 1 << mMapCacheSystemInfoGetTilesHigh(mapCache->sysConfig); + QImage rawMap = QImage(QSize(tilesW * 8, tilesH * 8), QImage::Format_ARGB32); + uchar* bgBits = rawMap.bits(); + for (int j = 0; j < tilesH; ++j) { + for (int i = 0; i < tilesW; ++i) { + mMapCacheCleanTile(mapCache, mapStatus, i, j); + } + for (int i = 0; i < 8; ++i) { + memcpy(static_cast(&bgBits[tilesW * 32 * (i + j * 8)]), mMapCacheGetRow(mapCache, i + j * 8), tilesW * 32); + } + } + return rawMap.rgbSwapped(); +} + +QImage AssetView::compositeObj(const ObjInfo& objInfo) { + mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, objInfo.paletteSet); + const color_t* rawPalette = mTileCacheGetPalette(tileCache, objInfo.paletteId); + unsigned colors = 1 << objInfo.bits; + QVector palette; + + palette.append(rawPalette[0] & 0xFFFFFF); + for (unsigned c = 1; c < colors && c < 256; ++c) { + palette.append(rawPalette[c] | 0xFF000000); + } + + QImage image = QImage(QSize(objInfo.width * 8, objInfo.height * 8), QImage::Format_Indexed8); + image.setColorTable(palette); + uchar* bits = image.bits(); + unsigned t = objInfo.tile; + for (int y = 0; y < objInfo.height; ++y) { + for (int x = 0; x < objInfo.width; ++x, ++t) { + compositeTile(static_cast(mTileCacheGetVRAM(tileCache, t)), bits, objInfo.width * 8, x * 8, y * 8, objInfo.bits); + } + t += objInfo.stride - objInfo.width; + } + return image.rgbSwapped(); +} + +bool AssetView::lookupObj(int id, struct ObjInfo* info) { + switch (m_controller->platform()) { +#ifdef M_CORE_GBA + case PLATFORM_GBA: + return lookupObjGBA(id, info); +#endif +#ifdef M_CORE_GB + case PLATFORM_GB: + return lookupObjGB(id, info); +#endif + default: + return false; + } +} + +#ifdef M_CORE_GBA +bool AssetView::lookupObjGBA(int id, struct ObjInfo* info) { + if (id > 127) { + return false; + } + + const GBA* gba = static_cast(m_controller->thread()->core->board); + const GBAObj* obj = &gba->video.oam.obj[id]; + + unsigned shape = GBAObjAttributesAGetShape(obj->a); + unsigned size = GBAObjAttributesBGetSize(obj->b); + unsigned width = GBAVideoObjSizes[shape * 4 + size][0]; + unsigned height = GBAVideoObjSizes[shape * 4 + size][1]; + unsigned tile = GBAObjAttributesCGetTile(obj->c); + unsigned palette = GBAObjAttributesCGetPalette(obj->c); + unsigned tileBase = tile; + unsigned paletteSet; + unsigned bits; + if (GBAObjAttributesAIs256Color(obj->a)) { + paletteSet = 3; + palette = 0; + tile /= 2; + bits = 8; + } else { + paletteSet = 2; + bits = 4; + } + ObjInfo newInfo{ + tile, + width / 8, + height / 8, + width / 8, + palette, + paletteSet, + bits, + !GBAObjAttributesAIsDisable(obj->a) || GBAObjAttributesAIsTransformed(obj->a), + GBAObjAttributesCGetPriority(obj->c), + GBAObjAttributesBGetX(obj->b), + GBAObjAttributesAGetY(obj->a), + GBAObjAttributesBIsHFlip(obj->b), + GBAObjAttributesBIsVFlip(obj->b), + }; + GBARegisterDISPCNT dispcnt = gba->memory.io[0]; // FIXME: Register name can't be imported due to namespacing issues + if (!GBARegisterDISPCNTIsObjCharacterMapping(dispcnt)) { + newInfo.stride = 0x20 >> (GBAObjAttributesAGet256Color(obj->a)); + }; + *info = newInfo; + return true; +} +#endif + +#ifdef M_CORE_GB +bool AssetView::lookupObjGB(int id, struct ObjInfo* info) { + if (id > 39) { + return false; + } + + const GB* gb = static_cast(m_controller->thread()->core->board); + const GBObj* obj = &gb->video.oam.obj[id]; + + unsigned width = 8; + unsigned height = 8; + GBRegisterLCDC lcdc = gb->memory.io[REG_LCDC]; + if (GBRegisterLCDCIsObjSize(lcdc)) { + height = 16; + } + unsigned tile = obj->tile; + unsigned palette = 0; + if (gb->model >= GB_MODEL_CGB) { + if (GBObjAttributesIsBank(obj->attr)) { + tile += 512; + } + palette = GBObjAttributesGetCGBPalette(obj->attr); + } else { + palette = GBObjAttributesGetPalette(obj->attr); + } + palette += 8; + + ObjInfo newInfo{ + tile, + 1, + height / 8, + 1, + palette, + 0, + 2, + obj->y != 0 && obj->y < 160, + GBObjAttributesGetPriority(obj->attr), + obj->x, + obj->y, + GBObjAttributesIsXFlip(obj->attr), + GBObjAttributesIsYFlip(obj->attr), + }; + *info = newInfo; + return true; +} +#endif + +bool AssetView::ObjInfo::operator!=(const ObjInfo& other) const { + return other.tile != tile || + other.width != width || + other.height != height || + other.stride != stride || + other.paletteId != paletteId || + other.paletteSet != paletteSet; +} \ No newline at end of file diff --git a/src/platform/qt/AssetView.h b/src/platform/qt/AssetView.h index a95483c5e..779e22890 100644 --- a/src/platform/qt/AssetView.h +++ b/src/platform/qt/AssetView.h @@ -12,6 +12,8 @@ #include +struct mMapCacheEntry; + namespace QGBA { class CoreController; @@ -22,8 +24,6 @@ Q_OBJECT public: AssetView(std::shared_ptr controller, QWidget* parent = nullptr); - static void compositeTile(const void* tile, void* image, size_t stride, size_t x, size_t y, int depth = 8); - protected slots: void updateTiles(); void updateTiles(bool force); @@ -40,9 +40,42 @@ protected: void showEvent(QShowEvent*) override; mCacheSet* const m_cacheSet; + std::shared_ptr m_controller; + +protected: + struct ObjInfo { + unsigned tile; + unsigned width; + unsigned height; + unsigned stride; + unsigned paletteId; + unsigned paletteSet; + unsigned bits; + + bool enabled : 1; + unsigned priority : 2; + unsigned x : 9; + unsigned y : 9; + bool hflip : 1; + bool vflip : 1; + + bool operator!=(const ObjInfo&) const; + }; + + static void compositeTile(const void* tile, void* image, size_t stride, size_t x, size_t y, int depth = 8); + QImage compositeMap(int map, mMapCacheEntry*); + QImage compositeObj(const ObjInfo&); + + bool lookupObj(int id, struct ObjInfo*); private: - std::shared_ptr m_controller; +#ifdef M_CORE_GBA + bool lookupObjGBA(int id, struct ObjInfo*); +#endif +#ifdef M_CORE_GB + bool lookupObjGB(int id, struct ObjInfo*); +#endif + QTimer m_updateTimer; }; diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 0dbb75efa..49be1cda7 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -72,6 +72,7 @@ set(SOURCE_FILES Display.cpp DisplayGL.cpp DisplayQt.cpp + FrameView.cpp GBAApp.cpp GBAKeyEditor.cpp GIFView.cpp @@ -122,6 +123,7 @@ set(UI_FILES BattleChipView.ui CheatsView.ui DebuggerConsole.ui + FrameView.ui GIFView.ui IOViewer.ui LoadSaveState.ui diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp new file mode 100644 index 000000000..121bd9171 --- /dev/null +++ b/src/platform/qt/FrameView.cpp @@ -0,0 +1,309 @@ +/* Copyright (c) 2013-2019 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/. */ +#include "FrameView.h" + +#include +#include +#include + +#include + +#include "CoreController.h" + +#ifdef M_CORE_GBA +#include +#include +#include +#include +#endif +#ifdef M_CORE_GB +#include +#include +#endif + +using namespace QGBA; + +FrameView::FrameView(std::shared_ptr controller, QWidget* parent) + : AssetView(controller, parent) +{ + m_ui.setupUi(this); + + m_glowTimer.setInterval(33); + connect(&m_glowTimer, &QTimer::timeout, this, [this]() { + ++m_glowFrame; + invalidateQueue(); + }); + m_glowTimer.start(); + + m_ui.compositedView->installEventFilter(this); + + connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) { + Layer& layer = m_queue[item->data(Qt::UserRole).toInt()]; + layer.enabled = item->checkState() == Qt::Checked; + if (layer.enabled) { + m_disabled.remove(layer.id); + } else { + m_disabled.insert(layer.id); + } + invalidateQueue(); + }); + connect(m_ui.queue, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* item) { + if (item) { + m_active = m_queue[item->data(Qt::UserRole).toInt()].id; + } else { + m_active = {}; + } + invalidateQueue(); + }); +} + +void FrameView::selectLayer(const QPointF& coord) { + for (const Layer& layer : m_queue) { + QPointF location = layer.location; + QSizeF layerDims(layer.image.width(), layer.image.height()); + QRegion region; + if (layer.repeats) { + if (location.x() + layerDims.width() < 0) { + location.setX(std::fmod(location.x(), layerDims.width())); + } + if (location.y() + layerDims.height() < 0) { + location.setY(std::fmod(location.y(), layerDims.height())); + } + + region += layer.mask.translated(location.x(), location.y()); + region += layer.mask.translated(location.x() + layerDims.width(), location.y()); + region += layer.mask.translated(location.x(), location.y() + layerDims.height()); + region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height()); + } else { + region = layer.mask.translated(location.x(), location.y()); + } + + if (region.contains(QPoint(coord.x(), coord.y()))) { + m_active = layer.id; + m_glowFrame = 0; + break; + } + } +} + +void FrameView::updateTilesGBA(bool force) { + if (m_ui.freeze->checkState() == Qt::Checked) { + return; + } + m_queue.clear(); + { + CoreController::Interrupter interrupter(m_controller); + updateRendered(); + + uint16_t* io = static_cast(m_controller->thread()->core->board)->memory.io; + int mode = GBARegisterDISPCNTGetMode(io[REG_DISPCNT >> 1]); + + std::array enabled{ + GBARegisterDISPCNTIsBg0Enable(io[REG_DISPCNT >> 1]), + GBARegisterDISPCNTIsBg1Enable(io[REG_DISPCNT >> 1]), + GBARegisterDISPCNTIsBg2Enable(io[REG_DISPCNT >> 1]), + GBARegisterDISPCNTIsBg3Enable(io[REG_DISPCNT >> 1]), + }; + + for (int priority = 0; priority < 4; ++priority) { + for (int sprite = 0; sprite < 128; ++sprite) { + ObjInfo info; + lookupObj(sprite, &info); + + if (!info.enabled || info.priority != priority) { + continue; + } + + QPointF offset(info.x, info.y); + QImage obj(compositeObj(info)); + if (info.hflip || info.vflip) { + obj = obj.mirrored(info.hflip, info.vflip); + } + m_queue.append({ + { LayerId::SPRITE, sprite }, + !m_disabled.contains({ LayerId::SPRITE, sprite}), + QPixmap::fromImage(obj), + {}, offset, false + }); + if (m_queue.back().image.hasAlpha()) { + m_queue.back().mask = QRegion(m_queue.back().image.mask()); + } else { + m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height()); + } + } + + for (int bg = 0; bg < 4; ++bg) { + if (!enabled[bg]) { + continue; + } + if (GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + bg]) != priority) { + continue; + } + + QPointF offset; + if (mode == 0) { + offset.setX(-(io[(REG_BG0HOFS >> 1) + (bg << 1)] & 0x1FF)); + offset.setY(-(io[(REG_BG0VOFS >> 1) + (bg << 1)] & 0x1FF)); + }; + m_queue.append({ + { LayerId::BACKGROUND, bg }, + !m_disabled.contains({ LayerId::BACKGROUND, bg}), + QPixmap::fromImage(compositeMap(bg, m_mapStatus[bg])), + {}, offset, true + }); + if (m_queue.back().image.hasAlpha()) { + m_queue.back().mask = QRegion(m_queue.back().image.mask()); + } else { + m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height()); + } + } + } + } + invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS)); +} + +void FrameView::updateTilesGB(bool force) { + if (m_ui.freeze->checkState() == Qt::Checked) { + return; + } + m_queue.clear(); + { + CoreController::Interrupter interrupter(m_controller); + updateRendered(); + } + invalidateQueue(m_controller->screenDimensions()); +} + +void FrameView::invalidateQueue(const QSize& dims) { + QSize realDims = dims; + if (!dims.isValid()) { + realDims = m_composited.size() / m_ui.magnification->value(); + } + bool blockSignals = m_ui.queue->blockSignals(true); + QPixmap composited(realDims); + + QPainter painter(&composited); + QPalette palette; + QColor activeColor = palette.color(QPalette::HighlightedText); + activeColor.setAlpha(sin(m_glowFrame * M_PI / 60) * 16 + 96); + + QRectF rect(0, 0, realDims.width(), realDims.height()); + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.fillRect(rect, QColor(0, 0, 0, 0)); + + painter.setCompositionMode(QPainter::CompositionMode_DestinationOver); + for (int i = 0; i < m_queue.count(); ++i) { + const Layer& layer = m_queue[i]; + QListWidgetItem* item; + if (i >= m_ui.queue->count()) { + item = new QListWidgetItem; + m_ui.queue->addItem(item); + } else { + item = m_ui.queue->item(i); + } + item->setText(layer.id.readable()); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + item->setCheckState(layer.enabled ? Qt::Checked : Qt::Unchecked); + item->setData(Qt::UserRole, i); + item->setSelected(layer.id == m_active); + + if (!layer.enabled) { + continue; + } + + QPointF location = layer.location; + QSizeF layerDims(layer.image.width(), layer.image.height()); + QRegion region; + if (layer.repeats) { + if (location.x() + layerDims.width() < 0) { + location.setX(std::fmod(location.x(), layerDims.width())); + } + if (location.y() + layerDims.height() < 0) { + location.setY(std::fmod(location.y(), layerDims.height())); + } + + if (layer.id == m_active) { + region = layer.mask.translated(location.x(), location.y()); + region += layer.mask.translated(location.x() + layerDims.width(), location.y()); + region += layer.mask.translated(location.x(), location.y() + layerDims.height()); + region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height()); + } + } else { + QRectF layerRect(location, layerDims); + if (!rect.intersects(layerRect)) { + continue; + } + if (layer.id == m_active) { + region = layer.mask.translated(location.x(), location.y()); + } + } + + if (layer.id == m_active) { + painter.setClipping(true); + painter.setClipRegion(region); + painter.fillRect(rect, activeColor); + painter.setClipping(false); + } + + if (layer.repeats) { + painter.drawPixmap(location, layer.image); + painter.drawPixmap(location + QPointF(layerDims.width(), 0), layer.image); + painter.drawPixmap(location + QPointF(0, layerDims.height()), layer.image); + painter.drawPixmap(location + QPointF(layerDims.width(), layerDims.height()), layer.image); + } else { + painter.drawPixmap(location, layer.image); + } + } + painter.end(); + + while (m_ui.queue->count() > m_queue.count()) { + delete m_ui.queue->takeItem(m_queue.count()); + } + m_ui.queue->blockSignals(blockSignals); + + m_composited = composited.scaled(realDims * m_ui.magnification->value()); + m_ui.compositedView->setPixmap(m_composited); +} + +void FrameView::updateRendered() { + if (m_ui.freeze->checkState() == Qt::Checked) { + return; + } + m_rendered.convertFromImage(m_controller->getPixels()); + m_rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value()); + m_ui.renderedView->setPixmap(m_rendered); +} + +bool FrameView::eventFilter(QObject* obj, QEvent* event) { + if (event->type() != QEvent::MouseButtonPress) { + return false; + } + QPointF pos = static_cast(event)->localPos(); + pos /= m_ui.magnification->value(); + selectLayer(pos); + return true; +} + +QString FrameView::LayerId::readable() const { + QString typeStr; + switch (type) { + case NONE: + return tr("None"); + case BACKGROUND: + typeStr = tr("Background"); + break; + case WINDOW: + typeStr = tr("Window"); + break; + case SPRITE: + typeStr = tr("Sprite"); + break; + } + if (index < 0) { + return typeStr; + } + return tr("%1 %2").arg(typeStr).arg(index); +} \ No newline at end of file diff --git a/src/platform/qt/FrameView.h b/src/platform/qt/FrameView.h new file mode 100644 index 000000000..626dfbd62 --- /dev/null +++ b/src/platform/qt/FrameView.h @@ -0,0 +1,85 @@ +/* Copyright (c) 2013-2019 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/. */ +#pragma once + +#include "ui_FrameView.h" + +#include +#include +#include +#include +#include +#include + +#include "AssetView.h" + +#include + +namespace QGBA { + +class CoreController; + +class FrameView : public AssetView { +Q_OBJECT + +public: + FrameView(std::shared_ptr controller, QWidget* parent = nullptr); + +public slots: + void selectLayer(const QPointF& coord); + +protected: +#ifdef M_CORE_GBA + void updateTilesGBA(bool force) override; +#endif +#ifdef M_CORE_GB + void updateTilesGB(bool force) override; +#endif + + bool eventFilter(QObject* obj, QEvent* event) override; + +private: + struct LayerId { + enum { + NONE = 0, + BACKGROUND, + WINDOW, + SPRITE + } type = NONE; + int index = -1; + + bool operator==(const LayerId& other) const { return other.type == type && other.index == index; } + operator uint() const { return (type << 8) | index; } + QString readable() const; + }; + + struct Layer { + LayerId id; + bool enabled; + QPixmap image; + QRegion mask; + QPointF location; + bool repeats; + }; + + void invalidateQueue(const QSize& dims = QSize()); + void updateRendered(); + + Ui::FrameView m_ui; + + LayerId m_active{}; + + int m_glowFrame; + QTimer m_glowTimer; + + QList m_queue; + QSet m_disabled; + QPixmap m_composited; + QPixmap m_rendered; + mMapCacheEntry m_mapStatus[4][128 * 128] = {}; // TODO: Correct size +}; + +} \ No newline at end of file diff --git a/src/platform/qt/FrameView.ui b/src/platform/qt/FrameView.ui new file mode 100644 index 000000000..2e61e09c8 --- /dev/null +++ b/src/platform/qt/FrameView.ui @@ -0,0 +1,156 @@ + + + FrameView + + + + 0 + 0 + 869 + 875 + + + + Inspect frame + + + + + + + + + 0 + 0 + + + + × + + + 1 + + + 8 + + + + + + + Magnification + + + + + + + + + Freeze frame + + + + + + + true + + + + + 0 + 0 + 591 + 403 + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + true + + + + + 0 + 0 + 591 + 446 + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + false + + + Export + + + + + + + + 0 + 0 + + + + + + + + + diff --git a/src/platform/qt/MapView.cpp b/src/platform/qt/MapView.cpp index 91a283121..0fe80c2c3 100644 --- a/src/platform/qt/MapView.cpp +++ b/src/platform/qt/MapView.cpp @@ -211,6 +211,7 @@ void MapView::updateTilesGBA(bool force) { mBitmapCacheCleanRow(bitmapCache, m_bitmapStatus, j); memcpy(static_cast(&bgBits[width * j * 4]), mBitmapCacheGetRow(bitmapCache, j), width * 4); } + m_rawMap = m_rawMap.rgbSwapped(); } else { mMapCache* mapCache = mMapCacheSetGetPointer(&m_cacheSet->maps, m_map); int tilesW = 1 << mMapCacheSystemInfoGetTilesWide(mapCache->sysConfig); @@ -225,19 +226,9 @@ void MapView::updateTilesGBA(bool force) { m_ui.bgInfo->setCustomProperty("priority", priority); m_ui.bgInfo->setCustomProperty("offset", offset); m_ui.bgInfo->setCustomProperty("transform", transform); - m_rawMap = QImage(QSize(tilesW * 8, tilesH * 8), QImage::Format_ARGB32); - uchar* bgBits = m_rawMap.bits(); - for (int j = 0; j < tilesH; ++j) { - for (int i = 0; i < tilesW; ++i) { - mMapCacheCleanTile(mapCache, m_mapStatus, i, j); - } - for (int i = 0; i < 8; ++i) { - memcpy(static_cast(&bgBits[tilesW * 32 * (i + j * 8)]), mMapCacheGetRow(mapCache, i + j * 8), tilesW * 32); - } - } + m_rawMap = compositeMap(m_map, m_mapStatus); } } - m_rawMap = m_rawMap.rgbSwapped(); QPixmap map = QPixmap::fromImage(m_rawMap.convertToFormat(QImage::Format_RGB32)); if (m_ui.magnification->value() > 1) { map = map.scaled(map.size() * m_ui.magnification->value()); diff --git a/src/platform/qt/MapView.ui b/src/platform/qt/MapView.ui index b9d5418f3..d21cce841 100644 --- a/src/platform/qt/MapView.ui +++ b/src/platform/qt/MapView.ui @@ -6,8 +6,8 @@ 0 0 - 1273 - 736 + 941 + 617 @@ -92,8 +92,8 @@ 0 0 - 835 - 720 + 613 + 601 diff --git a/src/platform/qt/ObjView.cpp b/src/platform/qt/ObjView.cpp index 64b689640..e482f023b 100644 --- a/src/platform/qt/ObjView.cpp +++ b/src/platform/qt/ObjView.cpp @@ -19,9 +19,7 @@ #endif #ifdef M_CORE_GB #include -#include #endif -#include #include using namespace QGBA; @@ -53,11 +51,7 @@ ObjView::ObjView(std::shared_ptr controller, QWidget* parent) connect(m_ui.magnification, static_cast(&QSpinBox::valueChanged), [this]() { updateTiles(true); }); -#ifdef USE_PNG connect(m_ui.exportButton, &QAbstractButton::clicked, this, &ObjView::exportObj); -#else - m_ui.exportButton->setVisible(false); -#endif } void ObjView::selectObj(int obj) { @@ -77,79 +71,56 @@ void ObjView::updateTilesGBA(bool force) { const GBA* gba = static_cast(m_controller->thread()->core->board); const GBAObj* obj = &gba->video.oam.obj[m_objId]; - unsigned shape = GBAObjAttributesAGetShape(obj->a); - unsigned size = GBAObjAttributesBGetSize(obj->b); - unsigned width = GBAVideoObjSizes[shape * 4 + size][0]; - unsigned height = GBAVideoObjSizes[shape * 4 + size][1]; - unsigned tile = GBAObjAttributesCGetTile(obj->c); - m_ui.tiles->setTileCount(width * height / 64); - m_ui.tiles->setMinimumSize(QSize(width, height) * m_ui.magnification->value()); - m_ui.tiles->resize(QSize(width, height) * m_ui.magnification->value()); - unsigned palette = GBAObjAttributesCGetPalette(obj->c); - unsigned tileBase = tile; - unsigned paletteSet; - unsigned bits; + ObjInfo newInfo; + lookupObj(m_objId, &newInfo); + + m_ui.tiles->setTileCount(newInfo.width * newInfo.height); + m_ui.tiles->setMinimumSize(QSize(newInfo.width * 8, newInfo.height * 8) * m_ui.magnification->value()); + m_ui.tiles->resize(QSize(newInfo.width * 8, newInfo.height * 8) * m_ui.magnification->value()); + unsigned tileBase = newInfo.tile; + unsigned tile = newInfo.tile; if (GBAObjAttributesAIs256Color(obj->a)) { m_ui.palette->setText("256-color"); - paletteSet = 3; m_ui.tile->setBoundary(1024, 1, 3); m_ui.tile->setPalette(0); m_boundary = 1024; - palette = 0; - tile /= 2; - bits = 8; + tileBase *= 2; } else { - m_ui.palette->setText(QString::number(palette)); - paletteSet = 2; + m_ui.palette->setText(QString::number(newInfo.paletteId)); m_ui.tile->setBoundary(2048, 0, 2); - m_ui.tile->setPalette(palette); - m_boundary = 2048; - bits = 4; + m_ui.tile->setPalette(newInfo.paletteId); } - ObjInfo newInfo{ - tile, - width / 8, - height / 8, - width / 8, - palette, - paletteSet, - bits - }; if (newInfo != m_objInfo) { force = true; } - GBARegisterDISPCNT dispcnt = gba->memory.io[0]; // FIXME: Register name can't be imported due to namespacing issues - if (!GBARegisterDISPCNTIsObjCharacterMapping(dispcnt)) { - newInfo.stride = 0x20 >> (GBAObjAttributesAGet256Color(obj->a)); - }; m_objInfo = newInfo; - m_tileOffset = tile; - mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, paletteSet); + m_tileOffset = newInfo.tile; + mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, newInfo.paletteSet); int i = 0; - for (int y = 0; y < height / 8; ++y) { - for (int x = 0; x < width / 8; ++x, ++i, ++tile, ++tileBase) { - const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[16 * tileBase], tile, palette); + for (int y = 0; y < newInfo.height; ++y) { + for (int x = 0; x < newInfo.width; ++x, ++i, ++tile, ++tileBase) { + const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[16 * tileBase], tile, newInfo.paletteId); if (data) { m_ui.tiles->setTile(i, data); } else if (force) { - m_ui.tiles->setTile(i, mTileCacheGetTile(tileCache, tile, palette)); + m_ui.tiles->setTile(i, mTileCacheGetTile(tileCache, tile, newInfo.paletteId)); } } - tile += newInfo.stride - width / 8; - tileBase += newInfo.stride - width / 8; + tile += newInfo.stride - newInfo.width; + tileBase += newInfo.stride - newInfo.width; } - m_ui.x->setText(QString::number(GBAObjAttributesBGetX(obj->b))); - m_ui.y->setText(QString::number(GBAObjAttributesAGetY(obj->a))); - m_ui.w->setText(QString::number(width)); - m_ui.h->setText(QString::number(height)); + m_ui.x->setText(QString::number(newInfo.x)); + m_ui.y->setText(QString::number(newInfo.y)); + m_ui.w->setText(QString::number(newInfo.width * 8)); + m_ui.h->setText(QString::number(newInfo.height * 8)); m_ui.address->setText(tr("0x%0").arg(BASE_OAM + m_objId * sizeof(*obj), 8, 16, QChar('0'))); - m_ui.priority->setText(QString::number(GBAObjAttributesCGetPriority(obj->c))); - m_ui.flippedH->setChecked(GBAObjAttributesBIsHFlip(obj->b)); - m_ui.flippedV->setChecked(GBAObjAttributesBIsVFlip(obj->b)); - m_ui.enabled->setChecked(!GBAObjAttributesAIsDisable(obj->a) || GBAObjAttributesAIsTransformed(obj->a)); + m_ui.priority->setText(QString::number(newInfo.priority)); + m_ui.flippedH->setChecked(newInfo.hflip); + m_ui.flippedV->setChecked(newInfo.vflip); + m_ui.enabled->setChecked(newInfo.enabled); m_ui.doubleSize->setChecked(GBAObjAttributesAIsDoubleSize(obj->a) && GBAObjAttributesAIsTransformed(obj->a)); m_ui.mosaic->setChecked(GBAObjAttributesAIsMosaic(obj->a)); @@ -182,39 +153,17 @@ void ObjView::updateTilesGB(bool force) { const GB* gb = static_cast(m_controller->thread()->core->board); const GBObj* obj = &gb->video.oam.obj[m_objId]; - mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, 0); - unsigned width = 8; - unsigned height = 8; - GBRegisterLCDC lcdc = gb->memory.io[REG_LCDC]; - if (GBRegisterLCDCIsObjSize(lcdc)) { - height = 16; - } - unsigned tile = obj->tile; - m_ui.tiles->setTileCount(width * height / 64); - m_ui.tile->setBoundary(1024, 0, 0); - m_ui.tiles->setMinimumSize(QSize(width, height) * m_ui.magnification->value()); - m_ui.tiles->resize(QSize(width, height) * m_ui.magnification->value()); - unsigned palette = 0; - if (gb->model >= GB_MODEL_CGB) { - if (GBObjAttributesIsBank(obj->attr)) { - tile += 512; - } - palette = GBObjAttributesGetCGBPalette(obj->attr); - } else { - palette = GBObjAttributesGetPalette(obj->attr); - } - m_ui.palette->setText(QString::number(palette)); - palette += 8; + ObjInfo newInfo; + lookupObj(m_objId, &newInfo); + + mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, 0); + unsigned tile = newInfo.tile; + m_ui.tiles->setTileCount(newInfo.height); + m_ui.tile->setBoundary(1024, 0, 0); + m_ui.tiles->setMinimumSize(QSize(8, newInfo.height * 8) * m_ui.magnification->value()); + m_ui.tiles->resize(QSize(8, newInfo.height * 8) * m_ui.magnification->value()); + m_ui.palette->setText(QString::number(newInfo.paletteId - 8)); - ObjInfo newInfo{ - tile, - 1, - height / 8, - 1, - palette, - 0, - 2 - }; if (newInfo != m_objInfo) { force = true; } @@ -223,27 +172,27 @@ void ObjView::updateTilesGB(bool force) { m_boundary = 1024; int i = 0; - m_ui.tile->setPalette(palette); - for (int y = 0; y < height / 8; ++y, ++i) { + m_ui.tile->setPalette(newInfo.paletteId); + for (int y = 0; y < newInfo.height; ++y, ++i) { unsigned t = tile + i; - const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[8 * t], t, palette); + const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[8 * t], t, newInfo.paletteId); if (data) { m_ui.tiles->setTile(i, data); } else if (force) { - m_ui.tiles->setTile(i, mTileCacheGetTile(tileCache, t, palette)); + m_ui.tiles->setTile(i, mTileCacheGetTile(tileCache, t, newInfo.paletteId)); } } - m_ui.x->setText(QString::number(obj->x)); - m_ui.y->setText(QString::number(obj->y)); - m_ui.w->setText(QString::number(width)); - m_ui.h->setText(QString::number(height)); + m_ui.x->setText(QString::number(newInfo.x)); + m_ui.y->setText(QString::number(newInfo.y)); + m_ui.w->setText(QString::number(8)); + m_ui.h->setText(QString::number(newInfo.height * 8)); m_ui.address->setText(tr("0x%0").arg(GB_BASE_OAM + m_objId * sizeof(*obj), 4, 16, QChar('0'))); - m_ui.priority->setText(QString::number(GBObjAttributesGetPriority(obj->attr))); - m_ui.flippedH->setChecked(GBObjAttributesIsXFlip(obj->attr)); - m_ui.flippedV->setChecked(GBObjAttributesIsYFlip(obj->attr)); - m_ui.enabled->setChecked(obj->y != 0 && obj->y < 160); + m_ui.priority->setText(QString::number(newInfo.priority)); + m_ui.flippedH->setChecked(newInfo.hflip); + m_ui.flippedV->setChecked(newInfo.vflip); + m_ui.enabled->setChecked(newInfo.enabled); m_ui.doubleSize->setChecked(false); m_ui.mosaic->setChecked(false); m_ui.transform->setText(tr("N/A")); @@ -251,51 +200,10 @@ void ObjView::updateTilesGB(bool force) { } #endif -#ifdef USE_PNG void ObjView::exportObj() { QString filename = GBAApp::app()->getSaveFileName(this, tr("Export sprite"), tr("Portable Network Graphics (*.png)")); - VFile* vf = VFileDevice::open(filename, O_WRONLY | O_CREAT | O_TRUNC); - if (!vf) { - LOG(QT, ERROR) << tr("Failed to open output PNG file: %1").arg(filename); - return; - } - CoreController::Interrupter interrupter(m_controller); - png_structp png = PNGWriteOpen(vf); - png_infop info = PNGWriteHeader8(png, m_objInfo.width * 8, m_objInfo.height * 8); - - mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, m_objInfo.paletteSet); - const color_t* rawPalette = mTileCacheGetPalette(tileCache, m_objInfo.paletteId); - unsigned colors = 1 << m_objInfo.bits; - uint32_t palette[256]; - - palette[0] = rawPalette[0]; - for (unsigned c = 1; c < colors && c < 256; ++c) { - palette[c] = rawPalette[c] | 0xFF000000; - } - PNGWritePalette(png, info, palette, colors); - - uint8_t* buffer = new uint8_t[m_objInfo.width * m_objInfo.height * 8 * 8]; - unsigned t = m_objInfo.tile; - for (int y = 0; y < m_objInfo.height; ++y) { - for (int x = 0; x < m_objInfo.width; ++x, ++t) { - compositeTile(static_cast(mTileCacheGetVRAM(tileCache, t)), reinterpret_cast(buffer), m_objInfo.width * 8, x * 8, y * 8, m_objInfo.bits); - } - t += m_objInfo.stride - m_objInfo.width; - } - PNGWritePixels8(png, m_objInfo.width * 8, m_objInfo.height * 8, m_objInfo.width * 8, static_cast(buffer)); - PNGWriteClose(png, info); - delete[] buffer; - vf->close(vf); -} -#endif - -bool ObjView::ObjInfo::operator!=(const ObjInfo& other) { - return other.tile != tile || - other.width != width || - other.height != height || - other.stride != stride || - other.paletteId != paletteId || - other.paletteSet != paletteSet; + QImage obj = compositeObj(m_objInfo); + obj.save(filename, "PNG"); } diff --git a/src/platform/qt/ObjView.h b/src/platform/qt/ObjView.h index 41677ca92..42cd3f65e 100644 --- a/src/platform/qt/ObjView.h +++ b/src/platform/qt/ObjView.h @@ -21,10 +21,8 @@ Q_OBJECT public: ObjView(std::shared_ptr controller, QWidget* parent = nullptr); -#ifdef USE_PNG public slots: void exportObj(); -#endif private slots: void selectObj(int); @@ -43,17 +41,7 @@ private: std::shared_ptr m_controller; mTileCacheEntry m_tileStatus[1024 * 32] = {}; // TODO: Correct size int m_objId = 0; - struct ObjInfo { - unsigned tile; - unsigned width; - unsigned height; - unsigned stride; - unsigned paletteId; - unsigned paletteSet; - unsigned bits; - - bool operator!=(const ObjInfo&); - } m_objInfo = {}; + ObjInfo m_objInfo = {}; int m_tileOffset; int m_boundary; diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index d87674d55..fc4d9a6e2 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -30,6 +30,7 @@ #include "DebuggerConsoleController.h" #include "Display.h" #include "CoreController.h" +#include "FrameView.h" #include "GBAApp.h" #include "GDBController.h" #include "GDBWindow.h" @@ -1437,7 +1438,7 @@ void Window::setupMenu(QMenuBar* menubar) { m_overrideView->recheck(); }, "tools"); - m_actions.addAction(tr("Game &Pak sensors..."), "sensorWindow", [this]() { + m_actions.addAction(tr("Game Pak sensors..."), "sensorWindow", [this]() { if (!m_sensorView) { m_sensorView = std::move(std::make_unique(&m_inputController)); if (m_controller) { @@ -1467,6 +1468,12 @@ void Window::setupMenu(QMenuBar* menubar) { addGameAction(tr("View &sprites..."), "spriteWindow", openControllerTView(), "tools"); addGameAction(tr("View &tiles..."), "tileWindow", openControllerTView(), "tools"); addGameAction(tr("View &map..."), "mapWindow", openControllerTView(), "tools"); + +#ifdef M_CORE_GBA + Action* frameWindow = addGameAction(tr("&Frame inspector..."), "frameWindow", openControllerTView(), "tools"); + m_platformActions.insert(PLATFORM_GBA, frameWindow); +#endif + addGameAction(tr("View memory..."), "memoryView", openControllerTView(), "tools"); addGameAction(tr("Search memory..."), "memorySearch", openControllerTView(), "tools"); From 306139a73c77131fd7d4d76bc3750ffd49717fd9 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 31 May 2019 16:27:02 -0700 Subject: [PATCH 07/50] Qt: Improve FrameView UI --- src/platform/qt/AssetView.cpp | 8 +-- src/platform/qt/FrameView.cpp | 93 +++++++++++++++++++++++++++-------- src/platform/qt/FrameView.h | 12 +++-- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/src/platform/qt/AssetView.cpp b/src/platform/qt/AssetView.cpp index eef257025..c13af9403 100644 --- a/src/platform/qt/AssetView.cpp +++ b/src/platform/qt/AssetView.cpp @@ -204,8 +204,8 @@ bool AssetView::lookupObjGBA(int id, struct ObjInfo* info) { GBAObjAttributesCGetPriority(obj->c), GBAObjAttributesBGetX(obj->b), GBAObjAttributesAGetY(obj->a), - GBAObjAttributesBIsHFlip(obj->b), - GBAObjAttributesBIsVFlip(obj->b), + bool(GBAObjAttributesBIsHFlip(obj->b)), + bool(GBAObjAttributesBIsVFlip(obj->b)), }; GBARegisterDISPCNT dispcnt = gba->memory.io[0]; // FIXME: Register name can't be imported due to namespacing issues if (!GBARegisterDISPCNTIsObjCharacterMapping(dispcnt)) { @@ -255,8 +255,8 @@ bool AssetView::lookupObjGB(int id, struct ObjInfo* info) { GBObjAttributesGetPriority(obj->attr), obj->x, obj->y, - GBObjAttributesIsXFlip(obj->attr), - GBObjAttributesIsYFlip(obj->attr), + bool(GBObjAttributesIsXFlip(obj->attr)), + bool(GBObjAttributesIsYFlip(obj->attr)), }; *info = newInfo; return true; diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp index 121bd9171..989e0f775 100644 --- a/src/platform/qt/FrameView.cpp +++ b/src/platform/qt/FrameView.cpp @@ -38,6 +38,7 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent }); m_glowTimer.start(); + m_ui.renderedView->installEventFilter(this); m_ui.compositedView->installEventFilter(this); connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) { @@ -58,10 +59,19 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent } invalidateQueue(); }); + connect(m_ui.magnification, static_cast(&QSpinBox::valueChanged), this, [this]() { + invalidateQueue(); + + QPixmap rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value()); + m_ui.renderedView->setPixmap(rendered); + }); } -void FrameView::selectLayer(const QPointF& coord) { - for (const Layer& layer : m_queue) { +bool FrameView::lookupLayer(const QPointF& coord, Layer*& out) { + for (Layer& layer : m_queue) { + if (!layer.enabled || m_disabled.contains(layer.id)) { + continue; + } QPointF location = layer.location; QSizeF layerDims(layer.image.width(), layer.image.height()); QRegion region; @@ -82,11 +92,33 @@ void FrameView::selectLayer(const QPointF& coord) { } if (region.contains(QPoint(coord.x(), coord.y()))) { - m_active = layer.id; - m_glowFrame = 0; - break; + out = &layer; + return true; } } + return false; +} + +void FrameView::selectLayer(const QPointF& coord) { + Layer* layer; + if (!lookupLayer(coord, layer)) { + return; + } + if (layer->id == m_active) { + m_active = {}; + } else { + m_active = layer->id; + } + m_glowFrame = 0; +} + +void FrameView::disableLayer(const QPointF& coord) { + Layer* layer; + if (!lookupLayer(coord, layer)) { + return; + } + layer->enabled = false; + m_disabled.insert(layer->id); } void FrameView::updateTilesGBA(bool force) { @@ -99,6 +131,7 @@ void FrameView::updateTilesGBA(bool force) { updateRendered(); uint16_t* io = static_cast(m_controller->thread()->core->board)->memory.io; + QRgb backdrop = M_RGB5_TO_RGB8(static_cast(m_controller->thread()->core->board)->video.palette[0]); int mode = GBARegisterDISPCNTGetMode(io[REG_DISPCNT >> 1]); std::array enabled{ @@ -124,7 +157,7 @@ void FrameView::updateTilesGBA(bool force) { } m_queue.append({ { LayerId::SPRITE, sprite }, - !m_disabled.contains({ LayerId::SPRITE, sprite}), + !m_disabled.contains({ LayerId::SPRITE, sprite }), QPixmap::fromImage(obj), {}, offset, false }); @@ -150,7 +183,7 @@ void FrameView::updateTilesGBA(bool force) { }; m_queue.append({ { LayerId::BACKGROUND, bg }, - !m_disabled.contains({ LayerId::BACKGROUND, bg}), + !m_disabled.contains({ LayerId::BACKGROUND, bg }), QPixmap::fromImage(compositeMap(bg, m_mapStatus[bg])), {}, offset, true }); @@ -161,6 +194,15 @@ void FrameView::updateTilesGBA(bool force) { } } } + QImage backdropImage(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS), QImage::Format_Mono); + backdropImage.fill(1); + backdropImage.setColorTable({backdrop, backdrop | 0xFF000000 }); + m_queue.append({ + { LayerId::BACKDROP }, + !m_disabled.contains({ LayerId::BACKDROP }), + QPixmap::fromImage(backdropImage), + {}, {0, 0}, false + }); } invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS)); } @@ -178,19 +220,18 @@ void FrameView::updateTilesGB(bool force) { } void FrameView::invalidateQueue(const QSize& dims) { - QSize realDims = dims; - if (!dims.isValid()) { - realDims = m_composited.size() / m_ui.magnification->value(); + if (dims.isValid()) { + m_dims = dims; } bool blockSignals = m_ui.queue->blockSignals(true); - QPixmap composited(realDims); + QPixmap composited(m_dims); QPainter painter(&composited); QPalette palette; QColor activeColor = palette.color(QPalette::HighlightedText); activeColor.setAlpha(sin(m_glowFrame * M_PI / 60) * 16 + 96); - QRectF rect(0, 0, realDims.width(), realDims.height()); + QRectF rect(0, 0, m_dims.width(), m_dims.height()); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(rect, QColor(0, 0, 0, 0)); @@ -264,7 +305,7 @@ void FrameView::invalidateQueue(const QSize& dims) { } m_ui.queue->blockSignals(blockSignals); - m_composited = composited.scaled(realDims * m_ui.magnification->value()); + m_composited = composited.scaled(m_dims * m_ui.magnification->value()); m_ui.compositedView->setPixmap(m_composited); } @@ -273,18 +314,25 @@ void FrameView::updateRendered() { return; } m_rendered.convertFromImage(m_controller->getPixels()); - m_rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value()); - m_ui.renderedView->setPixmap(m_rendered); + QPixmap rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value()); + m_ui.renderedView->setPixmap(rendered); } bool FrameView::eventFilter(QObject* obj, QEvent* event) { - if (event->type() != QEvent::MouseButtonPress) { - return false; + QPointF pos; + switch (event->type()) { + case QEvent::MouseButtonPress: + pos = static_cast(event)->localPos(); + pos /= m_ui.magnification->value(); + selectLayer(pos); + return true; + case QEvent::MouseButtonDblClick: + pos = static_cast(event)->localPos(); + pos /= m_ui.magnification->value(); + disableLayer(pos); + return true; } - QPointF pos = static_cast(event)->localPos(); - pos /= m_ui.magnification->value(); - selectLayer(pos); - return true; + return false; } QString FrameView::LayerId::readable() const { @@ -301,6 +349,9 @@ QString FrameView::LayerId::readable() const { case SPRITE: typeStr = tr("Sprite"); break; + case BACKDROP: + typeStr = tr("Backdrop"); + break; } if (index < 0) { return typeStr; diff --git a/src/platform/qt/FrameView.h b/src/platform/qt/FrameView.h index 626dfbd62..4fd1bea09 100644 --- a/src/platform/qt/FrameView.h +++ b/src/platform/qt/FrameView.h @@ -30,6 +30,7 @@ public: public slots: void selectLayer(const QPointF& coord); + void disableLayer(const QPointF& coord); protected: #ifdef M_CORE_GBA @@ -41,13 +42,18 @@ protected: bool eventFilter(QObject* obj, QEvent* event) override; +private slots: + void invalidateQueue(const QSize& dims = QSize()); + void updateRendered(); + private: struct LayerId { enum { NONE = 0, BACKGROUND, WINDOW, - SPRITE + SPRITE, + BACKDROP } type = NONE; int index = -1; @@ -65,8 +71,7 @@ private: bool repeats; }; - void invalidateQueue(const QSize& dims = QSize()); - void updateRendered(); + bool lookupLayer(const QPointF& coord, Layer*&); Ui::FrameView m_ui; @@ -75,6 +80,7 @@ private: int m_glowFrame; QTimer m_glowTimer; + QSize m_dims; QList m_queue; QSet m_disabled; QPixmap m_composited; From b230b6e0f64dad1f431a38b7c93f2e6ba89757c3 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 31 May 2019 16:30:11 -0700 Subject: [PATCH 08/50] Qt: Clang buildfixes --- src/platform/qt/FrameView.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp index 989e0f775..22bac7fce 100644 --- a/src/platform/qt/FrameView.cpp +++ b/src/platform/qt/FrameView.cpp @@ -10,6 +10,7 @@ #include #include +#include #include "CoreController.h" @@ -135,10 +136,10 @@ void FrameView::updateTilesGBA(bool force) { int mode = GBARegisterDISPCNTGetMode(io[REG_DISPCNT >> 1]); std::array enabled{ - GBARegisterDISPCNTIsBg0Enable(io[REG_DISPCNT >> 1]), - GBARegisterDISPCNTIsBg1Enable(io[REG_DISPCNT >> 1]), - GBARegisterDISPCNTIsBg2Enable(io[REG_DISPCNT >> 1]), - GBARegisterDISPCNTIsBg3Enable(io[REG_DISPCNT >> 1]), + bool(GBARegisterDISPCNTIsBg0Enable(io[REG_DISPCNT >> 1])), + bool(GBARegisterDISPCNTIsBg1Enable(io[REG_DISPCNT >> 1])), + bool(GBARegisterDISPCNTIsBg2Enable(io[REG_DISPCNT >> 1])), + bool(GBARegisterDISPCNTIsBg3Enable(io[REG_DISPCNT >> 1])), }; for (int priority = 0; priority < 4; ++priority) { From 3cce95b287c589bce3b371a1677c01afd3adf0ef Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 11:07:49 -0700 Subject: [PATCH 09/50] Core: Video log enhancements --- include/mgba/feature/video-logger.h | 1 + src/feature/video-logger.c | 47 +++++++++++++++++++---------- src/gb/core.c | 8 +++-- src/gba/core.c | 8 +++-- src/platform/qt/CoreController.cpp | 21 ++++++++++--- src/platform/qt/CoreController.h | 5 +-- 6 files changed, 62 insertions(+), 28 deletions(-) diff --git a/include/mgba/feature/video-logger.h b/include/mgba/feature/video-logger.h index d33e83155..2acd9c87f 100644 --- a/include/mgba/feature/video-logger.h +++ b/include/mgba/feature/video-logger.h @@ -104,6 +104,7 @@ void mVideoLoggerAttachChannel(struct mVideoLogger* logger, struct mVideoLogCont struct mCore; struct mVideoLogContext* mVideoLogContextCreate(struct mCore* core); +void mVideoLogContextSetCompression(struct mVideoLogContext*, bool enable); void mVideoLogContextSetOutput(struct mVideoLogContext*, struct VFile*); void mVideoLogContextWriteHeader(struct mVideoLogContext*, struct mCore* core); diff --git a/src/feature/video-logger.c b/src/feature/video-logger.c index be1c7b3f7..9e208f9f1 100644 --- a/src/feature/video-logger.c +++ b/src/feature/video-logger.c @@ -94,6 +94,7 @@ struct mVideoLogContext { struct mVideoLogChannel channels[mVL_MAX_CHANNELS]; bool write; + bool compression; uint32_t activeChannel; struct VFile* backing; }; @@ -465,6 +466,12 @@ struct mVideoLogContext* mVideoLogContextCreate(struct mCore* core) { context->initialStateSize = 0; context->initialState = NULL; +#ifdef USE_ZLIB + context->compression = true; +#else + context->compression = false; +#endif + if (core) { context->initialStateSize = core->stateSize(core); context->initialState = anonymousMemoryMap(context->initialStateSize); @@ -482,6 +489,10 @@ void mVideoLogContextSetOutput(struct mVideoLogContext* context, struct VFile* v vf->seek(vf, 0, SEEK_SET); } +void mVideoLogContextSetCompression(struct mVideoLogContext* context, bool compression) { + context->compression = compression; +} + void mVideoLogContextWriteHeader(struct mVideoLogContext* context, struct mCore* core) { struct mVideoLogHeader header = { { 0 } }; memcpy(header.magic, mVL_MAGIC, sizeof(header.magic)); @@ -499,21 +510,24 @@ void mVideoLogContextWriteHeader(struct mVideoLogContext* context, struct mCore* struct mVLBlockHeader chheader = { 0 }; STORE_32LE(mVL_BLOCK_INITIAL_STATE, 0, &chheader.blockType); #ifdef USE_ZLIB - STORE_32LE(mVL_FLAG_BLOCK_COMPRESSED, 0, &chheader.flags); + if (context->compression) { + STORE_32LE(mVL_FLAG_BLOCK_COMPRESSED, 0, &chheader.flags); - struct VFile* vfm = VFileMemChunk(NULL, 0); - struct VFile* src = VFileFromConstMemory(context->initialState, context->initialStateSize); - _compress(vfm, src); - src->close(src); - STORE_32LE(vfm->size(vfm), 0, &chheader.length); - context->backing->write(context->backing, &chheader, sizeof(chheader)); - _copyVf(context->backing, vfm); - vfm->close(vfm); -#else - STORE_32LE(context->initialStateSize, 0, &chheader.length); - context->backing->write(context->backing, &chheader, sizeof(chheader)); - context->backing->write(context->backing, context->initialState, context->initialStateSize); + struct VFile* vfm = VFileMemChunk(NULL, 0); + struct VFile* src = VFileFromConstMemory(context->initialState, context->initialStateSize); + _compress(vfm, src); + src->close(src); + STORE_32LE(vfm->size(vfm), 0, &chheader.length); + context->backing->write(context->backing, &chheader, sizeof(chheader)); + _copyVf(context->backing, vfm); + vfm->close(vfm); + } else #endif + { + STORE_32LE(context->initialStateSize, 0, &chheader.length); + context->backing->write(context->backing, &chheader, sizeof(chheader)); + context->backing->write(context->backing, context->initialState, context->initialStateSize); + } } size_t i; @@ -647,9 +661,10 @@ static void _flushBufferCompressed(struct mVideoLogContext* context) { static void _flushBuffer(struct mVideoLogContext* context) { #ifdef USE_ZLIB - // TODO: Make optional - _flushBufferCompressed(context); - return; + if (context->compression) { + _flushBufferCompressed(context); + return; + } #endif struct CircleBuffer* buffer = &context->channels[context->activeChannel].buffer; diff --git a/src/gb/core.c b/src/gb/core.c index 264f29bf1..9c00519a0 100644 --- a/src/gb/core.c +++ b/src/gb/core.c @@ -879,9 +879,11 @@ static void _GBCoreStartVideoLog(struct mCore* core, struct mVideoLogContext* co static void _GBCoreEndVideoLog(struct mCore* core) { struct GBCore* gbcore = (struct GBCore*) core; struct GB* gb = core->board; - GBVideoProxyRendererUnshim(&gb->video, &gbcore->proxyRenderer); - free(gbcore->proxyRenderer.logger); - gbcore->proxyRenderer.logger = NULL; + if (gbcore->proxyRenderer.logger) { + GBVideoProxyRendererUnshim(&gb->video, &gbcore->proxyRenderer); + free(gbcore->proxyRenderer.logger); + gbcore->proxyRenderer.logger = NULL; + } } #endif diff --git a/src/gba/core.c b/src/gba/core.c index 53925cc52..bf3a7ecfd 100644 --- a/src/gba/core.c +++ b/src/gba/core.c @@ -990,9 +990,11 @@ static void _GBACoreStartVideoLog(struct mCore* core, struct mVideoLogContext* c static void _GBACoreEndVideoLog(struct mCore* core) { struct GBACore* gbacore = (struct GBACore*) core; struct GBA* gba = core->board; - GBAVideoProxyRendererUnshim(&gba->video, &gbacore->proxyRenderer); - free(gbacore->proxyRenderer.logger); - gbacore->proxyRenderer.logger = NULL; + if (gbacore->proxyRenderer.logger) { + GBAVideoProxyRendererUnshim(&gba->video, &gbacore->proxyRenderer); + free(gbacore->proxyRenderer.logger); + gbacore->proxyRenderer.logger = NULL; + } } #endif diff --git a/src/platform/qt/CoreController.cpp b/src/platform/qt/CoreController.cpp index e1d4bc7ea..fb59baf5c 100644 --- a/src/platform/qt/CoreController.cpp +++ b/src/platform/qt/CoreController.cpp @@ -809,26 +809,39 @@ void CoreController::clearOverride() { m_override.reset(); } -void CoreController::startVideoLog(const QString& path) { +void CoreController::startVideoLog(const QString& path, bool compression) { if (m_vl) { return; } + VFile* vf = VFileDevice::open(path, O_WRONLY | O_CREAT | O_TRUNC); + if (!vf) { + return; + } + startVideoLog(vf); +} + +void CoreController::startVideoLog(VFile* vf, bool compression) { + if (m_vl || !vf) { + return; + } + Interrupter interrupter(this); m_vl = mVideoLogContextCreate(m_threadContext.core); - m_vlVf = VFileDevice::open(path, O_WRONLY | O_CREAT | O_TRUNC); + m_vlVf = vf; mVideoLogContextSetOutput(m_vl, m_vlVf); + mVideoLogContextSetCompression(m_vl, compression); mVideoLogContextWriteHeader(m_vl, m_threadContext.core); } -void CoreController::endVideoLog() { +void CoreController::endVideoLog(bool closeVf) { if (!m_vl) { return; } Interrupter interrupter(this); mVideoLogContextDestroy(m_threadContext.core, m_vl); - if (m_vlVf) { + if (m_vlVf && closeVf) { m_vlVf->close(m_vlVf); m_vlVf = nullptr; } diff --git a/src/platform/qt/CoreController.h b/src/platform/qt/CoreController.h index ac1e327ba..b6f9d7ecf 100644 --- a/src/platform/qt/CoreController.h +++ b/src/platform/qt/CoreController.h @@ -161,8 +161,9 @@ public slots: void clearOverride(); - void startVideoLog(const QString& path); - void endVideoLog(); + void startVideoLog(const QString& path, bool compression = true); + void startVideoLog(VFile* vf, bool compression = true); + void endVideoLog(bool closeVf = true); void setFramebufferHandle(int fb); From 5436d2576ffd0dac6254963e4a8d656e445ef078 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 11:08:28 -0700 Subject: [PATCH 10/50] Core: Fix crashes if core directories aren't set --- CHANGES | 1 + src/core/core.c | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGES b/CHANGES index 8b449be32..c49cc0538 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,7 @@ Other fixes: - Core: Improved lockstep driver reliability (Le Hoang Quyen) - Switch: Fix threading-related crash on second launch - Qt: Fix FPS target maxing out at 59.727 (fixes mgba.io/i/1421) + - Core: Fix crashes if core directories aren't set Misc: - GBA Savedata: EEPROM performance fixes - GBA Savedata: Automatically map 1Mbit Flash files as 1Mbit Flash diff --git a/src/core/core.c b/src/core/core.c index 57c2dd811..af3ff68a8 100644 --- a/src/core/core.c +++ b/src/core/core.c @@ -157,10 +157,16 @@ bool mCorePreloadFile(struct mCore* core, const char* path) { } bool mCoreAutoloadSave(struct mCore* core) { + if (!core->dirs.save) { + return false; + } return core->loadSave(core, mDirectorySetOpenSuffix(&core->dirs, core->dirs.save, ".sav", O_CREAT | O_RDWR)); } bool mCoreAutoloadPatch(struct mCore* core) { + if (!core->dirs.patch) { + return false; + } return core->loadPatch(core, mDirectorySetOpenSuffix(&core->dirs, core->dirs.patch, ".ups", O_RDONLY)) || core->loadPatch(core, mDirectorySetOpenSuffix(&core->dirs, core->dirs.patch, ".ips", O_RDONLY)) || core->loadPatch(core, mDirectorySetOpenSuffix(&core->dirs, core->dirs.patch, ".bps", O_RDONLY)); @@ -217,6 +223,9 @@ bool mCoreLoadState(struct mCore* core, int slot, int flags) { } struct VFile* mCoreGetState(struct mCore* core, int slot, bool write) { + if (!core->dirs.state) { + return NULL; + } char name[PATH_MAX + 14]; // Quash warning snprintf(name, sizeof(name), "%s.ss%i", core->dirs.baseName, slot); return core->dirs.state->openFile(core->dirs.state, name, write ? (O_CREAT | O_TRUNC | O_RDWR) : O_RDONLY); From 4420054c1a3b13e406e28aaa7ab8f44ab3509438 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 11:08:49 -0700 Subject: [PATCH 11/50] Qt: Expose frame actions --- src/platform/qt/CoreController.cpp | 24 +++++++++++++++--------- src/platform/qt/CoreController.h | 5 ++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/platform/qt/CoreController.cpp b/src/platform/qt/CoreController.cpp index fb59baf5c..4608a1b45 100644 --- a/src/platform/qt/CoreController.cpp +++ b/src/platform/qt/CoreController.cpp @@ -203,10 +203,10 @@ CoreController::~CoreController() { } const color_t* CoreController::drawContext() { - QMutexLocker locker(&m_mutex); if (m_hwaccel) { return nullptr; } + QMutexLocker locker(&m_bufferMutex); return reinterpret_cast(m_completeBuffer.constData()); } @@ -401,8 +401,7 @@ void CoreController::setPaused(bool paused) { return; } if (paused) { - QMutexLocker locker(&m_mutex); - m_frameActions.append([this]() { + addFrameAction([this]() { mCoreThreadPauseFromThread(&m_threadContext); }); } else { @@ -411,13 +410,17 @@ void CoreController::setPaused(bool paused) { } void CoreController::frameAdvance() { - QMutexLocker locker(&m_mutex); - m_frameActions.append([this]() { + addFrameAction([this]() { mCoreThreadPauseFromThread(&m_threadContext); }); setPaused(false); } +void CoreController::addFrameAction(std::function action) { + QMutexLocker locker(&m_actionMutex); + m_frameActions.append(action); +} + void CoreController::setSync(bool sync) { if (sync) { m_threadContext.impl->sync.audioWait = m_audioSync; @@ -880,9 +883,9 @@ int CoreController::updateAutofire() { } void CoreController::finishFrame() { - QMutexLocker locker(&m_mutex); if (!m_hwaccel) { - memcpy(m_completeBuffer.data(), m_activeBuffer->constData(), m_activeBuffer->size()); + QMutexLocker locker(&m_bufferMutex); + memcpy(m_completeBuffer.data(), m_activeBuffer->constData(), m_activeBuffer->size()); // TODO: Generalize this to triple buffering? m_activeBuffer = &m_buffers[0]; @@ -893,10 +896,13 @@ void CoreController::finishFrame() { memcpy(m_activeBuffer->data(), m_completeBuffer.constData(), m_activeBuffer->size()); m_threadContext.core->setVideoBuffer(m_threadContext.core, reinterpret_cast(m_activeBuffer->data()), screenDimensions().width()); } - for (auto& action : m_frameActions) { + + QMutexLocker locker(&m_actionMutex); + QList> frameActions(m_frameActions); + m_frameActions.clear(); + for (auto& action : frameActions) { action(); } - m_frameActions.clear(); updateKeys(); QMetaObject::invokeMethod(this, "frameAvailable"); diff --git a/src/platform/qt/CoreController.h b/src/platform/qt/CoreController.h index b6f9d7ecf..5463bf76b 100644 --- a/src/platform/qt/CoreController.h +++ b/src/platform/qt/CoreController.h @@ -102,6 +102,8 @@ public: bool audioSync() const { return m_audioSync; } bool videoSync() const { return m_videoSync; } + void addFrameAction(std::function callback); + public slots: void start(); void stop(); @@ -209,7 +211,8 @@ private: QList> m_resetActions; QList> m_frameActions; - QMutex m_mutex; + QMutex m_actionMutex{QMutex::Recursive}; + QMutex m_bufferMutex; int m_activeKeys = 0; bool m_autofire[32] = {}; From 570f2c5f380464ae7f6d468242151d520cb00c15 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 14:28:39 -0700 Subject: [PATCH 12/50] Core: Video packet injection --- include/mgba/feature/video-logger.h | 12 ++++ src/feature/video-logger.c | 104 ++++++++++++++++++++++++---- 2 files changed, 101 insertions(+), 15 deletions(-) diff --git a/include/mgba/feature/video-logger.h b/include/mgba/feature/video-logger.h index 2acd9c87f..b466e4b6c 100644 --- a/include/mgba/feature/video-logger.h +++ b/include/mgba/feature/video-logger.h @@ -35,6 +35,11 @@ enum mVideoLoggerEvent { LOGGER_EVENT_GET_PIXELS, }; +enum mVideoLoggerInjectionPoint { + LOGGER_INJECTION_IMMEDIATE = 0, + LOGGER_INJECTION_FIRST_SCANLINE, +}; + struct mVideoLoggerDirtyInfo { enum mVideoLoggerDirtyType type; uint32_t address; @@ -97,6 +102,7 @@ void mVideoLoggerRendererFlush(struct mVideoLogger* logger); void mVideoLoggerRendererFinishFrame(struct mVideoLogger* logger); bool mVideoLoggerRendererRun(struct mVideoLogger* logger, bool block); +bool mVideoLoggerRendererRunInjected(struct mVideoLogger* logger); struct mVideoLogContext; void mVideoLoggerAttachChannel(struct mVideoLogger* logger, struct mVideoLogContext* context, size_t channelId); @@ -116,6 +122,12 @@ void* mVideoLogContextInitialState(struct mVideoLogContext*, size_t* size); int mVideoLoggerAddChannel(struct mVideoLogContext*); +void mVideoLoggerInjectionPoint(struct mVideoLogger* logger, enum mVideoLoggerInjectionPoint); +void mVideoLoggerIgnoreAfterInjection(struct mVideoLogger* logger, uint32_t mask); +void mVideoLoggerInjectVideoRegister(struct mVideoLogger* logger, uint32_t address, uint16_t value); +void mVideoLoggerInjectPalette(struct mVideoLogger* logger, uint32_t address, uint16_t value); +void mVideoLoggerInjectOAM(struct mVideoLogger* logger, uint32_t address, uint16_t value); + struct mCore* mVideoLogCoreFind(struct VFile*); CXX_GUARD_END diff --git a/src/feature/video-logger.c b/src/feature/video-logger.c index 9e208f9f1..a2884a107 100644 --- a/src/feature/video-logger.c +++ b/src/feature/video-logger.c @@ -84,6 +84,11 @@ struct mVideoLogChannel { z_stream inflateStream; #endif + bool injecting; + enum mVideoLoggerInjectionPoint injectionPoint; + uint32_t ignorePackets; + + struct CircleBuffer injectedBuffer; struct CircleBuffer buffer; }; @@ -286,14 +291,28 @@ void mVideoLoggerWriteBuffer(struct mVideoLogger* logger, uint32_t bufferId, uin } bool mVideoLoggerRendererRun(struct mVideoLogger* logger, bool block) { + struct mVideoLogChannel* channel = logger->dataContext; + uint32_t ignorePackets = 0; + if (channel->injectionPoint == LOGGER_INJECTION_IMMEDIATE && !channel->injecting) { + mVideoLoggerRendererRunInjected(logger); + ignorePackets = channel->ignorePackets; + } struct mVideoLoggerDirtyInfo item = {0}; while (logger->readData(logger, &item, sizeof(item), block)) { + if (ignorePackets & (1 << item.type)) { + continue; + } switch (item.type) { + case DIRTY_SCANLINE: + if (channel->injectionPoint == LOGGER_INJECTION_FIRST_SCANLINE && !channel->injecting && item.address == 0) { + mVideoLoggerRendererRunInjected(logger); + ignorePackets = channel->ignorePackets; + } + // Fall through case DIRTY_REGISTER: case DIRTY_PALETTE: case DIRTY_OAM: case DIRTY_VRAM: - case DIRTY_SCANLINE: case DIRTY_FLUSH: case DIRTY_FRAME: case DIRTY_RANGE: @@ -309,15 +328,34 @@ bool mVideoLoggerRendererRun(struct mVideoLogger* logger, bool block) { return !block; } +bool mVideoLoggerRendererRunInjected(struct mVideoLogger* logger) { + struct mVideoLogChannel* channel = logger->dataContext; + channel->injecting = true; + bool res = mVideoLoggerRendererRun(logger, false); + channel->injecting = false; + return res; +} + +void mVideoLoggerInjectionPoint(struct mVideoLogger* logger, enum mVideoLoggerInjectionPoint injectionPoint) { + struct mVideoLogChannel* channel = logger->dataContext; + channel->injectionPoint = injectionPoint; +} + +void mVideoLoggerIgnoreAfterInjection(struct mVideoLogger* logger, uint32_t mask) { + struct mVideoLogChannel* channel = logger->dataContext; + channel->ignorePackets = mask; +} + static bool _writeData(struct mVideoLogger* logger, const void* data, size_t length) { struct mVideoLogChannel* channel = logger->dataContext; return mVideoLoggerWriteChannel(channel, data, length) == (ssize_t) length; } static bool _writeNull(struct mVideoLogger* logger, const void* data, size_t length) { - UNUSED(logger); - UNUSED(data); - UNUSED(length); + struct mVideoLogChannel* channel = logger->dataContext; + if (channel->injecting) { + return mVideoLoggerWriteChannel(channel, data, length) == (ssize_t) length; + } return false; } @@ -623,6 +661,7 @@ bool mVideoLogContextLoad(struct mVideoLogContext* context, struct VFile* vf) { size_t i; for (i = 0; i < context->nChannels; ++i) { + CircleBufferInit(&context->channels[i].injectedBuffer, BUFFER_BASE_SIZE); CircleBufferInit(&context->channels[i].buffer, BUFFER_BASE_SIZE); context->channels[i].bufferRemaining = 0; context->channels[i].currentPointer = pointer; @@ -703,6 +742,7 @@ void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* contex size_t i; for (i = 0; i < context->nChannels; ++i) { + CircleBufferDeinit(&context->channels[i].injectedBuffer); CircleBufferDeinit(&context->channels[i].buffer); #ifdef USE_ZLIB if (context->channels[i].inflating) { @@ -733,6 +773,7 @@ void mVideoLogContextRewind(struct mVideoLogContext* context, struct mCore* core size_t i; for (i = 0; i < context->nChannels; ++i) { + CircleBufferClear(&context->channels[i].injectedBuffer); CircleBufferClear(&context->channels[i].buffer); context->channels[i].bufferRemaining = 0; context->channels[i].currentPointer = pointer; @@ -759,10 +800,35 @@ int mVideoLoggerAddChannel(struct mVideoLogContext* context) { int chid = context->nChannels; ++context->nChannels; context->channels[chid].p = context; + CircleBufferInit(&context->channels[chid].injectedBuffer, BUFFER_BASE_SIZE); CircleBufferInit(&context->channels[chid].buffer, BUFFER_BASE_SIZE); + context->channels[chid].injecting = false; + context->channels[chid].injectionPoint = LOGGER_INJECTION_IMMEDIATE; + context->channels[chid].ignorePackets = 0; return chid; } +void mVideoLoggerInjectVideoRegister(struct mVideoLogger* logger, uint32_t address, uint16_t value) { + struct mVideoLogChannel* channel = logger->dataContext; + channel->injecting = true; + mVideoLoggerRendererWriteVideoRegister(logger, address, value); + channel->injecting = false; +} + +void mVideoLoggerInjectPalette(struct mVideoLogger* logger, uint32_t address, uint16_t value) { + struct mVideoLogChannel* channel = logger->dataContext; + channel->injecting = true; + mVideoLoggerRendererWritePalette(logger, address, value); + channel->injecting = false; +} + +void mVideoLoggerInjectOAM(struct mVideoLogger* logger, uint32_t address, uint16_t value) { + struct mVideoLogChannel* channel = logger->dataContext; + channel->injecting = true; + mVideoLoggerRendererWriteOAM(logger, address, value); + channel->injecting = false; +} + #ifdef USE_ZLIB static size_t _readBufferCompressed(struct VFile* vf, struct mVideoLogChannel* channel, size_t length) { uint8_t fbuffer[0x400]; @@ -915,12 +981,16 @@ static ssize_t mVideoLoggerReadChannel(struct mVideoLogChannel* channel, void* d if (channelId >= mVL_MAX_CHANNELS) { return 0; } - if (CircleBufferSize(&channel->buffer) >= length) { - return CircleBufferRead(&channel->buffer, data, length); + struct CircleBuffer* buffer = &channel->buffer; + if (channel->injecting) { + buffer = &channel->injectedBuffer; + } + if (CircleBufferSize(buffer) >= length) { + return CircleBufferRead(buffer, data, length); } ssize_t size = 0; - if (CircleBufferSize(&channel->buffer)) { - size = CircleBufferRead(&channel->buffer, data, CircleBufferSize(&channel->buffer)); + if (CircleBufferSize(buffer)) { + size = CircleBufferRead(buffer, data, CircleBufferSize(buffer)); if (size <= 0) { return size; } @@ -930,7 +1000,7 @@ static ssize_t mVideoLoggerReadChannel(struct mVideoLogChannel* channel, void* d if (!_fillBuffer(context, channelId, BUFFER_BASE_SIZE)) { return size; } - size += CircleBufferRead(&channel->buffer, data, length); + size += CircleBufferRead(buffer, data, length); return size; } @@ -944,16 +1014,20 @@ static ssize_t mVideoLoggerWriteChannel(struct mVideoLogChannel* channel, const _flushBuffer(context); context->activeChannel = channelId; } - if (CircleBufferCapacity(&channel->buffer) - CircleBufferSize(&channel->buffer) < length) { + struct CircleBuffer* buffer = &channel->buffer; + if (channel->injecting) { + buffer = &channel->injectedBuffer; + } + if (CircleBufferCapacity(buffer) - CircleBufferSize(buffer) < length) { _flushBuffer(context); - if (CircleBufferCapacity(&channel->buffer) < length) { - CircleBufferDeinit(&channel->buffer); - CircleBufferInit(&channel->buffer, toPow2(length << 1)); + if (CircleBufferCapacity(buffer) < length) { + CircleBufferDeinit(buffer); + CircleBufferInit(buffer, toPow2(length << 1)); } } - ssize_t read = CircleBufferWrite(&channel->buffer, data, length); - if (CircleBufferCapacity(&channel->buffer) == CircleBufferSize(&channel->buffer)) { + ssize_t read = CircleBufferWrite(buffer, data, length); + if (CircleBufferCapacity(buffer) == CircleBufferSize(buffer)) { _flushBuffer(context); } return read; From f41f3a847893450843f33e6c45c7025fdd8e1fc9 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 14:30:22 -0700 Subject: [PATCH 13/50] GBA Video: Support highlighting layers --- include/mgba/internal/gba/renderers/common.h | 1 + .../internal/gba/renderers/video-software.h | 5 +++ include/mgba/internal/gba/video.h | 6 ++++ src/gba/extra/proxy.c | 6 ++++ src/gba/renderers/common.c | 1 + src/gba/renderers/software-mode0.c | 6 ++++ src/gba/renderers/software-obj.c | 8 ++++- src/gba/renderers/software-private.h | 14 +++++++- src/gba/renderers/video-software.c | 33 ++++++++++++++++++- 9 files changed, 77 insertions(+), 3 deletions(-) diff --git a/include/mgba/internal/gba/renderers/common.h b/include/mgba/internal/gba/renderers/common.h index e90b1f350..ba368edc9 100644 --- a/include/mgba/internal/gba/renderers/common.h +++ b/include/mgba/internal/gba/renderers/common.h @@ -16,6 +16,7 @@ struct GBAVideoRendererSprite { struct GBAObj obj; int16_t y; int16_t endY; + int8_t index; }; int GBAVideoRendererCleanOAM(struct GBAObj* oam, struct GBAVideoRendererSprite* sprites, int offsetY); diff --git a/include/mgba/internal/gba/renderers/video-software.h b/include/mgba/internal/gba/renderers/video-software.h index 47b8ddfb6..b15dc47b4 100644 --- a/include/mgba/internal/gba/renderers/video-software.h +++ b/include/mgba/internal/gba/renderers/video-software.h @@ -42,6 +42,7 @@ struct GBAVideoSoftwareBackground { uint16_t mapCache[64]; int32_t offsetX; int32_t offsetY; + bool highlight; }; enum { @@ -105,6 +106,8 @@ struct GBAVideoSoftwareRenderer { enum GBAVideoBlendEffect blendEffect; color_t normalPalette[512]; color_t variantPalette[512]; + color_t highlightPalette[512]; + color_t highlightVariantPalette[512]; uint16_t blda; uint16_t bldb; @@ -144,6 +147,8 @@ struct GBAVideoSoftwareRenderer { int start; int end; + + uint8_t lastHighlightAmount; }; void GBAVideoSoftwareRendererCreate(struct GBAVideoSoftwareRenderer* renderer); diff --git a/include/mgba/internal/gba/video.h b/include/mgba/internal/gba/video.h index ba8048ffd..908954aaf 100644 --- a/include/mgba/internal/gba/video.h +++ b/include/mgba/internal/gba/video.h @@ -12,6 +12,7 @@ CXX_GUARD_START #include #include +#include mLOG_DECLARE_CATEGORY(GBA_VIDEO); @@ -192,6 +193,11 @@ struct GBAVideoRenderer { bool disableBG[4]; bool disableOBJ; + + bool highlightBG[4]; + bool highlightOBJ[128]; + color_t highlightColor; + uint8_t highlightAmount; }; struct GBAVideo { diff --git a/src/gba/extra/proxy.c b/src/gba/extra/proxy.c index 52a0047b4..6121f4f99 100644 --- a/src/gba/extra/proxy.c +++ b/src/gba/extra/proxy.c @@ -191,6 +191,12 @@ static bool _parsePacket(struct mVideoLogger* logger, const struct mVideoLoggerD proxyRenderer->backend->disableBG[2] = proxyRenderer->d.disableBG[2]; proxyRenderer->backend->disableBG[3] = proxyRenderer->d.disableBG[3]; proxyRenderer->backend->disableOBJ = proxyRenderer->d.disableOBJ; + proxyRenderer->backend->highlightBG[0] = proxyRenderer->d.highlightBG[0]; + proxyRenderer->backend->highlightBG[1] = proxyRenderer->d.highlightBG[1]; + proxyRenderer->backend->highlightBG[2] = proxyRenderer->d.highlightBG[2]; + proxyRenderer->backend->highlightBG[3] = proxyRenderer->d.highlightBG[3]; + memcpy(proxyRenderer->backend->highlightOBJ, proxyRenderer->d.highlightOBJ, sizeof(proxyRenderer->backend->highlightOBJ)); + proxyRenderer->backend->highlightAmount = proxyRenderer->d.highlightAmount; if (item->address < GBA_VIDEO_VERTICAL_PIXELS) { proxyRenderer->backend->drawScanline(proxyRenderer->backend, item->address); } diff --git a/src/gba/renderers/common.c b/src/gba/renderers/common.c index 9065a6267..2ec71b3f7 100644 --- a/src/gba/renderers/common.c +++ b/src/gba/renderers/common.c @@ -25,6 +25,7 @@ int GBAVideoRendererCleanOAM(struct GBAObj* oam, struct GBAVideoRendererSprite* sprites[oamMax].y = y; sprites[oamMax].endY = y + height; sprites[oamMax].obj = obj; + sprites[oamMax].index = i; ++oamMax; } } diff --git a/src/gba/renderers/software-mode0.c b/src/gba/renderers/software-mode0.c index df5626c4b..9c5697e6c 100644 --- a/src/gba/renderers/software-mode0.c +++ b/src/gba/renderers/software-mode0.c @@ -482,8 +482,14 @@ void GBAVideoSoftwareRendererDrawBackgroundMode0(struct GBAVideoSoftwareRenderer uint32_t charBase; int variant = background->target1 && GBAWindowControlIsBlendEnable(renderer->currentWindow.packed) && (renderer->blendEffect == BLEND_BRIGHTEN || renderer->blendEffect == BLEND_DARKEN); color_t* mainPalette = renderer->normalPalette; + if (renderer->d.highlightAmount && background->highlight) { + mainPalette = renderer->highlightPalette; + } if (variant) { mainPalette = renderer->variantPalette; + if (renderer->d.highlightAmount && background->highlight) { + mainPalette = renderer->highlightVariantPalette; + } } color_t* palette = mainPalette; PREPARE_OBJWIN; diff --git a/src/gba/renderers/software-obj.c b/src/gba/renderers/software-obj.c index 8e3e677e1..6f0c1bb9e 100644 --- a/src/gba/renderers/software-obj.c +++ b/src/gba/renderers/software-obj.c @@ -127,7 +127,7 @@ renderer->row[outX] |= FLAG_OBJWIN; \ } -int GBAVideoSoftwareRendererPreprocessSprite(struct GBAVideoSoftwareRenderer* renderer, struct GBAObj* sprite, int y) { +int GBAVideoSoftwareRendererPreprocessSprite(struct GBAVideoSoftwareRenderer* renderer, struct GBAObj* sprite, int index, int y) { int width = GBAVideoObjSizes[GBAObjAttributesAGetShape(sprite->a) * 4 + GBAObjAttributesBGetSize(sprite->b)][0]; int height = GBAVideoObjSizes[GBAObjAttributesAGetShape(sprite->a) * 4 + GBAObjAttributesBGetSize(sprite->b)][1]; int start = renderer->start; @@ -167,10 +167,16 @@ int GBAVideoSoftwareRendererPreprocessSprite(struct GBAVideoSoftwareRenderer* re } color_t* palette = &renderer->normalPalette[0x100]; + if (renderer->d.highlightAmount && renderer->d.highlightOBJ[index]) { + palette = &renderer->highlightPalette[0x100]; + } color_t* objwinPalette = palette; if (variant) { palette = &renderer->variantPalette[0x100]; + if (renderer->d.highlightAmount && renderer->d.highlightOBJ[index]) { + palette = &renderer->highlightVariantPalette[0x100]; + } if (GBAWindowControlIsBlendEnable(renderer->objwin.packed)) { objwinPalette = palette; } diff --git a/src/gba/renderers/software-private.h b/src/gba/renderers/software-private.h index 4e4e19166..07e588439 100644 --- a/src/gba/renderers/software-private.h +++ b/src/gba/renderers/software-private.h @@ -26,7 +26,7 @@ void GBAVideoSoftwareRendererDrawBackgroundMode4(struct GBAVideoSoftwareRenderer void GBAVideoSoftwareRendererDrawBackgroundMode5(struct GBAVideoSoftwareRenderer* renderer, struct GBAVideoSoftwareBackground* background, int y); -int GBAVideoSoftwareRendererPreprocessSprite(struct GBAVideoSoftwareRenderer* renderer, struct GBAObj* sprite, int y); +int GBAVideoSoftwareRendererPreprocessSprite(struct GBAVideoSoftwareRenderer* renderer, struct GBAObj* sprite, int index, int y); void GBAVideoSoftwareRendererPostprocessSprite(struct GBAVideoSoftwareRenderer* renderer, unsigned priority); static inline unsigned _brighten(unsigned color, int y); @@ -141,11 +141,17 @@ static inline void _compositeNoBlendNoObjwin(struct GBAVideoSoftwareRenderer* re int objwinForceEnable = 0; \ UNUSED(objwinForceEnable); \ color_t* objwinPalette = renderer->normalPalette; \ + if (renderer->d.highlightAmount && background->highlight) { \ + objwinPalette = renderer->highlightPalette; \ + } \ UNUSED(objwinPalette); \ if (objwinSlowPath) { \ if (background->target1 && GBAWindowControlIsBlendEnable(renderer->objwin.packed) && \ (renderer->blendEffect == BLEND_BRIGHTEN || renderer->blendEffect == BLEND_DARKEN)) { \ objwinPalette = renderer->variantPalette; \ + if (renderer->d.highlightAmount && background->highlight) { \ + palette = renderer->highlightVariantPalette; \ + } \ } \ switch (background->index) { \ case 0: \ @@ -200,8 +206,14 @@ static inline void _compositeNoBlendNoObjwin(struct GBAVideoSoftwareRenderer* re int variant = background->target1 && GBAWindowControlIsBlendEnable(renderer->currentWindow.packed) && \ (renderer->blendEffect == BLEND_BRIGHTEN || renderer->blendEffect == BLEND_DARKEN); \ color_t* palette = renderer->normalPalette; \ + if (renderer->d.highlightAmount && background->highlight) { \ + palette = renderer->highlightPalette; \ + } \ if (variant) { \ palette = renderer->variantPalette; \ + if (renderer->d.highlightAmount && background->highlight) { \ + palette = renderer->highlightVariantPalette; \ + } \ } \ UNUSED(palette); \ PREPARE_OBJWIN; diff --git a/src/gba/renderers/video-software.c b/src/gba/renderers/video-software.c index 86c3bc409..565b7a452 100644 --- a/src/gba/renderers/video-software.c +++ b/src/gba/renderers/video-software.c @@ -62,6 +62,17 @@ void GBAVideoSoftwareRendererCreate(struct GBAVideoSoftwareRenderer* renderer) { renderer->d.disableBG[3] = false; renderer->d.disableOBJ = false; + renderer->d.highlightBG[0] = false; + renderer->d.highlightBG[1] = false; + renderer->d.highlightBG[2] = false; + renderer->d.highlightBG[3] = false; + int i; + for (i = 0; i < 128; ++i) { + renderer->d.highlightOBJ[i] = false; + } + renderer->d.highlightColor = GBA_COLOR_WHITE; + renderer->d.highlightAmount = 0; + renderer->temporaryBuffer = 0; } @@ -568,6 +579,13 @@ static void GBAVideoSoftwareRendererDrawScanline(struct GBAVideoRenderer* render softwareRenderer->windows[0].control.packed = 0xFF; } + if (softwareRenderer->lastHighlightAmount != softwareRenderer->d.highlightAmount) { + softwareRenderer->lastHighlightAmount = softwareRenderer->d.highlightAmount; + if (softwareRenderer->lastHighlightAmount) { + softwareRenderer->blendDirty = true; + } + } + if (softwareRenderer->blendDirty) { _updatePalettes(softwareRenderer); softwareRenderer->blendDirty = false; @@ -595,6 +613,11 @@ static void GBAVideoSoftwareRendererDrawScanline(struct GBAVideoRenderer* render } } + softwareRenderer->bg[0].highlight = softwareRenderer->d.highlightBG[0]; + softwareRenderer->bg[1].highlight = softwareRenderer->d.highlightBG[1]; + softwareRenderer->bg[2].highlight = softwareRenderer->d.highlightBG[2]; + softwareRenderer->bg[3].highlight = softwareRenderer->d.highlightBG[3]; + _drawScanline(softwareRenderer, y); if (softwareRenderer->target2Bd) { @@ -828,7 +851,7 @@ static void _drawScanline(struct GBAVideoSoftwareRenderer* renderer, int y) { continue; } - int drawn = GBAVideoSoftwareRendererPreprocessSprite(renderer, &sprite->obj, localY); + int drawn = GBAVideoSoftwareRendererPreprocessSprite(renderer, &sprite->obj, sprite->index, localY); spriteLayers |= drawn << GBAObjAttributesCGetPriority(sprite->obj.c); } if (renderer->spriteCyclesRemaining <= 0) { @@ -925,4 +948,12 @@ static void _updatePalettes(struct GBAVideoSoftwareRenderer* renderer) { renderer->variantPalette[i] = renderer->normalPalette[i]; } } + unsigned highlightAmount = renderer->d.highlightAmount >> 4; + + if (highlightAmount) { + for (i = 0; i < 512; ++i) { + renderer->highlightPalette[i] = _mix(0x10 - highlightAmount, renderer->normalPalette[i], highlightAmount, renderer->d.highlightColor); + renderer->highlightVariantPalette[i] = _mix(0x10 - highlightAmount, renderer->variantPalette[i], highlightAmount, renderer->d.highlightColor); + } + } } From 59d2e58bbbd42746d2bc781fe8d8adfe1ff3588f Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 14:30:44 -0700 Subject: [PATCH 14/50] GBA Core: VLP fixes --- src/gba/core.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gba/core.c b/src/gba/core.c index bf3a7ecfd..67864c409 100644 --- a/src/gba/core.c +++ b/src/gba/core.c @@ -1094,6 +1094,7 @@ static void _GBAVLPStartFrameCallback(void *context) { GBAVideoProxyRendererUnshim(&gba->video, &gbacore->proxyRenderer); mVideoLogContextRewind(gbacore->logContext, core); GBAVideoProxyRendererShim(&gba->video, &gbacore->proxyRenderer); + gba->earlyExit = true; } } @@ -1109,6 +1110,7 @@ static bool _GBAVLPInit(struct mCore* core) { gbacore->logCallbacks.videoFrameStarted = _GBAVLPStartFrameCallback; gbacore->logCallbacks.context = core; core->addCoreCallbacks(core, &gbacore->logCallbacks); + core->videoLogger = gbacore->proxyRenderer.logger; return true; } From ef3cc7bd9f28d73fdff912587fc42260948d5b51 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 14:31:51 -0700 Subject: [PATCH 15/50] Qt: Redo frame inspector using video logs --- src/platform/qt/FrameView.cpp | 203 +++++++++++++++++++++++----------- src/platform/qt/FrameView.h | 28 ++++- src/platform/qt/FrameView.ui | 30 ++--- 3 files changed, 178 insertions(+), 83 deletions(-) diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp index 22bac7fce..bb5e23e48 100644 --- a/src/platform/qt/FrameView.cpp +++ b/src/platform/qt/FrameView.cpp @@ -14,6 +14,8 @@ #include "CoreController.h" +#include +#include #ifdef M_CORE_GBA #include #include @@ -37,7 +39,6 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent ++m_glowFrame; invalidateQueue(); }); - m_glowTimer.start(); m_ui.renderedView->installEventFilter(this); m_ui.compositedView->installEventFilter(this); @@ -66,6 +67,15 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent QPixmap rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value()); m_ui.renderedView->setPixmap(rendered); }); + m_controller->addFrameAction(std::bind(&FrameView::frameCallback, this, m_callbackLocker)); +} + +FrameView::~FrameView() { + QMutexLocker locker(&m_mutex); + *m_callbackLocker = false; + if (m_vl) { + m_vl->deinit(m_vl); + } } bool FrameView::lookupLayer(const QPointF& coord, Layer*& out) { @@ -122,24 +132,26 @@ void FrameView::disableLayer(const QPointF& coord) { m_disabled.insert(layer->id); } +#ifdef M_CORE_GBA void FrameView::updateTilesGBA(bool force) { if (m_ui.freeze->checkState() == Qt::Checked) { return; } + QMutexLocker locker(&m_mutex); m_queue.clear(); { CoreController::Interrupter interrupter(m_controller); - updateRendered(); uint16_t* io = static_cast(m_controller->thread()->core->board)->memory.io; QRgb backdrop = M_RGB5_TO_RGB8(static_cast(m_controller->thread()->core->board)->video.palette[0]); - int mode = GBARegisterDISPCNTGetMode(io[REG_DISPCNT >> 1]); + m_gbaDispcnt = io[REG_DISPCNT >> 1]; + int mode = GBARegisterDISPCNTGetMode(m_gbaDispcnt); std::array enabled{ - bool(GBARegisterDISPCNTIsBg0Enable(io[REG_DISPCNT >> 1])), - bool(GBARegisterDISPCNTIsBg1Enable(io[REG_DISPCNT >> 1])), - bool(GBARegisterDISPCNTIsBg2Enable(io[REG_DISPCNT >> 1])), - bool(GBARegisterDISPCNTIsBg3Enable(io[REG_DISPCNT >> 1])), + bool(GBARegisterDISPCNTIsBg0Enable(m_gbaDispcnt)), + bool(GBARegisterDISPCNTIsBg1Enable(m_gbaDispcnt)), + bool(GBARegisterDISPCNTIsBg2Enable(m_gbaDispcnt)), + bool(GBARegisterDISPCNTIsBg3Enable(m_gbaDispcnt)), }; for (int priority = 0; priority < 4; ++priority) { @@ -204,10 +216,53 @@ void FrameView::updateTilesGBA(bool force) { QPixmap::fromImage(backdropImage), {}, {0, 0}, false }); + updateRendered(); } invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS)); } +void FrameView::injectGBA() { + mVideoLogger* logger = m_vl->videoLogger; + mVideoLoggerInjectionPoint(logger, LOGGER_INJECTION_FIRST_SCANLINE); + GBA* gba = static_cast(m_vl->board); + gba->video.renderer->highlightBG[0] = false; + gba->video.renderer->highlightBG[1] = false; + gba->video.renderer->highlightBG[2] = false; + gba->video.renderer->highlightBG[3] = false; + for (int i = 0; i < 128; ++i) { + gba->video.renderer->highlightOBJ[i] = false; + } + QPalette palette; + gba->video.renderer->highlightColor = palette.color(QPalette::HighlightedText).rgb(); + gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 64 + 64; + + for (const Layer& layer : m_queue) { + switch (layer.id.type) { + case LayerId::SPRITE: + if (!layer.enabled) { + mVideoLoggerInjectOAM(logger, layer.id.index << 2, 0x200); + } + if (layer.id == m_active) { + gba->video.renderer->highlightOBJ[layer.id.index] = true; + } + break; + case LayerId::BACKGROUND: + m_vl->enableVideoLayer(m_vl, layer.id.index, layer.enabled); + if (layer.id == m_active) { + gba->video.renderer->highlightBG[layer.id.index] = true; + } + break; + } + } + if (m_ui.disableScanline->checkState() == Qt::Checked) { + mVideoLoggerIgnoreAfterInjection(logger, (1 << DIRTY_PALETTE) | (1 << DIRTY_OAM) | (1 << DIRTY_REGISTER)); + } else { + mVideoLoggerIgnoreAfterInjection(logger, 0); + } +} +#endif + +#ifdef M_CORE_GB void FrameView::updateTilesGB(bool force) { if (m_ui.freeze->checkState() == Qt::Checked) { return; @@ -220,23 +275,33 @@ void FrameView::updateTilesGB(bool force) { invalidateQueue(m_controller->screenDimensions()); } +void FrameView::injectGB() { + for (const Layer& layer : m_queue) { + } +} +#endif + void FrameView::invalidateQueue(const QSize& dims) { if (dims.isValid()) { m_dims = dims; } bool blockSignals = m_ui.queue->blockSignals(true); - QPixmap composited(m_dims); + QMutexLocker locker(&m_mutex); + if (m_vl) { + m_vl->reset(m_vl); + switch (m_controller->platform()) { +#ifdef M_CORE_GBA + case PLATFORM_GBA: + injectGBA(); +#endif +#ifdef M_CORE_GB + case PLATFORM_GB: + injectGB(); +#endif + } + m_vl->runFrame(m_vl); + } - QPainter painter(&composited); - QPalette palette; - QColor activeColor = palette.color(QPalette::HighlightedText); - activeColor.setAlpha(sin(m_glowFrame * M_PI / 60) * 16 + 96); - - QRectF rect(0, 0, m_dims.width(), m_dims.height()); - painter.setCompositionMode(QPainter::CompositionMode_Source); - painter.fillRect(rect, QColor(0, 0, 0, 0)); - - painter.setCompositionMode(QPainter::CompositionMode_DestinationOver); for (int i = 0; i < m_queue.count(); ++i) { const Layer& layer = m_queue[i]; QListWidgetItem* item; @@ -251,61 +316,20 @@ void FrameView::invalidateQueue(const QSize& dims) { item->setCheckState(layer.enabled ? Qt::Checked : Qt::Unchecked); item->setData(Qt::UserRole, i); item->setSelected(layer.id == m_active); - - if (!layer.enabled) { - continue; - } - - QPointF location = layer.location; - QSizeF layerDims(layer.image.width(), layer.image.height()); - QRegion region; - if (layer.repeats) { - if (location.x() + layerDims.width() < 0) { - location.setX(std::fmod(location.x(), layerDims.width())); - } - if (location.y() + layerDims.height() < 0) { - location.setY(std::fmod(location.y(), layerDims.height())); - } - - if (layer.id == m_active) { - region = layer.mask.translated(location.x(), location.y()); - region += layer.mask.translated(location.x() + layerDims.width(), location.y()); - region += layer.mask.translated(location.x(), location.y() + layerDims.height()); - region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height()); - } - } else { - QRectF layerRect(location, layerDims); - if (!rect.intersects(layerRect)) { - continue; - } - if (layer.id == m_active) { - region = layer.mask.translated(location.x(), location.y()); - } - } - - if (layer.id == m_active) { - painter.setClipping(true); - painter.setClipRegion(region); - painter.fillRect(rect, activeColor); - painter.setClipping(false); - } - - if (layer.repeats) { - painter.drawPixmap(location, layer.image); - painter.drawPixmap(location + QPointF(layerDims.width(), 0), layer.image); - painter.drawPixmap(location + QPointF(0, layerDims.height()), layer.image); - painter.drawPixmap(location + QPointF(layerDims.width(), layerDims.height()), layer.image); - } else { - painter.drawPixmap(location, layer.image); - } } - painter.end(); while (m_ui.queue->count() > m_queue.count()) { delete m_ui.queue->takeItem(m_queue.count()); } m_ui.queue->blockSignals(blockSignals); + QPixmap composited; + if (m_framebuffer.isNull()) { + updateRendered(); + composited = m_rendered; + } else { + composited.convertFromImage(m_framebuffer); + } m_composited = composited.scaled(m_dims * m_ui.magnification->value()); m_ui.compositedView->setPixmap(m_composited); } @@ -336,6 +360,53 @@ bool FrameView::eventFilter(QObject* obj, QEvent* event) { return false; } +void FrameView::refreshVl() { + QMutexLocker locker(&m_mutex); + m_currentFrame = m_nextFrame; + m_nextFrame = VFileMemChunk(nullptr, 0); + if (m_currentFrame) { + m_controller->endVideoLog(false); + VFile* currentFrame = VFileMemChunk(nullptr, m_currentFrame->size(m_currentFrame)); + void* buffer = currentFrame->map(currentFrame, m_currentFrame->size(m_currentFrame), MAP_WRITE); + m_currentFrame->seek(m_currentFrame, 0, SEEK_SET); + m_currentFrame->read(m_currentFrame, buffer, m_currentFrame->size(m_currentFrame)); + currentFrame->unmap(currentFrame, buffer, m_currentFrame->size(m_currentFrame)); + m_currentFrame = currentFrame; + QMetaObject::invokeMethod(this, "newVl"); + } + m_controller->endVideoLog(); + m_controller->startVideoLog(m_nextFrame, false); +} + +void FrameView::newVl() { + if (!m_glowTimer.isActive()) { + m_glowTimer.start(); + } + QMutexLocker locker(&m_mutex); + if (m_vl) { + m_vl->deinit(m_vl); + } + m_vl = mCoreFindVF(m_currentFrame); + m_vl->init(m_vl); + m_vl->loadROM(m_vl, m_currentFrame); + mCoreInitConfig(m_vl, nullptr); + unsigned width, height; + m_vl->desiredVideoDimensions(m_vl, &width, &height); + m_framebuffer = QImage(width, height, QImage::Format_RGBX8888); + m_vl->setVideoBuffer(m_vl, reinterpret_cast(m_framebuffer.bits()), width); + m_vl->reset(m_vl); +} + +void FrameView::frameCallback(FrameView* viewer, std::shared_ptr lock) { + if (!*lock) { + return; + } + CoreController::Interrupter interrupter(viewer->m_controller, true); + viewer->refreshVl(); + viewer->m_controller->addFrameAction(std::bind(&FrameView::frameCallback, viewer, lock)); +} + + QString FrameView::LayerId::readable() const { QString typeStr; switch (type) { diff --git a/src/platform/qt/FrameView.h b/src/platform/qt/FrameView.h index 4fd1bea09..f255b62fc 100644 --- a/src/platform/qt/FrameView.h +++ b/src/platform/qt/FrameView.h @@ -10,14 +10,19 @@ #include #include #include +#include #include #include #include #include "AssetView.h" +#include + #include +struct VFile; + namespace QGBA { class CoreController; @@ -27,6 +32,7 @@ Q_OBJECT public: FrameView(std::shared_ptr controller, QWidget* parent = nullptr); + ~FrameView(); public slots: void selectLayer(const QPointF& coord); @@ -35,16 +41,20 @@ public slots: protected: #ifdef M_CORE_GBA void updateTilesGBA(bool force) override; + void injectGBA(); #endif #ifdef M_CORE_GB void updateTilesGB(bool force) override; + void injectGB(); #endif bool eventFilter(QObject* obj, QEvent* event) override; private slots: - void invalidateQueue(const QSize& dims = QSize()); + void invalidateQueue(const QSize& = {}); void updateRendered(); + void refreshVl(); + void newVl(); private: struct LayerId { @@ -57,7 +67,7 @@ private: } type = NONE; int index = -1; - bool operator==(const LayerId& other) const { return other.type == type && other.index == index; } + bool operator!=(const LayerId& other) const { return other.type != type || other.index != index; } operator uint() const { return (type << 8) | index; } QString readable() const; }; @@ -73,6 +83,8 @@ private: bool lookupLayer(const QPointF& coord, Layer*&); + static void frameCallback(FrameView*, std::shared_ptr); + Ui::FrameView m_ui; LayerId m_active{}; @@ -80,12 +92,24 @@ private: int m_glowFrame; QTimer m_glowTimer; + QMutex m_mutex{QMutex::Recursive}; + VFile* m_currentFrame = nullptr; + VFile* m_nextFrame = nullptr; + mCore* m_vl = nullptr; + QImage m_framebuffer; + QSize m_dims; QList m_queue; QSet m_disabled; QPixmap m_composited; QPixmap m_rendered; mMapCacheEntry m_mapStatus[4][128 * 128] = {}; // TODO: Correct size + +#ifdef M_CORE_GBA + uint16_t m_gbaDispcnt; +#endif + + std::shared_ptr m_callbackLocker{std::make_shared(true)}; }; } \ No newline at end of file diff --git a/src/platform/qt/FrameView.ui b/src/platform/qt/FrameView.ui index 2e61e09c8..c437b14f6 100644 --- a/src/platform/qt/FrameView.ui +++ b/src/platform/qt/FrameView.ui @@ -13,7 +13,7 @@ Inspect frame - + @@ -51,7 +51,7 @@ - + true @@ -61,8 +61,8 @@ 0 0 - 591 - 403 + 567 + 382 @@ -90,7 +90,7 @@ - + true @@ -100,8 +100,8 @@ 0 0 - 591 - 446 + 567 + 467 @@ -129,7 +129,10 @@ - + + + + false @@ -139,13 +142,10 @@ - - - - - 0 - 0 - + + + + Disable scanline effects From c7b6c4412d772011f4e1b9ee63dd830975e30972 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 14:57:35 -0700 Subject: [PATCH 16/50] Qt: Support export button in frame inspector --- src/platform/qt/FrameView.cpp | 9 +++++++++ src/platform/qt/FrameView.h | 1 + 2 files changed, 10 insertions(+) diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp index bb5e23e48..2f1c68a08 100644 --- a/src/platform/qt/FrameView.cpp +++ b/src/platform/qt/FrameView.cpp @@ -13,6 +13,7 @@ #include #include "CoreController.h" +#include "GBAApp.h" #include #include @@ -67,6 +68,7 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent QPixmap rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value()); m_ui.renderedView->setPixmap(rendered); }); + connect(m_ui.exportButton, &QAbstractButton::pressed, this, &FrameView::exportFrame); m_controller->addFrameAction(std::bind(&FrameView::frameCallback, this, m_callbackLocker)); } @@ -328,6 +330,7 @@ void FrameView::invalidateQueue(const QSize& dims) { updateRendered(); composited = m_rendered; } else { + m_ui.exportButton->setEnabled(true); composited.convertFromImage(m_framebuffer); } m_composited = composited.scaled(m_dims * m_ui.magnification->value()); @@ -406,6 +409,12 @@ void FrameView::frameCallback(FrameView* viewer, std::shared_ptr lock) { viewer->m_controller->addFrameAction(std::bind(&FrameView::frameCallback, viewer, lock)); } +void FrameView::exportFrame() { + QString filename = GBAApp::app()->getSaveFileName(this, tr("Export frame"), + tr("Portable Network Graphics (*.png)")); + CoreController::Interrupter interrupter(m_controller); + m_framebuffer.save(filename, "PNG"); +} QString FrameView::LayerId::readable() const { QString typeStr; diff --git a/src/platform/qt/FrameView.h b/src/platform/qt/FrameView.h index f255b62fc..2a2b5f680 100644 --- a/src/platform/qt/FrameView.h +++ b/src/platform/qt/FrameView.h @@ -37,6 +37,7 @@ public: public slots: void selectLayer(const QPointF& coord); void disableLayer(const QPointF& coord); + void exportFrame(); protected: #ifdef M_CORE_GBA From 2743905845e0e3bc7bbb56f34bbc1be06d84ebaa Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 15:52:23 -0700 Subject: [PATCH 17/50] Qt: Add backdrop editor --- src/platform/qt/ColorPicker.cpp | 8 +++ src/platform/qt/ColorPicker.h | 3 + src/platform/qt/FrameView.cpp | 30 +++++--- src/platform/qt/FrameView.h | 3 + src/platform/qt/FrameView.ui | 119 ++++++++++++++++---------------- 5 files changed, 93 insertions(+), 70 deletions(-) diff --git a/src/platform/qt/ColorPicker.cpp b/src/platform/qt/ColorPicker.cpp index 18782a5dc..818a25fd4 100644 --- a/src/platform/qt/ColorPicker.cpp +++ b/src/platform/qt/ColorPicker.cpp @@ -34,6 +34,14 @@ ColorPicker& ColorPicker::operator=(const ColorPicker& other) { return *this; } +void ColorPicker::setColor(const QColor& color) { + m_defaultColor = color; + + QPalette palette = m_parent->palette(); + palette.setColor(m_parent->backgroundRole(), color); + m_parent->setPalette(palette); +} + bool ColorPicker::eventFilter(QObject* obj, QEvent* event) { if (event->type() != QEvent::MouseButtonRelease) { return false; diff --git a/src/platform/qt/ColorPicker.h b/src/platform/qt/ColorPicker.h index 1e94933c8..bf50c5528 100644 --- a/src/platform/qt/ColorPicker.h +++ b/src/platform/qt/ColorPicker.h @@ -24,6 +24,9 @@ public: signals: void colorChanged(const QColor&); +public slots: + void setColor(const QColor&); + protected: bool eventFilter(QObject* obj, QEvent* event) override; diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp index 2f1c68a08..2dea0f08b 100644 --- a/src/platform/qt/FrameView.cpp +++ b/src/platform/qt/FrameView.cpp @@ -41,7 +41,6 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent invalidateQueue(); }); - m_ui.renderedView->installEventFilter(this); m_ui.compositedView->installEventFilter(this); connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) { @@ -50,7 +49,7 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent if (layer.enabled) { m_disabled.remove(layer.id); } else { - m_disabled.insert(layer.id); + m_disabled.insert(layer.id); } invalidateQueue(); }); @@ -64,12 +63,20 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent }); connect(m_ui.magnification, static_cast(&QSpinBox::valueChanged), this, [this]() { invalidateQueue(); - - QPixmap rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value()); - m_ui.renderedView->setPixmap(rendered); }); connect(m_ui.exportButton, &QAbstractButton::pressed, this, &FrameView::exportFrame); + + m_backdropPicker = ColorPicker(m_ui.backdrop, QColor(0, 0, 0, 0)); + connect(&m_backdropPicker, &ColorPicker::colorChanged, this, [this](const QColor& color) { + m_overrideBackdrop = color; + }); m_controller->addFrameAction(std::bind(&FrameView::frameCallback, this, m_callbackLocker)); + + { + CoreController::Interrupter interrupter(m_controller); + refreshVl(); + } + m_controller->frameAdvance(); } FrameView::~FrameView() { @@ -237,7 +244,12 @@ void FrameView::injectGBA() { QPalette palette; gba->video.renderer->highlightColor = palette.color(QPalette::HighlightedText).rgb(); gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 64 + 64; + if (!m_overrideBackdrop.isValid()) { + QRgb backdrop = M_RGB5_TO_RGB8(gba->video.palette[0]) | 0xFF000000; + m_backdropPicker.setColor(backdrop); + } + m_vl->reset(m_vl); for (const Layer& layer : m_queue) { switch (layer.id.type) { case LayerId::SPRITE: @@ -256,10 +268,13 @@ void FrameView::injectGBA() { break; } } + if (m_overrideBackdrop.isValid()) { + mVideoLoggerInjectPalette(logger, 0, M_RGB8_TO_RGB5(m_overrideBackdrop.rgb())); + } if (m_ui.disableScanline->checkState() == Qt::Checked) { mVideoLoggerIgnoreAfterInjection(logger, (1 << DIRTY_PALETTE) | (1 << DIRTY_OAM) | (1 << DIRTY_REGISTER)); } else { - mVideoLoggerIgnoreAfterInjection(logger, 0); + mVideoLoggerIgnoreAfterInjection(logger, 0); } } #endif @@ -290,7 +305,6 @@ void FrameView::invalidateQueue(const QSize& dims) { bool blockSignals = m_ui.queue->blockSignals(true); QMutexLocker locker(&m_mutex); if (m_vl) { - m_vl->reset(m_vl); switch (m_controller->platform()) { #ifdef M_CORE_GBA case PLATFORM_GBA: @@ -342,8 +356,6 @@ void FrameView::updateRendered() { return; } m_rendered.convertFromImage(m_controller->getPixels()); - QPixmap rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value()); - m_ui.renderedView->setPixmap(rendered); } bool FrameView::eventFilter(QObject* obj, QEvent* event) { diff --git a/src/platform/qt/FrameView.h b/src/platform/qt/FrameView.h index 2a2b5f680..cce09a42c 100644 --- a/src/platform/qt/FrameView.h +++ b/src/platform/qt/FrameView.h @@ -16,6 +16,7 @@ #include #include "AssetView.h" +#include "ColorPicker.h" #include @@ -105,6 +106,8 @@ private: QPixmap m_composited; QPixmap m_rendered; mMapCacheEntry m_mapStatus[4][128 * 128] = {}; // TODO: Correct size + ColorPicker m_backdropPicker; + QColor m_overrideBackdrop; #ifdef M_CORE_GBA uint16_t m_gbaDispcnt; diff --git a/src/platform/qt/FrameView.ui b/src/platform/qt/FrameView.ui index c437b14f6..8cf75ec08 100644 --- a/src/platform/qt/FrameView.ui +++ b/src/platform/qt/FrameView.ui @@ -13,7 +13,7 @@ Inspect frame - + @@ -51,7 +51,63 @@ - + + + + + + + 0 + 0 + + + + + 32 + 32 + + + + true + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Backdrop color + + + + + + + + + Disable scanline effects + + + + + + + + + + false + + + Export + + + + true @@ -90,65 +146,6 @@ - - - - true - - - - - 0 - 0 - 567 - 467 - - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - false - - - Export - - - - - - - Disable scanline effects - - - From b99d8164ddba2e6f8fc16de941d1551674974e4c Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 1 Jun 2019 23:41:28 -0700 Subject: [PATCH 18/50] Qt: Initial mask support for transformed sprites --- include/mgba/internal/gba/video.h | 24 ++++++++++++------------ src/platform/qt/AssetView.cpp | 13 +++++++++++-- src/platform/qt/AssetView.h | 2 ++ src/platform/qt/FrameView.cpp | 8 ++++++-- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/include/mgba/internal/gba/video.h b/include/mgba/internal/gba/video.h index 908954aaf..ea65699aa 100644 --- a/include/mgba/internal/gba/video.h +++ b/include/mgba/internal/gba/video.h @@ -81,20 +81,20 @@ struct GBAObj { uint16_t d; }; +struct GBAOAMMatrix { + int16_t padding0[3]; + int16_t a; + int16_t padding1[3]; + int16_t b; + int16_t padding2[3]; + int16_t c; + int16_t padding3[3]; + int16_t d; +}; + union GBAOAM { struct GBAObj obj[128]; - - struct GBAOAMMatrix { - int16_t padding0[3]; - int16_t a; - int16_t padding1[3]; - int16_t b; - int16_t padding2[3]; - int16_t c; - int16_t padding3[3]; - int16_t d; - } mat[32]; - + struct GBAOAMMatrix mat[32]; uint16_t raw[512]; }; diff --git a/src/platform/qt/AssetView.cpp b/src/platform/qt/AssetView.cpp index c13af9403..97d44578d 100644 --- a/src/platform/qt/AssetView.cpp +++ b/src/platform/qt/AssetView.cpp @@ -204,9 +204,18 @@ bool AssetView::lookupObjGBA(int id, struct ObjInfo* info) { GBAObjAttributesCGetPriority(obj->c), GBAObjAttributesBGetX(obj->b), GBAObjAttributesAGetY(obj->a), - bool(GBAObjAttributesBIsHFlip(obj->b)), - bool(GBAObjAttributesBIsVFlip(obj->b)), + false, + false, }; + if (GBAObjAttributesAIsTransformed(obj->a)) { + int matIndex = GBAObjAttributesBGetMatIndex(obj->b); + const GBAOAMMatrix* mat = &gba->video.oam.mat[matIndex]; + QTransform invXform(mat->a / 256., mat->c / 256., mat->b / 256., mat->d / 256., 0, 0); + newInfo.xform = invXform.inverted(); + } else { + newInfo.hflip = bool(GBAObjAttributesBIsHFlip(obj->b)); + newInfo.vflip = bool(GBAObjAttributesBIsVFlip(obj->b)); + } GBARegisterDISPCNT dispcnt = gba->memory.io[0]; // FIXME: Register name can't be imported due to namespacing issues if (!GBARegisterDISPCNTIsObjCharacterMapping(dispcnt)) { newInfo.stride = 0x20 >> (GBAObjAttributesAGet256Color(obj->a)); diff --git a/src/platform/qt/AssetView.h b/src/platform/qt/AssetView.h index 779e22890..acc4a1454 100644 --- a/src/platform/qt/AssetView.h +++ b/src/platform/qt/AssetView.h @@ -6,6 +6,7 @@ #pragma once #include +#include #include #include @@ -58,6 +59,7 @@ protected: unsigned y : 9; bool hflip : 1; bool vflip : 1; + QTransform xform; bool operator!=(const ObjInfo&) const; }; diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp index 2dea0f08b..00cb56d0e 100644 --- a/src/platform/qt/FrameView.cpp +++ b/src/platform/qt/FrameView.cpp @@ -6,7 +6,6 @@ #include "FrameView.h" #include -#include #include #include @@ -177,6 +176,11 @@ void FrameView::updateTilesGBA(bool force) { if (info.hflip || info.vflip) { obj = obj.mirrored(info.hflip, info.vflip); } + if (!info.xform.isIdentity()) { + offset += QPointF(obj.width(), obj.height()) / 2; + obj = obj.transformed(info.xform); + offset -= QPointF(obj.width() / 2, obj.height() / 2); + } m_queue.append({ { LayerId::SPRITE, sprite }, !m_disabled.contains({ LayerId::SPRITE, sprite }), @@ -243,7 +247,7 @@ void FrameView::injectGBA() { } QPalette palette; gba->video.renderer->highlightColor = palette.color(QPalette::HighlightedText).rgb(); - gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 64 + 64; + gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 48 + 64; if (!m_overrideBackdrop.isValid()) { QRgb backdrop = M_RGB5_TO_RGB8(gba->video.palette[0]) | 0xFF000000; m_backdropPicker.setColor(backdrop); From 427e3a61029a8c49f7dbd4edc4ad4400f6427866 Mon Sep 17 00:00:00 2001 From: Lothar Serra Mari Date: Sun, 2 Jun 2019 11:35:42 +0200 Subject: [PATCH 19/50] Qt: Update German GUI translation --- src/platform/qt/ts/mgba-de.ts | 485 +++++++++++++++++++++------------- 1 file changed, 303 insertions(+), 182 deletions(-) diff --git a/src/platform/qt/ts/mgba-de.ts b/src/platform/qt/ts/mgba-de.ts index 28f090df8..fc55841be 100644 --- a/src/platform/qt/ts/mgba-de.ts +++ b/src/platform/qt/ts/mgba-de.ts @@ -254,6 +254,44 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd.Unterbrechen + + FrameView + + + Inspect frame + Bild beobachten + + + + × + × + + + + Magnification + Vergrößerung + + + + Freeze frame + Bild einfrieren + + + + Backdrop color + Hintergrundfarbe + + + + Disable scanline effects + Scanline-Effekte deaktivieren + + + + Export + Exportieren + + GIFView @@ -546,17 +584,17 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd.Maps - + × × - + Magnification Vergrößerung - + Export Exportieren @@ -1231,14 +1269,14 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. QGBA::AssetTile - + %0%1%2 %0%1%2 - - - + + + 0x%0 (%1) 0x%0 (%1) @@ -1289,22 +1327,22 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. QGBA::CoreController - + Failed to open save file: %1 Fehler beim Öffnen der Speicherdatei: %1 - + Failed to open game file: %1 Fehler beim Öffnen der Spieldatei: %1 - + Failed to open snapshot file for reading: %1 Konnte Snapshot-Datei %1 nicht zum Lesen öffnen - + Failed to open snapshot file for writing: %1 Konnte Snapshot-Datei %1 nicht zum Schreiben öffnen @@ -1317,6 +1355,49 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd.Fehler beim Öffnen der Spieldatei: %1 + + QGBA::FrameView + + + Export frame + Bild exportieren + + + + Portable Network Graphics (*.png) + Portable Network Graphics (*.png) + + + + None + Keine + + + + Background + Hintergrund + + + + Window + Fenster + + + + Sprite + Sprite + + + + Backdrop + Hintergrund + + + + %1 %2 + %1 %2 + + QGBA::GBAApp @@ -2909,47 +2990,87 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. QGBA::MapView - + + Priority + Priorität + + + + + Map base + Map-Basis + + + + + Tile base + Tile-Basis + + + + Size + Größe + + + + + Offset + Versatz + + + + Xform + Xform + + + Map Addr. Map-Addr. - + Mirror Spiegel - + None Keiner - + Both Beidseitig - + Horizontal Horizontal - + Vertical Vertikal - + + + + N/A + N/A + + + Export map Map exportieren - + Portable Network Graphics (*.png) Portable Network Graphics (*.png) - + Failed to open output PNG file: %1 Fehler beim Öffnen der Ausgabe-PNG-Datei: %1 @@ -3043,57 +3164,52 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. QGBA::ObjView - - + + 0x%0 0x%0 - + Off Aus - + Normal Normal - + Trans Trans - + OBJWIN OBJWIN - + Invalid Ungültig - - + + N/A N/A - + Export sprite Sprite exportieren - + Portable Network Graphics (*.png) Portable Network Graphics (*.png) - - - Failed to open output PNG file: %1 - Fehler beim Öffnen der Ausgabe-PNG-Datei: %1 - QGBA::PaletteView @@ -3290,103 +3406,103 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. QGBA::Window - + Game Boy Advance ROMs (%1) Game Boy Advance-ROMs (%1) - + Game Boy ROMs (%1) Game Boy-ROMs (%1) - + All ROMs (%1) Alle ROMs (%1) - + %1 Video Logs (*.mvl) %1 Video-Logs (*.mvl) - + Archives (%1) Archive (%1) - - - + + + Select ROM ROM auswählen - + Game Boy Advance save files (%1) Game Boy Advance-Speicherdateien (%1) - - - + + + Select save Speicherdatei wählen - + mGBA savestate files (%1) mGBA Savestate-Dateien (%1) - - + + Select savestate Savestate auswählen - + Select patch Patch wählen - + Patches (*.ips *.ups *.bps) Patches (*.ips *.ups *.bps) - + Select image Bild auswählen - + Image file (*.png *.gif *.jpg *.jpeg);;All files (*) Bild-Datei (*.png *.gif *.jpg *.jpeg);;Alle Dateien (*) - - + + GameShark saves (*.sps *.xps) GameShark-Speicherdaten (*.sps *.xps) - + Select video log Video-Log auswählen - + Video logs (*.mvl) Video-Logs (*.mvl) - + Crash Absturz - + The game has crashed with the following error: %1 @@ -3395,578 +3511,583 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. - + Couldn't Load Konnte nicht geladen werden - + Could not load game. Are you sure it's in the correct format? Konnte das Spiel nicht laden. Sind Sie sicher, dass es im korrekten Format vorliegt? - + Unimplemented BIOS call Nicht implementierter BIOS-Aufruf - + This game uses a BIOS call that is not implemented. Please use the official BIOS for best experience. Dieses Spiel verwendet einen BIOS-Aufruf, der nicht implementiert ist. Bitte verwenden Sie für die beste Spielerfahrung das offizielle BIOS. - + Really make portable? Portablen Modus wirklich aktivieren? - + This will make the emulator load its configuration from the same directory as the executable. Do you want to continue? Diese Einstellung wird den Emulator so konfigurieren, dass er seine Konfiguration aus dem gleichen Verzeichnis wie die Programmdatei lädt. Möchten Sie fortfahren? - + Restart needed Neustart benötigt - + Some changes will not take effect until the emulator is restarted. Einige Änderungen werden erst übernommen, wenn der Emulator neu gestartet wurde. - + - Player %1 of %2 - Spieler %1 von %2 - + %1 - %2 %1 - %2 - + %1 - %2 - %3 %1 - %2 - %3 - + %1 - %2 (%3 fps) - %4 %1 - %2 (%3 Bilder/Sekunde) - %4 - + &File &Datei - + Load &ROM... &ROM laden... - + Load ROM in archive... ROM aus Archiv laden... - + Load alternate save... Alternative Speicherdatei laden... - + Load temporary save... Temporäre Speicherdatei laden... - + Load &patch... &Patch laden... - + Boot BIOS BIOS booten - + Replace ROM... ROM ersetzen... - + ROM &info... ROM-&Informationen... - + Recent Zuletzt verwendet - + Make portable Portablen Modus aktivieren - + &Load state Savestate (aktueller Zustand) &laden - + Load state file... Ssavestate-Datei laden... - + &Save state Savestate (aktueller Zustand) &speichern - + Save state file... Savestate-Datei speichern... - + Quick load Schnell laden - + Quick save Schnell speichern - + Load recent Lade zuletzt gespeicherten Savestate - + Save recent Speichere aktuellen Zustand - + Undo load state Laden des Savestate rückgängig machen - + Undo save state Speichern des Savestate rückgängig machen - - + + State &%1 Savestate &%1 - + Load camera image... Lade Kamerabild... - + Import GameShark Save Importiere GameShark-Speicherstand - + Export GameShark Save Exportiere GameShark-Speicherstand - + New multiplayer window Neues Multiplayer-Fenster - + E&xit &Beenden - + &Emulation &Emulation - + &Reset Zu&rücksetzen - + Sh&utdown Schli&eßen - + Yank game pak Spielmodul herausziehen - + &Pause &Pause - + &Next frame &Nächstes Bild - + Fast forward (held) Schneller Vorlauf (gehalten) - + &Fast forward Schneller &Vorlauf - + Fast forward speed Vorlauf-Geschwindigkeit - + Unbounded Unbegrenzt - + %0x %0x - + Rewind (held) Zurückspulen (gehalten) - + Re&wind Zur&ückspulen - + Step backwards Schrittweiser Rücklauf - + Sync to &video Mit &Video synchronisieren - + Sync to &audio Mit &Audio synchronisieren - + Solar sensor - Solar-Sensor + Sonnen-Sensor - + Increase solar level Sonnen-Level erhöhen - + Decrease solar level Sonnen-Level verringern - + Brightest solar level Hellster Sonnen-Level - + Darkest solar level Dunkelster Sonnen-Level - + Brightness %1 Helligkeit %1 - + BattleChip Gate... BattleChip Gate... - + Audio/&Video Audio/&Video - + Frame size Bildgröße - + Toggle fullscreen Vollbildmodus umschalten - + Lock aspect ratio Seitenverhältnis korrigieren - + Force integer scaling Pixelgenaue Skalierung (Integer scaling) - + Interframe blending Interframe-Überblendung - + Frame&skip Frame&skip - + Mute Stummschalten - + FPS target Bildwiederholrate - + Take &screenshot &Screenshot erstellen - + F12 F12 - + Record GIF... GIF aufzeichen... - + Game Boy Printer... Game Boy Printer... - + Video layers Video-Ebenen - + Audio channels Audio-Kanäle - + Adjust layer placement... Lage der Bildebenen anpassen... - + &Tools &Werkzeuge - + View &logs... &Logs ansehen... - + Game &overrides... Spiel-&Überschreibungen... - - Game &Pak sensors... - Game &Pak-Sensoren... - - - + &Cheats... &Cheats... - + Open debugger console... Debugger-Konsole öffnen... - + Start &GDB server... &GDB-Server starten... - + Settings... Einstellungen... - + Select folder Ordner auswählen - + Add folder to library... Ordner zur Bibliothek hinzufügen... - + About... Über... - + %1× %1x - + Bilinear filtering Bilineare Filterung - + Native (59.7275) Nativ (59.7275) - + Record A/V... Audio/Video aufzeichnen... - + + Game Pak sensors... + Spielmodul-Sensoren... + + + View &palette... &Palette betrachten... - + View &sprites... &Sprites betrachten... - + View &tiles... &Tiles betrachten... - + View &map... &Map betrachten... - + + &Frame inspector... + &Bildbetrachter... + + + View memory... Speicher betrachten... - + Search memory... Speicher durchsuchen... - + View &I/O registers... &I/O-Register betrachten... - + Record debug video log... Video-Protokoll aufzeichnen... - + Stop debug video log Aufzeichnen des Video-Protokolls beenden - + Exit fullscreen Vollbildmodus beenden - + GameShark Button (held) GameShark-Taste (gehalten) - + Autofire Autofeuer - + Autofire A Autofeuer A - + Autofire B Autofeuer B - + Autofire L Autofeuer L - + Autofire R Autofeuer R - + Autofire Start Autofeuer Start - + Autofire Select Autofeuer Select - + Autofire Up Autofeuer nach oben - + Autofire Right Autofeuer rechts - + Autofire Down Autofeuer nach unten - + Autofire Left Autofeuer links From 00e8b9877f76420e1e85c34cc2a156d1a52b8a30 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sun, 2 Jun 2019 15:56:21 -0700 Subject: [PATCH 20/50] Qt: Add reset button to frame inspector --- src/platform/qt/FrameView.cpp | 10 +++++++ src/platform/qt/FrameView.h | 1 + src/platform/qt/FrameView.ui | 51 ++++++++++++++++++++--------------- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp index 00cb56d0e..5c13f2b77 100644 --- a/src/platform/qt/FrameView.cpp +++ b/src/platform/qt/FrameView.cpp @@ -64,6 +64,7 @@ FrameView::FrameView(std::shared_ptr controller, QWidget* parent invalidateQueue(); }); connect(m_ui.exportButton, &QAbstractButton::pressed, this, &FrameView::exportFrame); + connect(m_ui.reset, &QAbstractButton::pressed, this, &FrameView::reset); m_backdropPicker = ColorPicker(m_ui.backdrop, QColor(0, 0, 0, 0)); connect(&m_backdropPicker, &ColorPicker::colorChanged, this, [this](const QColor& color) { @@ -432,6 +433,15 @@ void FrameView::exportFrame() { m_framebuffer.save(filename, "PNG"); } +void FrameView::reset() { + m_disabled.clear(); + for (Layer& layer : m_queue) { + layer.enabled = true; + } + m_overrideBackdrop = QColor(); + invalidateQueue(); +} + QString FrameView::LayerId::readable() const { QString typeStr; switch (type) { diff --git a/src/platform/qt/FrameView.h b/src/platform/qt/FrameView.h index cce09a42c..e47e82054 100644 --- a/src/platform/qt/FrameView.h +++ b/src/platform/qt/FrameView.h @@ -39,6 +39,7 @@ public slots: void selectLayer(const QPointF& coord); void disableLayer(const QPointF& coord); void exportFrame(); + void reset(); protected: #ifdef M_CORE_GBA diff --git a/src/platform/qt/FrameView.ui b/src/platform/qt/FrameView.ui index 8cf75ec08..80b184561 100644 --- a/src/platform/qt/FrameView.ui +++ b/src/platform/qt/FrameView.ui @@ -13,7 +13,7 @@ Inspect frame - + @@ -87,27 +87,7 @@ - - - - Disable scanline effects - - - - - - - - - - false - - - Export - - - - + true @@ -146,6 +126,33 @@ + + + + Disable scanline effects + + + + + + + + + + false + + + Export + + + + + + + Reset + + + From ff735e35b77c2ee4246ed0413605a45ed6158847 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sun, 2 Jun 2019 17:21:44 -0700 Subject: [PATCH 21/50] GB: mVL-related fixes --- src/gb/core.c | 2 ++ src/gb/video.c | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/gb/core.c b/src/gb/core.c index 9c00519a0..87eedfd40 100644 --- a/src/gb/core.c +++ b/src/gb/core.c @@ -984,6 +984,7 @@ static void _GBVLPStartFrameCallback(void *context) { GBVideoProxyRendererUnshim(&gb->video, &gbcore->proxyRenderer); mVideoLogContextRewind(gbcore->logContext, core); GBVideoProxyRendererShim(&gb->video, &gbcore->proxyRenderer); + gb->earlyExit = true; } } @@ -999,6 +1000,7 @@ static bool _GBVLPInit(struct mCore* core) { gbcore->logCallbacks.videoFrameStarted = _GBVLPStartFrameCallback; gbcore->logCallbacks.context = core; core->addCoreCallbacks(core, &gbcore->logCallbacks); + core->videoLogger = gbcore->proxyRenderer.logger; return true; } diff --git a/src/gb/video.c b/src/gb/video.c index 8aa978292..cfe80f913 100644 --- a/src/gb/video.c +++ b/src/gb/video.c @@ -343,19 +343,19 @@ void _updateFrameCount(struct mTiming* timing, void* context, uint32_t cyclesLat mTimingSchedule(timing, &video->frameEvent, 4 - ((video->p->cpu->executionState + 1) & 3)); return; } + if (!GBRegisterLCDCIsEnable(video->p->memory.io[REG_LCDC])) { + mTimingSchedule(timing, &video->frameEvent, GB_VIDEO_TOTAL_LENGTH); + } - GBFrameEnded(video->p); - mCoreSyncPostFrame(video->p->sync); --video->frameskipCounter; if (video->frameskipCounter < 0) { video->renderer->finishFrame(video->renderer); video->frameskipCounter = video->frameskip; } + GBFrameEnded(video->p); + mCoreSyncPostFrame(video->p->sync); ++video->frameCounter; - if (!GBRegisterLCDCIsEnable(video->p->memory.io[REG_LCDC])) { - mTimingSchedule(timing, &video->frameEvent, GB_VIDEO_TOTAL_LENGTH); - } GBFrameStarted(video->p); } From cffff67c4989c391ea30f6cc6647fa46bd17cb9a Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sun, 2 Jun 2019 22:57:23 -0700 Subject: [PATCH 22/50] Qt: Better handling of GB sprite coords --- src/platform/qt/AssetView.cpp | 6 +++--- src/platform/qt/AssetView.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/platform/qt/AssetView.cpp b/src/platform/qt/AssetView.cpp index 97d44578d..f77b779bb 100644 --- a/src/platform/qt/AssetView.cpp +++ b/src/platform/qt/AssetView.cpp @@ -260,10 +260,10 @@ bool AssetView::lookupObjGB(int id, struct ObjInfo* info) { palette, 0, 2, - obj->y != 0 && obj->y < 160, + obj->y != 0 && obj->y < 160 && obj->x != 0 && obj->x < 168, GBObjAttributesGetPriority(obj->attr), - obj->x, - obj->y, + obj->x - 8, + obj->y - 16, bool(GBObjAttributesIsXFlip(obj->attr)), bool(GBObjAttributesIsYFlip(obj->attr)), }; diff --git a/src/platform/qt/AssetView.h b/src/platform/qt/AssetView.h index acc4a1454..c3d3a92b2 100644 --- a/src/platform/qt/AssetView.h +++ b/src/platform/qt/AssetView.h @@ -55,8 +55,8 @@ protected: bool enabled : 1; unsigned priority : 2; - unsigned x : 9; - unsigned y : 9; + int x : 10; + int y : 10; bool hflip : 1; bool vflip : 1; QTransform xform; From c6b61d512382169d771fc3a0e8202352289a7f3b Mon Sep 17 00:00:00 2001 From: Lothar Serra Mari Date: Mon, 3 Jun 2019 18:32:13 +0200 Subject: [PATCH 23/50] Qt: Update German GUI translation Add translation for the "Reset" string in FrameView --- src/platform/qt/ts/mgba-de.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/platform/qt/ts/mgba-de.ts b/src/platform/qt/ts/mgba-de.ts index fc55841be..baba6f57c 100644 --- a/src/platform/qt/ts/mgba-de.ts +++ b/src/platform/qt/ts/mgba-de.ts @@ -282,15 +282,20 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd.Hintergrundfarbe - + Disable scanline effects Scanline-Effekte deaktivieren - + Export Exportieren + + + Reset + Zurücksetzen + GIFView @@ -1358,42 +1363,42 @@ Game Boy Advance ist ein eingetragenes Warenzeichen von Nintendo Co., Ltd. QGBA::FrameView - + Export frame Bild exportieren - + Portable Network Graphics (*.png) Portable Network Graphics (*.png) - + None Keine - + Background Hintergrund - + Window Fenster - + Sprite Sprite - + Backdrop Hintergrund - + %1 %2 %1 %2 From 982bc486b0dac7ae7fb91e58b28d8f5dc5364c03 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Mon, 3 Jun 2019 09:49:54 -0700 Subject: [PATCH 24/50] Feature: Fix video logger with no channel backing --- src/feature/video-logger.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/feature/video-logger.c b/src/feature/video-logger.c index a2884a107..a240d2c33 100644 --- a/src/feature/video-logger.c +++ b/src/feature/video-logger.c @@ -293,7 +293,7 @@ void mVideoLoggerWriteBuffer(struct mVideoLogger* logger, uint32_t bufferId, uin bool mVideoLoggerRendererRun(struct mVideoLogger* logger, bool block) { struct mVideoLogChannel* channel = logger->dataContext; uint32_t ignorePackets = 0; - if (channel->injectionPoint == LOGGER_INJECTION_IMMEDIATE && !channel->injecting) { + if (channel && channel->injectionPoint == LOGGER_INJECTION_IMMEDIATE && !channel->injecting) { mVideoLoggerRendererRunInjected(logger); ignorePackets = channel->ignorePackets; } @@ -304,7 +304,7 @@ bool mVideoLoggerRendererRun(struct mVideoLogger* logger, bool block) { } switch (item.type) { case DIRTY_SCANLINE: - if (channel->injectionPoint == LOGGER_INJECTION_FIRST_SCANLINE && !channel->injecting && item.address == 0) { + if (channel && channel->injectionPoint == LOGGER_INJECTION_FIRST_SCANLINE && !channel->injecting && item.address == 0) { mVideoLoggerRendererRunInjected(logger); ignorePackets = channel->ignorePackets; } From 2ef05b9aad15e43a9898bd427b7160d51f313ab0 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Mon, 3 Jun 2019 11:16:48 -0700 Subject: [PATCH 25/50] Python: cffi 1.12.3 is broken --- src/platform/python/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/python/setup.py b/src/platform/python/setup.py index c9030313c..e610f551f 100644 --- a/src/platform/python/setup.py +++ b/src/platform/python/setup.py @@ -21,8 +21,8 @@ setup( author_email="jeffrey@endrift.com", url="http://github.com/mgba-emu/mgba/", packages=["mgba"], - setup_requires=['cffi>=1.6', 'pytest-runner'], - install_requires=['cffi>=1.6', 'cached-property'], + setup_requires=['cffi>=1.6,!=1.12.3', 'pytest-runner'], + install_requires=['cffi>=1.6,!=1.12.3', 'cached-property'], extras_require={'pil': ['Pillow>=2.3'], 'cinema': ['pyyaml', 'pytest']}, tests_require=['pytest'], cffi_modules=["_builder.py:ffi"], From a1cdd65e19e73e4ddfbd828582f83d6fb0c48fe0 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Mon, 3 Jun 2019 11:46:57 -0700 Subject: [PATCH 26/50] GBA Video: Add missing initializers --- src/gba/extra/proxy.c | 11 +++++++++++ src/gba/renderers/gl.c | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/gba/extra/proxy.c b/src/gba/extra/proxy.c index 6121f4f99..196329036 100644 --- a/src/gba/extra/proxy.c +++ b/src/gba/extra/proxy.c @@ -44,6 +44,17 @@ void GBAVideoProxyRendererCreate(struct GBAVideoProxyRenderer* renderer, struct renderer->d.disableBG[3] = false; renderer->d.disableOBJ = false; + renderer->d.highlightBG[0] = false; + renderer->d.highlightBG[1] = false; + renderer->d.highlightBG[2] = false; + renderer->d.highlightBG[3] = false; + int i; + for (i = 0; i < 128; ++i) { + renderer->d.highlightOBJ[i] = false; + } + renderer->d.highlightColor = 0xFFFFFF; + renderer->d.highlightAmount = 0; + renderer->logger->context = renderer; renderer->logger->parsePacket = _parsePacket; renderer->logger->handleEvent = _handleEvent; diff --git a/src/gba/renderers/gl.c b/src/gba/renderers/gl.c index 97acc8e02..952467eea 100644 --- a/src/gba/renderers/gl.c +++ b/src/gba/renderers/gl.c @@ -651,6 +651,17 @@ void GBAVideoGLRendererCreate(struct GBAVideoGLRenderer* renderer) { renderer->d.disableBG[3] = false; renderer->d.disableOBJ = false; + renderer->d.highlightBG[0] = false; + renderer->d.highlightBG[1] = false; + renderer->d.highlightBG[2] = false; + renderer->d.highlightBG[3] = false; + int i; + for (i = 0; i < 128; ++i) { + renderer->d.highlightOBJ[i] = false; + } + renderer->d.highlightColor = 0xFFFFFF; + renderer->d.highlightAmount = 0; + renderer->scale = 1; } From 4a2d8d078b14485772909ec157414282043189af Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Mon, 3 Jun 2019 15:40:41 -0700 Subject: [PATCH 27/50] GBA Video: Fix color normalization in GL --- src/gba/renderers/gl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gba/renderers/gl.c b/src/gba/renderers/gl.c index 952467eea..1153d2d3c 100644 --- a/src/gba/renderers/gl.c +++ b/src/gba/renderers/gl.c @@ -1380,7 +1380,7 @@ void _drawScanlines(struct GBAVideoGLRenderer* glRenderer, int y) { glScissor(0, glRenderer->firstY, 1, y - glRenderer->firstY + 1); glBindFramebuffer(GL_FRAMEBUFFER, glRenderer->fbo[GBA_GL_FBO_BACKDROP]); glDrawBuffers(2, (GLenum[]) { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }); - glClearBufferfv(GL_COLOR, 0, (GLfloat[]) { ((backdrop >> 16) & 0xFF) / 256., ((backdrop >> 8) & 0xFF) / 256., (backdrop & 0xFF) / 256., 1.f }); + glClearBufferfv(GL_COLOR, 0, (GLfloat[]) { ((backdrop >> 16) & 0xF8) / 248., ((backdrop >> 8) & 0xF8) / 248., (backdrop & 0xF8) / 248., 1.f }); glClearBufferiv(GL_COLOR, 1, (GLint[]) { 32, glRenderer->target1Bd | (glRenderer->target2Bd * 2) | (glRenderer->blendEffect * 4), glRenderer->blda, 0 }); glDrawBuffers(1, (GLenum[]) { GL_COLOR_ATTACHMENT0 }); From 42818c764da9400a8dc9aef368711c786cff5884 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 4 Jun 2019 12:53:04 -0700 Subject: [PATCH 28/50] GBA Core: Separate mVL proxy from generic proxy --- src/gba/core.c | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/gba/core.c b/src/gba/core.c index 67864c409..98d4d036a 100644 --- a/src/gba/core.c +++ b/src/gba/core.c @@ -133,6 +133,7 @@ struct GBACore { #if defined(BUILD_GLES2) || defined(BUILD_GLES3) struct GBAVideoGLRenderer glRenderer; #endif + struct GBAVideoProxyRenderer vlProxy; struct GBAVideoProxyRenderer proxyRenderer; struct mVideoLogContext* logContext; struct mCoreCallbacks logCallbacks; @@ -188,6 +189,7 @@ static bool _GBACoreInit(struct mCore* core) { #ifndef DISABLE_THREADING mVideoThreadProxyCreate(&gbacore->threadProxy); #endif + gbacore->vlProxy.logger = NULL; gbacore->proxyRenderer.logger = NULL; gbacore->keys = 0; @@ -978,22 +980,22 @@ static void _GBACoreStartVideoLog(struct mCore* core, struct mVideoLogContext* c state->cpu.gprs[ARM_PC] = BASE_WORKING_RAM; int channelId = mVideoLoggerAddChannel(context); - gbacore->proxyRenderer.logger = malloc(sizeof(struct mVideoLogger)); - mVideoLoggerRendererCreate(gbacore->proxyRenderer.logger, false); - mVideoLoggerAttachChannel(gbacore->proxyRenderer.logger, context, channelId); - gbacore->proxyRenderer.logger->block = false; + gbacore->vlProxy.logger = malloc(sizeof(struct mVideoLogger)); + mVideoLoggerRendererCreate(gbacore->vlProxy.logger, false); + mVideoLoggerAttachChannel(gbacore->vlProxy.logger, context, channelId); + gbacore->vlProxy.logger->block = false; - GBAVideoProxyRendererCreate(&gbacore->proxyRenderer, &gbacore->renderer.d); - GBAVideoProxyRendererShim(&gba->video, &gbacore->proxyRenderer); + GBAVideoProxyRendererCreate(&gbacore->vlProxy, gba->video.renderer); + GBAVideoProxyRendererShim(&gba->video, &gbacore->vlProxy); } static void _GBACoreEndVideoLog(struct mCore* core) { struct GBACore* gbacore = (struct GBACore*) core; struct GBA* gba = core->board; - if (gbacore->proxyRenderer.logger) { - GBAVideoProxyRendererUnshim(&gba->video, &gbacore->proxyRenderer); - free(gbacore->proxyRenderer.logger); - gbacore->proxyRenderer.logger = NULL; + if (gbacore->vlProxy.logger) { + GBAVideoProxyRendererUnshim(&gba->video, &gbacore->vlProxy); + free(gbacore->vlProxy.logger); + gbacore->vlProxy.logger = NULL; } } #endif @@ -1090,10 +1092,10 @@ static void _GBAVLPStartFrameCallback(void *context) { struct GBACore* gbacore = (struct GBACore*) core; struct GBA* gba = core->board; - if (!mVideoLoggerRendererRun(gbacore->proxyRenderer.logger, true)) { - GBAVideoProxyRendererUnshim(&gba->video, &gbacore->proxyRenderer); + if (!mVideoLoggerRendererRun(gbacore->vlProxy.logger, true)) { + GBAVideoProxyRendererUnshim(&gba->video, &gbacore->vlProxy); mVideoLogContextRewind(gbacore->logContext, core); - GBAVideoProxyRendererShim(&gba->video, &gbacore->proxyRenderer); + GBAVideoProxyRendererShim(&gba->video, &gbacore->vlProxy); gba->earlyExit = true; } } @@ -1103,14 +1105,14 @@ static bool _GBAVLPInit(struct mCore* core) { if (!_GBACoreInit(core)) { return false; } - gbacore->proxyRenderer.logger = malloc(sizeof(struct mVideoLogger)); - mVideoLoggerRendererCreate(gbacore->proxyRenderer.logger, true); - GBAVideoProxyRendererCreate(&gbacore->proxyRenderer, NULL); + gbacore->vlProxy.logger = malloc(sizeof(struct mVideoLogger)); + mVideoLoggerRendererCreate(gbacore->vlProxy.logger, true); + GBAVideoProxyRendererCreate(&gbacore->vlProxy, NULL); memset(&gbacore->logCallbacks, 0, sizeof(gbacore->logCallbacks)); gbacore->logCallbacks.videoFrameStarted = _GBAVLPStartFrameCallback; gbacore->logCallbacks.context = core; core->addCoreCallbacks(core, &gbacore->logCallbacks); - core->videoLogger = gbacore->proxyRenderer.logger; + core->videoLogger = gbacore->vlProxy.logger; return true; } @@ -1125,8 +1127,8 @@ static void _GBAVLPDeinit(struct mCore* core) { static void _GBAVLPReset(struct mCore* core) { struct GBACore* gbacore = (struct GBACore*) core; struct GBA* gba = (struct GBA*) core->board; - if (gba->video.renderer == &gbacore->proxyRenderer.d) { - GBAVideoProxyRendererUnshim(&gba->video, &gbacore->proxyRenderer); + if (gba->video.renderer == &gbacore->vlProxy.d) { + GBAVideoProxyRendererUnshim(&gba->video, &gbacore->vlProxy); } else if (gbacore->renderer.outputBuffer) { struct GBAVideoRenderer* renderer = &gbacore->renderer.d; GBAVideoAssociateRenderer(&gba->video, renderer); @@ -1134,7 +1136,7 @@ static void _GBAVLPReset(struct mCore* core) { ARMReset(core->cpu); mVideoLogContextRewind(gbacore->logContext, core); - GBAVideoProxyRendererShim(&gba->video, &gbacore->proxyRenderer); + GBAVideoProxyRendererShim(&gba->video, &gbacore->vlProxy); // Make sure CPU loop never spins GBAHalt(gba); @@ -1150,7 +1152,7 @@ static bool _GBAVLPLoadROM(struct mCore* core, struct VFile* vf) { gbacore->logContext = NULL; return false; } - mVideoLoggerAttachChannel(gbacore->proxyRenderer.logger, gbacore->logContext, 0); + mVideoLoggerAttachChannel(gbacore->vlProxy.logger, gbacore->logContext, 0); return true; } From f2134e6b62dd18fe7245bda5674e0a1555c6a3ef Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 4 Jun 2019 12:56:50 -0700 Subject: [PATCH 29/50] Qt: Only allow one Frame Inspector to be open --- src/platform/qt/Window.cpp | 16 +++++++++++++++- src/platform/qt/Window.h | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index fc4d9a6e2..980506472 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -1470,7 +1470,21 @@ void Window::setupMenu(QMenuBar* menubar) { addGameAction(tr("View &map..."), "mapWindow", openControllerTView(), "tools"); #ifdef M_CORE_GBA - Action* frameWindow = addGameAction(tr("&Frame inspector..."), "frameWindow", openControllerTView(), "tools"); + Action* frameWindow = addGameAction(tr("&Frame inspector..."), "frameWindow", [this]() { + if (!m_frameView) { + m_frameView = new FrameView(m_controller); + connect(this, &Window::shutdown, this, [this]() { + if (m_frameView) { + m_frameView->close(); + } + }); + connect(m_frameView, &QObject::destroyed, this, [this]() { + m_frameView = nullptr; + }); + m_frameView->setAttribute(Qt::WA_DeleteOnClose); + } + m_frameView->show(); + }, "tools"); m_platformActions.insert(PLATFORM_GBA, frameWindow); #endif diff --git a/src/platform/qt/Window.h b/src/platform/qt/Window.h index 526a10496..b41944fa0 100644 --- a/src/platform/qt/Window.h +++ b/src/platform/qt/Window.h @@ -32,6 +32,7 @@ class CoreController; class CoreManager; class DebuggerConsoleController; class Display; +class FrameView; class GDBController; class GIFView; class LibraryController; @@ -215,6 +216,7 @@ private: std::unique_ptr m_overrideView; std::unique_ptr m_sensorView; + FrameView* m_frameView = nullptr; #ifdef USE_FFMPEG VideoView* m_videoView = nullptr; From d048917b72d008f0c0776cfc9f3defd9f6c72ef7 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 4 Jun 2019 14:20:10 -0700 Subject: [PATCH 30/50] Qt: Cap audio buffer size to 8192 --- CHANGES | 1 + src/platform/qt/SettingsView.cpp | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c49cc0538..ae38339f6 100644 --- a/CHANGES +++ b/CHANGES @@ -27,6 +27,7 @@ Other fixes: - Switch: Fix threading-related crash on second launch - Qt: Fix FPS target maxing out at 59.727 (fixes mgba.io/i/1421) - Core: Fix crashes if core directories aren't set + - Qt: Cap audio buffer size to 8192 (fixes mgba.io/i/1433) Misc: - GBA Savedata: EEPROM performance fixes - GBA Savedata: Automatically map 1Mbit Flash files as 1Mbit Flash diff --git a/src/platform/qt/SettingsView.cpp b/src/platform/qt/SettingsView.cpp index 75a639b8a..62a1d43f0 100644 --- a/src/platform/qt/SettingsView.cpp +++ b/src/platform/qt/SettingsView.cpp @@ -369,7 +369,6 @@ void SettingsView::updateConfig() { saveSetting("useCgbColors", m_ui.useCgbColors); saveSetting("useBios", m_ui.useBios); saveSetting("skipBios", m_ui.skipBios); - saveSetting("audioBuffers", m_ui.audioBufferSize); saveSetting("sampleRate", m_ui.sampleRate); saveSetting("videoSync", m_ui.videoSync); saveSetting("audioSync", m_ui.audioSync); @@ -407,6 +406,11 @@ void SettingsView::updateConfig() { saveSetting("useDiscordPresence", m_ui.useDiscordPresence); saveSetting("gba.audioHle", m_ui.audioHle); + if (m_ui.audioBufferSize->currentText().toInt() > 8192) { + m_ui.audioBufferSize->setCurrentText("8192"); + } + saveSetting("audioBuffers", m_ui.audioBufferSize); + if (m_ui.fastForwardUnbounded->isChecked()) { saveSetting("fastForwardRatio", "-1"); } else { From 4787eb29c5d953bac921281edae17920e149cb62 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 4 Jun 2019 16:21:53 -0700 Subject: [PATCH 31/50] GBA SIO: Stop using bitfield structs --- include/mgba/internal/gba/sio.h | 48 +++++++++++++-------------------- src/gba/extra/battlechip.c | 12 ++++----- src/gba/hardware.c | 4 +-- src/gba/sio/lockstep.c | 38 +++++++++++++------------- 4 files changed, 45 insertions(+), 57 deletions(-) diff --git a/include/mgba/internal/gba/sio.h b/include/mgba/internal/gba/sio.h index 07ede7009..df4cd2f7a 100644 --- a/include/mgba/internal/gba/sio.h +++ b/include/mgba/internal/gba/sio.h @@ -33,6 +33,23 @@ enum { JOYSTAT_RECV_BIT = 2, }; +DECL_BITFIELD(GBASIONormal, uint16_t); +DECL_BIT(GBASIONormal, Sc, 0); +DECL_BIT(GBASIONormal, InternalSc, 1); +DECL_BIT(GBASIONormal, Si, 2); +DECL_BIT(GBASIONormal, IdleSo, 3); +DECL_BIT(GBASIONormal, Start, 7); +DECL_BIT(GBASIONormal, Length, 12); +DECL_BIT(GBASIONormal, Irq, 14); +DECL_BITFIELD(GBASIOMultiplayer, uint16_t); +DECL_BITS(GBASIOMultiplayer, Baud, 0, 2); +DECL_BIT(GBASIOMultiplayer, Slave, 2); +DECL_BIT(GBASIOMultiplayer, Ready, 3); +DECL_BITS(GBASIOMultiplayer, Id, 4, 2); +DECL_BIT(GBASIOMultiplayer, Error, 6); +DECL_BIT(GBASIOMultiplayer, Busy, 8); +DECL_BIT(GBASIOMultiplayer, Irq, 14); + struct GBASIODriverSet { struct GBASIODriver* normal; struct GBASIODriver* multiplayer; @@ -47,36 +64,7 @@ struct GBASIO { struct GBASIODriver* activeDriver; uint16_t rcnt; - // TODO: Convert to bitfields - union { - struct { - unsigned sc : 1; - unsigned internalSc : 1; - unsigned si : 1; - unsigned idleSo : 1; - unsigned : 3; - unsigned start : 1; - unsigned : 4; - unsigned length : 1; - unsigned : 1; - unsigned irq : 1; - unsigned : 1; - } normalControl; - - struct { - unsigned baud : 2; - unsigned slave : 1; - unsigned ready : 1; - unsigned id : 2; - unsigned error : 1; - unsigned busy : 1; - unsigned : 6; - unsigned irq : 1; - unsigned : 1; - } multiplayerControl; - - uint16_t siocnt; - }; + uint16_t siocnt; }; void GBASIOInit(struct GBASIO* sio); diff --git a/src/gba/extra/battlechip.c b/src/gba/extra/battlechip.c index fb411eef5..ae8aedff0 100644 --- a/src/gba/extra/battlechip.c +++ b/src/gba/extra/battlechip.c @@ -87,7 +87,7 @@ void _battlechipTransfer(struct GBASIOBattlechipGate* gate) { if (gate->d.p->mode == SIO_NORMAL_32) { cycles = GBA_ARM7TDMI_FREQUENCY / 0x40000; } else { - cycles = GBASIOCyclesPerTransfer[gate->d.p->multiplayerControl.baud][1]; + cycles = GBASIOCyclesPerTransfer[GBASIOMultiplayerGetBaud(gate->d.p->siocnt)][1]; } mTimingDeschedule(&gate->d.p->p->timing, &gate->event); mTimingSchedule(&gate->d.p->p->timing, &gate->event, cycles); @@ -100,8 +100,8 @@ void _battlechipTransferEvent(struct mTiming* timing, void* user, uint32_t cycle if (gate->d.p->mode == SIO_NORMAL_32) { gate->d.p->p->memory.io[REG_SIODATA32_LO >> 1] = 0; gate->d.p->p->memory.io[REG_SIODATA32_HI >> 1] = 0; - gate->d.p->normalControl.start = 0; - if (gate->d.p->normalControl.irq) { + gate->d.p->siocnt = GBASIONormalClearStart(gate->d.p->siocnt); + if (GBASIONormalIsIrq(gate->d.p->siocnt)) { GBARaiseIRQ(gate->d.p->p, IRQ_SIO, cyclesLate); } return; @@ -112,8 +112,8 @@ void _battlechipTransferEvent(struct mTiming* timing, void* user, uint32_t cycle gate->d.p->p->memory.io[REG_SIOMULTI0 >> 1] = cmd; gate->d.p->p->memory.io[REG_SIOMULTI2 >> 1] = 0xFFFF; gate->d.p->p->memory.io[REG_SIOMULTI3 >> 1] = 0xFFFF; - gate->d.p->multiplayerControl.busy = 0; - gate->d.p->multiplayerControl.id = 0; + gate->d.p->siocnt = GBASIOMultiplayerClearBusy(gate->d.p->siocnt); + gate->d.p->siocnt = GBASIOMultiplayerSetId(gate->d.p->siocnt, 0); mLOG(GBA_BATTLECHIP, DEBUG, "Game: %04X (%i)", cmd, gate->state); @@ -193,7 +193,7 @@ void _battlechipTransferEvent(struct mTiming* timing, void* user, uint32_t cycle gate->d.p->p->memory.io[REG_SIOMULTI1 >> 1] = reply; - if (gate->d.p->multiplayerControl.irq) { + if (GBASIOMultiplayerIsIrq(gate->d.p->siocnt)) { GBARaiseIRQ(gate->d.p->p, IRQ_SIO, cyclesLate); } } diff --git a/src/gba/hardware.c b/src/gba/hardware.c index c2c19d50e..dedbd9af3 100644 --- a/src/gba/hardware.c +++ b/src/gba/hardware.c @@ -584,10 +584,10 @@ void _gbpSioProcessEvents(struct mTiming* timing, void* user, uint32_t cyclesLat ++gbp->p->gbpTxPosition; gbp->p->p->memory.io[REG_SIODATA32_LO >> 1] = tx; gbp->p->p->memory.io[REG_SIODATA32_HI >> 1] = tx >> 16; - if (gbp->d.p->normalControl.irq) { + if (GBASIONormalIsIrq(gbp->d.p->siocnt)) { GBARaiseIRQ(gbp->p->p, IRQ_SIO, cyclesLate); } - gbp->d.p->normalControl.start = 0; + gbp->d.p->siocnt = GBASIONormalClearStart(gbp->d.p->siocnt); gbp->p->p->memory.io[REG_SIOCNT >> 1] = gbp->d.p->siocnt & ~0x0080; } diff --git a/src/gba/sio/lockstep.c b/src/gba/sio/lockstep.c index 356fa130a..506854a7e 100644 --- a/src/gba/sio/lockstep.c +++ b/src/gba/sio/lockstep.c @@ -71,7 +71,7 @@ void GBASIOLockstepDetachNode(struct GBASIOLockstep* lockstep, struct GBASIOLock bool GBASIOLockstepNodeInit(struct GBASIODriver* driver) { struct GBASIOLockstepNode* node = (struct GBASIOLockstepNode*) driver; - node->d.p->multiplayerControl.slave = node->id > 0; + node->d.p->siocnt = GBASIOMultiplayerSetSlave(node->d.p->siocnt, node->id > 0); mLOG(GBA_SIO, DEBUG, "Lockstep %i: Node init", node->id); node->event.context = node; node->event.name = "GBA SIO Lockstep"; @@ -99,10 +99,10 @@ bool GBASIOLockstepNodeLoad(struct GBASIODriver* driver) { node->d.writeRegister = GBASIOLockstepNodeMultiWriteRegister; node->d.p->rcnt |= 3; ATOMIC_ADD(node->p->attachedMulti, 1); - node->d.p->multiplayerControl.ready = node->p->attachedMulti == node->p->d.attached; + node->d.p->siocnt = GBASIOMultiplayerSetReady(node->d.p->siocnt, node->p->attachedMulti == node->p->d.attached); if (node->id) { node->d.p->rcnt |= 4; - node->d.p->multiplayerControl.slave = 1; + node->d.p->siocnt = GBASIOMultiplayerFillSlave(node->d.p->siocnt); } break; case SIO_NORMAL_32: @@ -178,10 +178,10 @@ static uint16_t GBASIOLockstepNodeMultiWriteRegister(struct GBASIODriver* driver ATOMIC_LOAD(transferActive, node->p->d.transferActive); if (value & 0x0080 && transferActive == TRANSFER_IDLE) { - if (!node->id && node->d.p->multiplayerControl.ready) { + if (!node->id && GBASIOMultiplayerIsReady(node->d.p->siocnt)) { mLOG(GBA_SIO, DEBUG, "Lockstep %i: Transfer initiated", node->id); ATOMIC_STORE(node->p->d.transferActive, TRANSFER_STARTING); - ATOMIC_STORE(node->p->d.transferCycles, GBASIOCyclesPerTransfer[node->d.p->multiplayerControl.baud][node->p->d.attached - 1]); + ATOMIC_STORE(node->p->d.transferCycles, GBASIOCyclesPerTransfer[GBASIOMultiplayerGetBaud(node->d.p->siocnt)][node->p->d.attached - 1]); bool scheduled = mTimingIsScheduled(&driver->p->p->timing, &node->event); int oldWhen = node->event.when; @@ -220,37 +220,37 @@ static void _finishTransfer(struct GBASIOLockstepNode* node) { sio->p->memory.io[REG_SIOMULTI2 >> 1] = node->p->multiRecv[2]; sio->p->memory.io[REG_SIOMULTI3 >> 1] = node->p->multiRecv[3]; sio->rcnt |= 1; - sio->multiplayerControl.busy = 0; - sio->multiplayerControl.id = node->id; - if (sio->multiplayerControl.irq) { + sio->siocnt = GBASIOMultiplayerClearBusy(sio->siocnt); + sio->siocnt = GBASIOMultiplayerSetId(sio->siocnt, node->id); + if (GBASIOMultiplayerIsIrq(sio->siocnt)) { GBARaiseIRQ(sio->p, IRQ_SIO, 0); } break; case SIO_NORMAL_8: // TODO - sio->normalControl.start = 0; + sio->siocnt = GBASIONormalClearStart(sio->siocnt); if (node->id) { - sio->normalControl.si = node->p->players[node->id - 1]->d.p->normalControl.idleSo; + sio->siocnt = GBASIONormalSetSi(sio->siocnt, GBASIONormalGetIdleSo(node->p->players[node->id - 1]->d.p->siocnt)); node->d.p->p->memory.io[REG_SIODATA8 >> 1] = node->p->normalRecv[node->id - 1] & 0xFF; } else { node->d.p->p->memory.io[REG_SIODATA8 >> 1] = 0xFFFF; } - if (sio->multiplayerControl.irq) { + if (GBASIONormalIsIrq(sio->siocnt)) { GBARaiseIRQ(sio->p, IRQ_SIO, 0); } break; case SIO_NORMAL_32: // TODO - sio->normalControl.start = 0; + sio->siocnt = GBASIONormalClearStart(sio->siocnt); if (node->id) { - sio->normalControl.si = node->p->players[node->id - 1]->d.p->normalControl.idleSo; + sio->siocnt = GBASIONormalSetSi(sio->siocnt, GBASIONormalGetIdleSo(node->p->players[node->id - 1]->d.p->siocnt)); node->d.p->p->memory.io[REG_SIODATA32_LO >> 1] = node->p->normalRecv[node->id - 1]; node->d.p->p->memory.io[REG_SIODATA32_HI >> 1] |= node->p->normalRecv[node->id - 1] >> 16; } else { node->d.p->p->memory.io[REG_SIODATA32_LO >> 1] = 0xFFFF; node->d.p->p->memory.io[REG_SIODATA32_HI >> 1] = 0xFFFF; } - if (sio->multiplayerControl.irq) { + if (GBASIONormalIsIrq(sio->siocnt)) { GBARaiseIRQ(sio->p, IRQ_SIO, 0); } break; @@ -278,7 +278,7 @@ static int32_t _masterUpdate(struct GBASIOLockstepNode* node) { case TRANSFER_IDLE: // If the master hasn't initiated a transfer, it can keep going. node->nextEvent += LOCKSTEP_INCREMENT; - node->d.p->multiplayerControl.ready = attachedMulti == attached; + node->d.p->siocnt = GBASIOMultiplayerSetReady(node->d.p->siocnt, attachedMulti == attached); break; case TRANSFER_STARTING: // Start the transfer, but wait for the other GBAs to catch up @@ -352,11 +352,11 @@ static uint32_t _slaveUpdate(struct GBASIOLockstepNode* node) { ATOMIC_LOAD(attachedMulti, node->p->attachedMulti); ATOMIC_LOAD(attached, node->p->d.attached); - node->d.p->multiplayerControl.ready = attachedMulti == attached; + node->d.p->siocnt = GBASIOMultiplayerSetReady(node->d.p->siocnt, attachedMulti == attached); bool signal = false; switch (transferActive) { case TRANSFER_IDLE: - if (!node->d.p->multiplayerControl.ready) { + if (!GBASIOMultiplayerIsReady(node->d.p->siocnt)) { node->p->d.addCycles(&node->p->d, node->id, LOCKSTEP_INCREMENT); } break; @@ -376,7 +376,7 @@ static uint32_t _slaveUpdate(struct GBASIOLockstepNode* node) { node->d.p->p->memory.io[REG_SIOMULTI1 >> 1] = 0xFFFF; node->d.p->p->memory.io[REG_SIOMULTI2 >> 1] = 0xFFFF; node->d.p->p->memory.io[REG_SIOMULTI3 >> 1] = 0xFFFF; - node->d.p->multiplayerControl.busy = 1; + node->d.p->siocnt = GBASIOMultiplayerFillBusy(node->d.p->siocnt); break; case SIO_NORMAL_8: node->p->multiRecv[node->id] = 0xFFFF; @@ -455,7 +455,7 @@ static uint16_t GBASIOLockstepNodeNormalWriteRegister(struct GBASIODriver* drive mLOG(GBA_SIO, DEBUG, "Lockstep %i: SIOCNT <- %04x", node->id, value); value &= 0xFF8B; if (!node->id) { - driver->p->normalControl.si = 1; + driver->p->siocnt = GBASIONormalFillSi(driver->p->siocnt); } if (value & 0x0080 && !node->id) { // Internal shift clock From 5c11ea8c27810c7d9f556595d4b737fdcfe6a198 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 4 Jun 2019 16:26:11 -0700 Subject: [PATCH 32/50] GBA: Work around CFFI regression --- include/mgba/internal/gba/hardware.h | 4 ++++ src/platform/python/_builder.py | 12 ++++++++++++ src/platform/python/setup.py | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/include/mgba/internal/gba/hardware.h b/include/mgba/internal/gba/hardware.h index 540a9fdf4..fab175538 100644 --- a/include/mgba/internal/gba/hardware.h +++ b/include/mgba/internal/gba/hardware.h @@ -66,6 +66,7 @@ DECL_BITS(RTCCommandData, Magic, 0, 4); DECL_BITS(RTCCommandData, Command, 4, 3); DECL_BIT(RTCCommandData, Reading, 7); +#ifndef PYCPARSE #pragma pack(push, 1) struct GBARTC { int32_t bytesRemaining; @@ -78,6 +79,9 @@ struct GBARTC { uint8_t time[7]; }; #pragma pack(pop) +#else +struct GBATRC; +#endif struct GBAGBPKeyCallback { struct mKeyCallback d; diff --git a/src/platform/python/_builder.py b/src/platform/python/_builder.py index 43bcddc19..6b76ee698 100644 --- a/src/platform/python/_builder.py +++ b/src/platform/python/_builder.py @@ -65,6 +65,18 @@ for line in preprocessed.splitlines(): lines.append(line) ffi.cdef('\n'.join(lines)) +ffi.cdef(""" +struct GBARTC { + int32_t bytesRemaining; + int32_t transferStep; + int32_t bitsRead; + int32_t bits; + int32_t commandActive; + RTCCommandData command; + RTCControl control; + uint8_t time[7]; +};""", packed=True) + preprocessed = subprocess.check_output(cpp + ["-fno-inline", "-P"] + cppflags + [os.path.join(pydir, "lib.h")], universal_newlines=True) lines = [] diff --git a/src/platform/python/setup.py b/src/platform/python/setup.py index e610f551f..c9030313c 100644 --- a/src/platform/python/setup.py +++ b/src/platform/python/setup.py @@ -21,8 +21,8 @@ setup( author_email="jeffrey@endrift.com", url="http://github.com/mgba-emu/mgba/", packages=["mgba"], - setup_requires=['cffi>=1.6,!=1.12.3', 'pytest-runner'], - install_requires=['cffi>=1.6,!=1.12.3', 'cached-property'], + setup_requires=['cffi>=1.6', 'pytest-runner'], + install_requires=['cffi>=1.6', 'cached-property'], extras_require={'pil': ['Pillow>=2.3'], 'cinema': ['pyyaml', 'pytest']}, tests_require=['pytest'], cffi_modules=["_builder.py:ffi"], From 9b0e4af7b430ee1cd4bbc9b7a37768a820c2b236 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 4 Jun 2019 20:38:44 -0700 Subject: [PATCH 33/50] GBA Video: Fix GL output ivec rank --- src/gba/renderers/gl.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gba/renderers/gl.c b/src/gba/renderers/gl.c index 1153d2d3c..01d9f118d 100644 --- a/src/gba/renderers/gl.c +++ b/src/gba/renderers/gl.c @@ -442,7 +442,7 @@ static const char* const _renderObj = "uniform ivec4 mosaic;\n" "OUT(0) out vec4 color;\n" "OUT(1) out ivec4 flags;\n" - "OUT(2) out ivec3 window;\n" + "OUT(2) out ivec4 window;\n" "vec4 renderTile(int tile, int paletteId, ivec2 localCoord);\n" @@ -467,7 +467,7 @@ static const char* const _renderObj = " color = pix;\n" " flags = inflags;\n" " gl_FragDepth = float(flags.x) / 16.;\n" - " window = objwin.yzw;\n" + " window = ivec4(objwin.yzw, 0);\n" "}"; static const struct GBAVideoGLUniform _uniformsWindow[] = { @@ -488,7 +488,7 @@ static const char* const _renderWindow = "uniform ivec3 flags;\n" "uniform ivec4 win0[160];\n" "uniform ivec4 win1[160];\n" - "OUT(0) out ivec3 window;\n" + "OUT(0) out ivec4 window;\n" "void crop(vec4 windowParams, int flags, inout ivec3 windowFlags) {\n" " bvec4 compare = lessThan(texCoord.xxyy, windowParams);\n" @@ -526,7 +526,7 @@ static const char* const _renderWindow = "void main() {\n" " int dispflags = (dispcnt & 0x1F) | 0x20;\n" " if ((dispcnt & 0xE0) == 0) {\n" - " window = ivec3(dispflags, blend);\n" + " window = ivec4(dispflags, blend, 0);\n" " } else {\n" " ivec3 windowFlags = ivec3(flags.z, blend);\n" " if ((dispcnt & 0x40) != 0) { \n" @@ -535,7 +535,7 @@ static const char* const _renderWindow = " if ((dispcnt & 0x20) != 0) { \n" " crop(interpolate(win0), flags.x, windowFlags);\n" " }\n" - " window = windowFlags;\n" + " window = ivec4(windowFlags, 0);\n" " }\n" "}\n"; From 9ac838d14df205480010d9aa6464ef2c6aa389f2 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 4 Jun 2019 22:32:09 -0700 Subject: [PATCH 34/50] Switch: Option to use built-in brightness sensor for Boktai --- CHANGES | 1 + src/platform/switch/main.c | 50 +++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ae38339f6..a96f51dcb 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,7 @@ Features: - Experimental high level "XQ" audio for most GBA games - Interframe blending for games that use flicker effects - Frame inspector for dissecting and debugging rendering + - Switch: Option to use built-in brightness sensor for Boktai Emulation fixes: - GBA: All IRQs have 7 cycle delay (fixes mgba.io/i/539, mgba.io/i/1208) - GBA: Reset now reloads multiboot ROMs diff --git a/src/platform/switch/main.c b/src/platform/switch/main.c index 86d6abdfb..3b5124e83 100644 --- a/src/platform/switch/main.c +++ b/src/platform/switch/main.c @@ -99,6 +99,8 @@ static u8 vmode; static u32 vwidth; static u32 vheight; static bool interframeBlending = false; +static bool useLightSensor = true; +static struct mGUIRunnerLux lightSensor; static enum ScreenMode { SM_PA, @@ -268,6 +270,10 @@ static void _setup(struct mGUIRunner* runner) { runner->core->setPeripheral(runner->core, mPERIPH_ROTATION, &rotation); runner->core->setAVStream(runner->core, &stream); + if (runner->core->platform(runner->core) == PLATFORM_GBA && useLightSensor) { + runner->core->setPeripheral(runner->core, mPERIPH_GBA_LUMINANCE, &lightSensor.d); + } + unsigned mode; if (mCoreConfigGetUIntValue(&runner->config, "screenMode", &mode) && mode < SM_MAX) { screenMode = mode; @@ -292,6 +298,19 @@ static void _gameLoaded(struct mGUIRunner* runner) { if (mCoreConfigGetIntValue(&runner->config, "interframeBlending", &fakeBool)) { interframeBlending = fakeBool; } + if (mCoreConfigGetIntValue(&runner->config, "useLightSensor", &fakeBool)) { + if (useLightSensor != fakeBool) { + useLightSensor = fakeBool; + + if (runner->core->platform(runner->core) == PLATFORM_GBA) { + if (useLightSensor) { + runner->core->setPeripheral(runner->core, mPERIPH_GBA_LUMINANCE, &lightSensor.d); + } else { + runner->core->setPeripheral(runner->core, mPERIPH_GBA_LUMINANCE, &runner->luminanceSource.d); + } + } + } + } rumble.up = 0; rumble.down = 0; @@ -543,6 +562,18 @@ int32_t _readGyroZ(struct mRotationSource* source) { return sixaxis.gyroscope.z * -1.1e9f; } +static void _lightSensorSample(struct GBALuminanceSource* lux) { + struct mGUIRunnerLux* runnerLux = (struct mGUIRunnerLux*) lux; + float luxLevel = 0; + appletGetCurrentIlluminance(&luxLevel); + runnerLux->luxLevel = cbrtf(luxLevel) * 8; +} + +static uint8_t _lightSensorRead(struct GBALuminanceSource* lux) { + struct mGUIRunnerLux* runnerLux = (struct mGUIRunnerLux*) lux; + return 0xFF - runnerLux->luxLevel; +} + static int _batteryState(void) { u32 charge; int state = 0; @@ -690,6 +721,9 @@ int main(int argc, char* argv[]) { rotation.readTiltY = _readTiltY; rotation.readGyroZ = _readGyroZ; + lightSensor.d.readLuminance = _lightSensorRead; + lightSensor.d.sample = _lightSensorSample; + stream.videoDimensionsChanged = NULL; stream.postVideoFrame = NULL; stream.postAudioFrame = NULL; @@ -707,6 +741,9 @@ int main(int argc, char* argv[]) { audoutBuffer[i].data_offset = 0; } + bool illuminanceAvailable = false; + appletIsIlluminanceAvailable(&illuminanceAvailable); + struct mGUIRunner runner = { .params = { 1280, 720, @@ -829,8 +866,19 @@ int main(int argc, char* argv[]) { }, .nStates = 6 }, + { + .title = "Use built-in brightness sensor for Boktai", + .data = "useLightSensor", + .submenu = 0, + .state = illuminanceAvailable, + .validStates = (const char*[]) { + "Off", + "On", + }, + .nStates = 2 + }, }, - .nConfigExtra = 4, + .nConfigExtra = 5, .setup = _setup, .teardown = NULL, .gameLoaded = _gameLoaded, From c5fc0f0492e0c94128b2286b65ec5e2ab8d1211a Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Wed, 5 Jun 2019 10:06:41 -0700 Subject: [PATCH 35/50] Qt: Remove excess memcpying (fixes #1437) --- src/platform/qt/CoreController.cpp | 30 ++++++++++-------------------- src/platform/qt/CoreController.h | 3 +-- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/platform/qt/CoreController.cpp b/src/platform/qt/CoreController.cpp index 4608a1b45..cd4195420 100644 --- a/src/platform/qt/CoreController.cpp +++ b/src/platform/qt/CoreController.cpp @@ -82,8 +82,7 @@ CoreController::CoreController(mCore* core, QObject* parent) controller->m_resetActions.clear(); if (!controller->m_hwaccel) { - controller->m_activeBuffer = &controller->m_buffers[0]; - context->core->setVideoBuffer(context->core, reinterpret_cast(controller->m_activeBuffer->data()), controller->screenDimensions().width()); + context->core->setVideoBuffer(context->core, reinterpret_cast(controller->m_activeBuffer.data()), controller->screenDimensions().width()); } QMetaObject::invokeMethod(controller, "didReset"); @@ -357,15 +356,12 @@ void CoreController::setLogger(LogController* logger) { void CoreController::start() { if (!m_hwaccel) { - QSize size(1024, 2048); - m_buffers[0].resize(size.width() * size.height() * sizeof(color_t)); - m_buffers[1].resize(size.width() * size.height() * sizeof(color_t)); - m_buffers[0].fill(0xFF); - m_buffers[1].fill(0xFF); - m_activeBuffer = &m_buffers[0]; - m_completeBuffer = m_buffers[0]; + QSize size(256, 224); + m_activeBuffer.resize(size.width() * size.height() * sizeof(color_t)); + m_activeBuffer.fill(0xFF); + m_completeBuffer = m_activeBuffer; - m_threadContext.core->setVideoBuffer(m_threadContext.core, reinterpret_cast(m_activeBuffer->data()), size.width()); + m_threadContext.core->setVideoBuffer(m_threadContext.core, reinterpret_cast(m_activeBuffer.data()), size.width()); } if (!m_patched) { @@ -884,17 +880,11 @@ int CoreController::updateAutofire() { void CoreController::finishFrame() { if (!m_hwaccel) { - QMutexLocker locker(&m_bufferMutex); - memcpy(m_completeBuffer.data(), m_activeBuffer->constData(), m_activeBuffer->size()); + unsigned width, height; + m_threadContext.core->desiredVideoDimensions(m_threadContext.core, &width, &height); - // TODO: Generalize this to triple buffering? - m_activeBuffer = &m_buffers[0]; - if (m_activeBuffer == m_completeBuffer) { - m_activeBuffer = &m_buffers[1]; - } - // Copy contents to avoid issues when doing frameskip - memcpy(m_activeBuffer->data(), m_completeBuffer.constData(), m_activeBuffer->size()); - m_threadContext.core->setVideoBuffer(m_threadContext.core, reinterpret_cast(m_activeBuffer->data()), screenDimensions().width()); + QMutexLocker locker(&m_bufferMutex); + memcpy(m_completeBuffer.data(), m_activeBuffer.constData(), 256 * height * BYTES_PER_PIXEL); } QMutexLocker locker(&m_actionMutex); diff --git a/src/platform/qt/CoreController.h b/src/platform/qt/CoreController.h index 5463bf76b..e0ba677f7 100644 --- a/src/platform/qt/CoreController.h +++ b/src/platform/qt/CoreController.h @@ -201,8 +201,7 @@ private: bool m_patched = false; - QByteArray m_buffers[2]; - QByteArray* m_activeBuffer; + QByteArray m_activeBuffer; QByteArray m_completeBuffer; bool m_hwaccel = false; From e34c529f7edda021bcf6121b503eaf2f62eb9481 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Wed, 5 Jun 2019 12:55:30 -0700 Subject: [PATCH 36/50] Ports: Ability to enable or disable all SGB features (closes #1205) --- CHANGES | 1 + src/feature/gui/gui-config.c | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index a96f51dcb..1dbc740ea 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,7 @@ Features: - Interframe blending for games that use flicker effects - Frame inspector for dissecting and debugging rendering - Switch: Option to use built-in brightness sensor for Boktai + - Ports: Ability to enable or disable all SGB features (closes mgba.io/i/1205) Emulation fixes: - GBA: All IRQs have 7 cycle delay (fixes mgba.io/i/539, mgba.io/i/1208) - GBA: Reset now reloads multiboot ROMs diff --git a/src/feature/gui/gui-config.c b/src/feature/gui/gui-config.c index 484465b43..4695115a3 100644 --- a/src/feature/gui/gui-config.c +++ b/src/feature/gui/gui-config.c @@ -118,6 +118,20 @@ void mGUIShowConfig(struct mGUIRunner* runner, struct GUIMenuItem* extra, size_t }, .nStates = 2 }; + *GUIMenuItemListAppend(&menu.items) = (struct GUIMenuItem) { + .title = "Enable SGB features", + .data = "sgb.model", + .submenu = 0, + .state = true, + .validStates = (const char*[]) { + "Off", "On" + }, + .stateMappings = (const struct GUIVariant[]) { + GUI_V_S("DMG"), + GUI_V_S("SGB"), + }, + .nStates = 2 + }; *GUIMenuItemListAppend(&menu.items) = (struct GUIMenuItem) { .title = "Enable SGB borders", .data = "sgb.borders", @@ -173,8 +187,6 @@ void mGUIShowConfig(struct mGUIRunner* runner, struct GUIMenuItem* extra, size_t continue; } if (item->stateMappings) { - item->state = 0; - size_t j; for (j = 0; j < item->nStates; ++j) { const struct GUIVariant* v = &item->stateMappings[j]; From 1a6b422b4c4ade66c97d589e8be7306f849fdb05 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Wed, 5 Jun 2019 22:04:55 -0700 Subject: [PATCH 37/50] CMake: Fix libretro version-info dep (fixes #1438) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ccebef30..50ec9fc3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -998,7 +998,7 @@ endif() if(BUILD_LIBRETRO) file(GLOB RETRO_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/libretro/*.c) add_library(${BINARY_NAME}_libretro SHARED ${CORE_SRC} ${RETRO_SRC}) - add_dependencies(${BINARY_NAME} version-info) + add_dependencies(${BINARY_NAME}_libretro version-info) set_target_properties(${BINARY_NAME}_libretro PROPERTIES PREFIX "" COMPILE_DEFINITIONS "__LIBRETRO__;COLOR_16_BIT;COLOR_5_6_5;DISABLE_THREADING;${OS_DEFINES};${FUNCTION_DEFINES};MINIMAL_CORE=2") target_link_libraries(${BINARY_NAME}_libretro ${OS_LIB}) if(MSVC) From 9b9aeb0c2bd76a1f8bb8d6d0fc6b20c7432bbaed Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 6 Jun 2019 14:14:14 -0700 Subject: [PATCH 38/50] GBA Core: Fix libretro build (fixes #1439) --- src/gba/core.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gba/core.c b/src/gba/core.c index 98d4d036a..20c08779e 100644 --- a/src/gba/core.c +++ b/src/gba/core.c @@ -133,9 +133,11 @@ struct GBACore { #if defined(BUILD_GLES2) || defined(BUILD_GLES3) struct GBAVideoGLRenderer glRenderer; #endif +#ifndef MINIMAL_CORE struct GBAVideoProxyRenderer vlProxy; struct GBAVideoProxyRenderer proxyRenderer; struct mVideoLogContext* logContext; +#endif struct mCoreCallbacks logCallbacks; #ifndef DISABLE_THREADING struct mVideoThreadProxy threadProxy; @@ -167,7 +169,9 @@ static bool _GBACoreInit(struct mCore* core) { gbacore->overrides = NULL; gbacore->debuggerPlatform = NULL; gbacore->cheatDevice = NULL; +#ifndef MINIMAL_CORE gbacore->logContext = NULL; +#endif gbacore->audioMixer = NULL; GBACreate(gba); @@ -189,8 +193,10 @@ static bool _GBACoreInit(struct mCore* core) { #ifndef DISABLE_THREADING mVideoThreadProxyCreate(&gbacore->threadProxy); #endif +#ifndef MINIMAL_CORE gbacore->vlProxy.logger = NULL; gbacore->proxyRenderer.logger = NULL; +#endif gbacore->keys = 0; gba->keySource = &gbacore->keys; @@ -466,11 +472,13 @@ static void _GBACoreReset(struct mCore* core) { } } #endif +#ifndef MINIMAL_CORE if (core->videoLogger) { gbacore->proxyRenderer.logger = core->videoLogger; GBAVideoProxyRendererCreate(&gbacore->proxyRenderer, renderer); renderer = &gbacore->proxyRenderer.d; } +#endif GBAVideoAssociateRenderer(&gba->video, renderer); } From 7b12516df45ac2bfc0597d6a584e80f8b4721255 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 6 Jun 2019 15:54:35 -0700 Subject: [PATCH 39/50] Vita: L2/R2 and L3/R3 can now be mapped on PSTV (fixes #1292) --- CHANGES | 1 + src/platform/psp2/main.c | 14 +++++++------- src/platform/psp2/psp2-context.c | 18 +++++++++++++++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index 1dbc740ea..c3ed94cd7 100644 --- a/CHANGES +++ b/CHANGES @@ -51,6 +51,7 @@ Misc: - Qt: Improve sync code - Switch: Dynamic display resizing - Qt: Make mute menu option also toggle fast-forward mute (fixes mgba.io/i/1424) + - Vita: L2/R2 and L3/R3 can now be mapped on PSTV (fixes mgba.io/i/1292) 0.7.2: (2019-05-25) Emulation fixes: diff --git a/src/platform/psp2/main.c b/src/platform/psp2/main.c index 4dad587d8..594d9942f 100644 --- a/src/platform/psp2/main.c +++ b/src/platform/psp2/main.c @@ -41,7 +41,7 @@ static void _drawEnd(void) { static uint32_t _pollInput(const struct mInputMap* map) { SceCtrlData pad; - sceCtrlPeekBufferPositive(0, &pad, 1); + sceCtrlPeekBufferPositiveExt2(0, &pad, 1); int input = mInputMapKeyBits(map, PSP2_INPUT, pad.buttons, 0); if (pad.buttons & SCE_CTRL_UP || pad.ly < 64) { @@ -127,17 +127,17 @@ int main() { .id = PSP2_INPUT, .keyNames = (const char*[]) { "Select", - 0, - 0, + "L3", + "R3", "Start", "Up", "Right", "Down", "Left", - "L", - "R", - 0, // L2? - 0, // R2? + "L2", + "R2", + "L1", + "R1", "\1\xC", "\1\xA", "\1\xB", diff --git a/src/platform/psp2/psp2-context.c b/src/platform/psp2/psp2-context.c index f33738e65..8b5c6301d 100644 --- a/src/platform/psp2/psp2-context.c +++ b/src/platform/psp2/psp2-context.c @@ -267,7 +267,7 @@ static void _postAudioBuffer(struct mAVStream* stream, blip_t* left, blip_t* rig uint16_t mPSP2PollInput(struct mGUIRunner* runner) { SceCtrlData pad; - sceCtrlPeekBufferPositive(0, &pad, 1); + sceCtrlPeekBufferPositiveExt2(0, &pad, 1); int activeKeys = mInputMapKeyBits(&runner->core->inputMap, PSP2_INPUT, pad.buttons, 0); int angles = mInputMapAxis(&runner->core->inputMap, PSP2_INPUT, 0, pad.ly); @@ -313,8 +313,8 @@ void mPSP2Setup(struct mGUIRunner* runner) { mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_DOWN, GBA_KEY_DOWN); mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_LEFT, GBA_KEY_LEFT); mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_RIGHT, GBA_KEY_RIGHT); - mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_LTRIGGER, GBA_KEY_L); - mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_RTRIGGER, GBA_KEY_R); + mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_L1, GBA_KEY_L); + mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_R1, GBA_KEY_R); struct mInputAxis desc = { GBA_KEY_DOWN, GBA_KEY_UP, 192, 64 }; mInputBindAxis(&runner->core->inputMap, PSP2_INPUT, 0, &desc); @@ -398,6 +398,18 @@ void mPSP2LoadROM(struct mGUIRunner* runner) { interframeBlending = fakeBool; } + // Backcompat: Old versions of mGBA use an older binding system that has different mappings for L/R + if (!sceKernelIsPSVitaTV()) { + int key = mInputMapKey(&runner->core->inputMap, PSP2_INPUT, __builtin_ctz(SCE_CTRL_L2)); + if (key >= 0) { + mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_L1, key); + } + key = mInputMapKey(&runner->core->inputMap, PSP2_INPUT, __builtin_ctz(SCE_CTRL_R2)); + if (key >= 0) { + mPSP2MapKey(&runner->core->inputMap, SCE_CTRL_R1, key); + } + } + MutexInit(&audioContext.mutex); ConditionInit(&audioContext.cond); memset(audioContext.buffer, 0, sizeof(audioContext.buffer)); From 81476720e2f0fc450ae2be62f7c888e774626715 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 6 Jun 2019 16:15:07 -0700 Subject: [PATCH 40/50] GB Serialize: Fix loading non-BIOS state from BIOS (fixes #1280) --- CHANGES | 1 + include/mgba/internal/gb/gb.h | 1 + src/gb/gb.c | 19 +++++++++++-------- src/gb/io.c | 2 ++ src/gb/serialize.c | 16 ++++++++++++++++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index c3ed94cd7..76d22caf6 100644 --- a/CHANGES +++ b/CHANGES @@ -30,6 +30,7 @@ Other fixes: - Qt: Fix FPS target maxing out at 59.727 (fixes mgba.io/i/1421) - Core: Fix crashes if core directories aren't set - Qt: Cap audio buffer size to 8192 (fixes mgba.io/i/1433) + - GB Serialize: Fix loading non-BIOS state from BIOS (fixes mgba.io/i/1280) Misc: - GBA Savedata: EEPROM performance fixes - GBA Savedata: Automatically map 1Mbit Flash files as 1Mbit Flash diff --git a/include/mgba/internal/gb/gb.h b/include/mgba/internal/gb/gb.h index ac9d6230d..7d007296a 100644 --- a/include/mgba/internal/gb/gb.h +++ b/include/mgba/internal/gb/gb.h @@ -150,6 +150,7 @@ void GBDestroy(struct GB* gb); void GBReset(struct LR35902Core* cpu); void GBSkipBIOS(struct GB* gb); +void GBMapBIOS(struct GB* gb); void GBUnmapBIOS(struct GB* gb); void GBDetectModel(struct GB* gb); diff --git a/src/gb/gb.c b/src/gb/gb.c index 5f5178a90..a03eb36ff 100644 --- a/src/gb/gb.c +++ b/src/gb/gb.c @@ -419,14 +419,7 @@ void GBReset(struct LR35902Core* cpu) { gb->biosVf->close(gb->biosVf); gb->biosVf = NULL; } else { - gb->biosVf->seek(gb->biosVf, 0, SEEK_SET); - gb->memory.romBase = malloc(GB_SIZE_CART_BANK0); - ssize_t size = gb->biosVf->read(gb->biosVf, gb->memory.romBase, GB_SIZE_CART_BANK0); - memcpy(&gb->memory.romBase[size], &gb->memory.rom[size], GB_SIZE_CART_BANK0 - size); - if (size > 0x100) { - memcpy(&gb->memory.romBase[0x100], &gb->memory.rom[0x100], sizeof(struct GBCartridge)); - } - + GBMapBIOS(gb); cpu->a = 0; cpu->f.packed = 0; cpu->c = 0; @@ -563,6 +556,16 @@ void GBSkipBIOS(struct GB* gb) { } } +void GBMapBIOS(struct GB* gb) { + gb->biosVf->seek(gb->biosVf, 0, SEEK_SET); + gb->memory.romBase = malloc(GB_SIZE_CART_BANK0); + ssize_t size = gb->biosVf->read(gb->biosVf, gb->memory.romBase, GB_SIZE_CART_BANK0); + memcpy(&gb->memory.romBase[size], &gb->memory.rom[size], GB_SIZE_CART_BANK0 - size); + if (size > 0x100) { + memcpy(&gb->memory.romBase[0x100], &gb->memory.rom[0x100], sizeof(struct GBCartridge)); + } +} + void GBUnmapBIOS(struct GB* gb) { if (gb->memory.romBase < gb->memory.rom || gb->memory.romBase > &gb->memory.rom[gb->memory.romSize - 1]) { free(gb->memory.romBase); diff --git a/src/gb/io.c b/src/gb/io.c index beecce12a..c7b08dd03 100644 --- a/src/gb/io.c +++ b/src/gb/io.c @@ -185,8 +185,10 @@ void GBIOReset(struct GB* gb) { GBIOWrite(gb, REG_NR51, 0xF3); if (!gb->biosVf) { GBIOWrite(gb, REG_LCDC, 0x91); + gb->memory.io[0x50] = 1; } else { GBIOWrite(gb, REG_LCDC, 0x00); + gb->memory.io[0x50] = 0xFF; } GBIOWrite(gb, REG_SCY, 0x00); GBIOWrite(gb, REG_SCX, 0x00); diff --git a/src/gb/serialize.c b/src/gb/serialize.c index 7a4f5de4b..dfeced1ba 100644 --- a/src/gb/serialize.c +++ b/src/gb/serialize.c @@ -135,6 +135,16 @@ bool GBDeserialize(struct GB* gb, const struct GBSerializedState* state) { if (ucheck16 >= 0x40) { mLOG(GB_STATE, WARN, "Savestate is corrupted: OCPS is out of range"); } + bool differentBios = !gb->biosVf || gb->model != state->model; + if (state->io[0x50] == 0xFF) { + if (differentBios) { + mLOG(GB_STATE, WARN, "Incompatible savestate, please restart with correct BIOS in %s mode", GBModelToName(state->model)); + error = true; + } else { + // TODO: Make it work correctly + mLOG(GB_STATE, WARN, "ILoading savestate in BIOS. This may not work correctly"); + } + } if (error) { return false; } @@ -187,6 +197,12 @@ bool GBDeserialize(struct GB* gb, const struct GBSerializedState* state) { GBTimerDeserialize(&gb->timer, state); GBAudioDeserialize(&gb->audio, state); + if (gb->memory.io[0x50] == 0xFF) { + GBMapBIOS(gb); + } else { + GBUnmapBIOS(gb); + } + if (gb->model & GB_MODEL_SGB && canSgb) { GBSGBDeserialize(gb, state); } From 8c55de4b5f55c45182b78d815f741e597610021c Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 6 Jun 2019 16:27:52 -0700 Subject: [PATCH 41/50] README: Mention disco docker container --- README.md | 1 + README_DE.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 9042c0ead..803e1de0f 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ This will produce a `build-win32` directory with the build products. Replace `mg - mgba/ubuntu:xenial - mgba/ubuntu:bionic - mgba/ubuntu:cosmic +- mgba/ubuntu:disco - mgba/vita - mgba/wii - mgba/windows:w32 diff --git a/README_DE.md b/README_DE.md index 172b5f826..8a1fae9ed 100644 --- a/README_DE.md +++ b/README_DE.md @@ -124,6 +124,7 @@ Dieser Befehl erzeugt ein Verzeichnis `build-win32` mit den erzeugten Programmda - mgba/ubuntu:xenial - mgba/ubuntu:bionic - mgba/ubuntu:cosmic +- mgba/ubuntu:disco - mgba/vita - mgba/wii - mgba/windows:w32 From ace3bd57f7274db5544a8714b136287622d4ade9 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 6 Jun 2019 18:38:25 -0700 Subject: [PATCH 42/50] GB Serialize: Fix typo --- src/gb/serialize.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gb/serialize.c b/src/gb/serialize.c index dfeced1ba..78d455300 100644 --- a/src/gb/serialize.c +++ b/src/gb/serialize.c @@ -142,7 +142,7 @@ bool GBDeserialize(struct GB* gb, const struct GBSerializedState* state) { error = true; } else { // TODO: Make it work correctly - mLOG(GB_STATE, WARN, "ILoading savestate in BIOS. This may not work correctly"); + mLOG(GB_STATE, WARN, "Loading savestate in BIOS. This may not work correctly"); } } if (error) { From ff8f03ab74c20ce387dbbd20a57c317dfd225ab5 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 6 Jun 2019 23:36:35 -0700 Subject: [PATCH 43/50] GBA Video: Fix 512x512 backgrounds in GL --- src/gba/renderers/gl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gba/renderers/gl.c b/src/gba/renderers/gl.c index 01d9f118d..72ddd1d8c 100644 --- a/src/gba/renderers/gl.c +++ b/src/gba/renderers/gl.c @@ -155,7 +155,7 @@ static const char* const _renderMode0 = " if ((size & 1) == 1) {\n" " coord.y += coord.x & 256;\n" " }\n" - " coord &= ivec2(255, 511);\n" + " coord &= ivec2(255, 1023);\n" " int mapAddress = screenBase + (coord.x >> 3) + (coord.y >> 3) * 32;\n" " vec4 map = texelFetch(vram, ivec2(mapAddress & 255, mapAddress >> 8), 0);\n" " int tileFlags = int(map.g * 15.9);\n" From e9aff885a267c228999958ea590441064951c289 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 6 Jun 2019 19:26:54 -0700 Subject: [PATCH 44/50] Vita: Add SGB cropping --- src/platform/psp2/psp2-context.c | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/platform/psp2/psp2-context.c b/src/platform/psp2/psp2-context.c index 8b5c6301d..7e96f24fb 100644 --- a/src/platform/psp2/psp2-context.c +++ b/src/platform/psp2/psp2-context.c @@ -56,6 +56,7 @@ static vita2d_texture* oldTex; static vita2d_texture* screenshot; static Thread audioThread; static bool interframeBlending = false; +static bool sgbCrop = false; static struct mSceRotationSource { struct mRotationSource d; @@ -365,6 +366,10 @@ void mPSP2Setup(struct mGUIRunner* runner) { if (mCoreConfigGetUIntValue(&runner->config, "camera", &mode)) { camera.cam = mode; } + int fakeBool; + if (mCoreConfigGetIntValue(&runner->config, "sgb.borderCrop", &fakeBool)) { + sgbCrop = fakeBool; + } } void mPSP2LoadROM(struct mGUIRunner* runner) { @@ -473,8 +478,13 @@ void mPSP2Unpaused(struct mGUIRunner* runner) { } int fakeBool; - mCoreConfigGetIntValue(&runner->config, "interframeBlending", &fakeBool); - interframeBlending = fakeBool; + if (mCoreConfigGetIntValue(&runner->config, "interframeBlending", &fakeBool)) { + interframeBlending = fakeBool; + } + + if (mCoreConfigGetIntValue(&runner->config, "sgb.borderCrop", &fakeBool)) { + sgbCrop = fakeBool; + } } void mPSP2Teardown(struct mGUIRunner* runner) { @@ -519,6 +529,13 @@ void _drawTex(vita2d_texture* t, unsigned width, unsigned height, bool faded, bo vita2d_draw_texture_tint(backdrop, 0, 0, tint); // Fall through case SM_PLAIN: + if (sgbCrop && width == 256 && height == 224) { + w = 768; + h = 672; + scalex = 3; + scaley = 3; + break; + } w = 960 / width; h = 544 / height; if (w * height > 544) { @@ -533,6 +550,13 @@ void _drawTex(vita2d_texture* t, unsigned width, unsigned height, bool faded, bo scaley = scalex; break; case SM_ASPECT: + if (sgbCrop && width == 256 && height == 224) { + w = 967; + h = 846; + scalex = 34.0f / 9.0f; + scaley = scalex; + break; + } w = 960 / aspectw; h = 544 / aspecth; if (w * aspecth > 544) { From aab47e52f5261e714785c31c953f1a28ad8ad9ff Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 7 Jun 2019 00:15:27 -0700 Subject: [PATCH 45/50] Qt: Fix Software display driver frame sizing --- src/platform/qt/DisplayGL.cpp | 1 - src/platform/qt/Window.cpp | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/qt/DisplayGL.cpp b/src/platform/qt/DisplayGL.cpp index 86460606e..ad8f7ba4f 100644 --- a/src/platform/qt/DisplayGL.cpp +++ b/src/platform/qt/DisplayGL.cpp @@ -111,7 +111,6 @@ void DisplayGL::startDrawing(std::shared_ptr controller) { messagePainter()->resize(size(), isAspectRatioLocked(), devicePixelRatio()); #endif resizePainter(); - connect(m_context.get(), &CoreController::didReset, this, &DisplayGL::resizeContext); } void DisplayGL::stopDrawing() { diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index 980506472..8b7762a8b 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -906,6 +906,7 @@ void Window::reloadDisplayDriver() { connect(m_controller.get(), &CoreController::unpaused, m_display.get(), &Display::unpauseDrawing); connect(m_controller.get(), &CoreController::frameAvailable, m_display.get(), &Display::framePosted); connect(m_controller.get(), &CoreController::statusPosted, m_display.get(), &Display::showMessage); + connect(m_controller.get(), &CoreController::didReset, m_display.get(), &Display::resizeContext); attachWidget(m_display.get()); m_display->startDrawing(m_controller); @@ -1814,6 +1815,7 @@ void Window::setController(CoreController* controller, const QString& fname) { connect(m_controller.get(), &CoreController::unpaused, m_display.get(), &Display::unpauseDrawing); connect(m_controller.get(), &CoreController::frameAvailable, m_display.get(), &Display::framePosted); connect(m_controller.get(), &CoreController::statusPosted, m_display.get(), &Display::showMessage); + connect(m_controller.get(), &CoreController::didReset, m_display.get(), &Display::resizeContext); connect(m_controller.get(), &CoreController::unpaused, &m_inputController, &InputController::suspendScreensaver); connect(m_controller.get(), &CoreController::frameAvailable, this, &Window::recordFrame); From 101d80dca32cf2cab879600db2c6c959ea57c442 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 7 Jun 2019 11:20:34 -0700 Subject: [PATCH 46/50] Switch: Add SGB cropping --- src/platform/switch/main.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/platform/switch/main.c b/src/platform/switch/main.c index 3b5124e83..c0624423d 100644 --- a/src/platform/switch/main.c +++ b/src/platform/switch/main.c @@ -6,6 +6,7 @@ #include "feature/gui/gui-runner.h" #include #include +#include #include #include #include @@ -99,6 +100,7 @@ static u8 vmode; static u32 vwidth; static u32 vheight; static bool interframeBlending = false; +static bool sgbCrop = false; static bool useLightSensor = true; static struct mGUIRunnerLux lightSensor; @@ -298,6 +300,9 @@ static void _gameLoaded(struct mGUIRunner* runner) { if (mCoreConfigGetIntValue(&runner->config, "interframeBlending", &fakeBool)) { interframeBlending = fakeBool; } + if (mCoreConfigGetIntValue(&runner->config, "sgb.borderCrop", &fakeBool)) { + sgbCrop = fakeBool; + } if (mCoreConfigGetIntValue(&runner->config, "useLightSensor", &fakeBool)) { if (useLightSensor != fakeBool) { useLightSensor = fakeBool; @@ -332,8 +337,14 @@ static void _drawTex(struct mGUIRunner* runner, unsigned width, unsigned height, glUseProgram(program); glBindVertexArray(vao); - float aspectX = width / (float) vwidth; - float aspectY = height / (float) vheight; + float inwidth = width; + float inheight = height; + if (sgbCrop && width == 256 && height == 224) { + inwidth = GB_VIDEO_HORIZONTAL_PIXELS; + inheight = GB_VIDEO_VERTICAL_PIXELS; + } + float aspectX = inwidth / vwidth; + float aspectY = inheight / vheight; float max = 1.f; switch (screenMode) { case SM_PA: @@ -359,6 +370,11 @@ static void _drawTex(struct mGUIRunner* runner, unsigned width, unsigned height, break; } + if (screenMode != SM_SF) { + aspectX = width / (float) vwidth; + aspectY = height / (float) vheight; + } + aspectX *= max; aspectY *= max; From ea4c16042422a2b0a3848fd990fe5f4e26e44530 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 7 Jun 2019 11:32:52 -0700 Subject: [PATCH 47/50] Wii: Add SGB cropping --- src/platform/wii/main.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/platform/wii/main.c b/src/platform/wii/main.c index eea6b17d6..20cd2445b 100644 --- a/src/platform/wii/main.c +++ b/src/platform/wii/main.c @@ -17,6 +17,7 @@ #include #include #include "feature/gui/gui-runner.h" +#include #include #include #include @@ -113,6 +114,7 @@ static uint16_t* rescaleTexmem; static GXTexObj rescaleTex; static uint16_t* interframeTexmem; static GXTexObj interframeTex; +static bool sgbCrop = false; static int32_t tiltX; static int32_t tiltY; static int32_t gyroZ; @@ -862,6 +864,9 @@ void _unpaused(struct mGUIRunner* runner) { if (mCoreConfigGetIntValue(&runner->config, "interframeBlending", &fakeBool)) { interframeBlending = fakeBool; } + if (mCoreConfigGetIntValue(&runner->config, "sgb.borderCrop", &fakeBool)) { + sgbCrop = fakeBool; + } float stretch; if (mCoreConfigGetFloatValue(&runner->config, "stretchWidth", &stretch)) { @@ -952,20 +957,25 @@ void _drawFrame(struct mGUIRunner* runner, bool faded) { } } - int hfactor = (vmode->fbWidth * wStretch) / (corew * wAdjust); - int vfactor = (vmode->efbHeight * hStretch) / (coreh * hAdjust); - if (hfactor > vfactor) { - scaleFactor = vfactor; - } else { - scaleFactor = hfactor; - } - if (screenMode == SM_PA) { + unsigned factorWidth = corew; + unsigned factorHeight = coreh; + if (sgbCrop && factorWidth == 256 && factorHeight == 224) { + factorWidth = GB_VIDEO_HORIZONTAL_PIXELS; + factorHeight = GB_VIDEO_VERTICAL_PIXELS; + } + + int hfactor = (vmode->fbWidth * wStretch) / (factorWidth * wAdjust); + int vfactor = (vmode->efbHeight * hStretch) / (factorHeight * hAdjust); + if (hfactor > vfactor) { + scaleFactor = vfactor; + } else { + scaleFactor = hfactor; + } + vertWidth *= scaleFactor; vertHeight *= scaleFactor; - } - if (screenMode == SM_PA) { _reproj(corew * scaleFactor, coreh * scaleFactor); } else { _reproj2(corew, coreh); From 62e39558485d896d794e912663915349f25ae390 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 7 Jun 2019 12:11:57 -0700 Subject: [PATCH 48/50] 3DS: Add SGB cropping --- src/platform/3ds/main.c | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/platform/3ds/main.c b/src/platform/3ds/main.c index 9bd228a72..f5af6d50b 100644 --- a/src/platform/3ds/main.c +++ b/src/platform/3ds/main.c @@ -104,6 +104,7 @@ static bool frameStarted = false; static C3D_RenderTarget* upscaleBuffer; static C3D_Tex upscaleBufferTex; static bool interframeBlending = false; +static bool sgbCrop = false; static aptHookCookie cookie; static bool core2; @@ -382,6 +383,10 @@ static void _gameLoaded(struct mGUIRunner* runner) { if (mCoreConfigGetIntValue(&runner->config, "interframeBlending", &fakeBool)) { interframeBlending = fakeBool; } + + if (mCoreConfigGetIntValue(&runner->config, "sgb.borderCrop", &fakeBool)) { + sgbCrop = fakeBool; + } } static void _gameUnloaded(struct mGUIRunner* runner) { @@ -437,6 +442,12 @@ static void _drawTex(struct mCore* core, bool faded, bool both) { int w = corew; int h = coreh; + if (sgbCrop && w == 256 && h == 224) { + w = GB_VIDEO_HORIZONTAL_PIXELS; + h = GB_VIDEO_VERTICAL_PIXELS; + } + int innerw = w; + int innerh = h; // Get greatest common divisor while (w != 0) { int temp = h % w; @@ -444,8 +455,8 @@ static void _drawTex(struct mCore* core, bool faded, bool both) { w = temp; } int gcd = h; - unsigned aspectw = corew / gcd; - unsigned aspecth = coreh / gcd; + unsigned aspectw = innerw / gcd; + unsigned aspecth = innerh / gcd; int x = 0; int y = 0; @@ -517,6 +528,8 @@ static void _drawTex(struct mCore* core, bool faded, bool both) { } ctrFlushBatch(); + innerw = corew; + innerh = coreh; corew = w; coreh = h; screen_h = 240; @@ -529,19 +542,20 @@ static void _drawTex(struct mCore* core, bool faded, bool both) { } ctrSetViewportSize(screen_w, screen_h, true); + float afw, afh; switch (screenMode) { default: return; case SM_AF_TOP: case SM_AF_BOTTOM: - w = screen_w / aspectw; - h = screen_h / aspecth; - if (w * aspecth > screen_h) { - w = aspectw * h; - h = aspecth * h; + afw = screen_w / (float) aspectw; + afh = screen_h / (float) aspecth; + if (afw * aspecth > screen_h) { + w = innerw * afh / gcd; + h = innerh * afh / gcd; } else { - h = aspecth * w; - w = aspectw * w; + h = innerh * afw / gcd; + w = innerw * afw / gcd; } break; case SM_SF_TOP: From 1928d2b5fc234df201165c6e2e137264822ffd74 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 7 Jun 2019 12:13:20 -0700 Subject: [PATCH 49/50] Ports: Ability to crop SGB borders off screen (closes #1204) --- CHANGES | 1 + src/feature/gui/gui-config.c | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGES b/CHANGES index 76d22caf6..c2d7397ec 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,7 @@ Features: - Frame inspector for dissecting and debugging rendering - Switch: Option to use built-in brightness sensor for Boktai - Ports: Ability to enable or disable all SGB features (closes mgba.io/i/1205) + - Ports: Ability to crop SGB borders off screen (closes mgba.io/i/1204) Emulation fixes: - GBA: All IRQs have 7 cycle delay (fixes mgba.io/i/539, mgba.io/i/1208) - GBA: Reset now reloads multiboot ROMs diff --git a/src/feature/gui/gui-config.c b/src/feature/gui/gui-config.c index 4695115a3..37b239ba3 100644 --- a/src/feature/gui/gui-config.c +++ b/src/feature/gui/gui-config.c @@ -142,6 +142,16 @@ void mGUIShowConfig(struct mGUIRunner* runner, struct GUIMenuItem* extra, size_t }, .nStates = 2 }; + *GUIMenuItemListAppend(&menu.items) = (struct GUIMenuItem) { + .title = "Crop SGB borders", + .data = "sgb.borderCrop", + .submenu = 0, + .state = false, + .validStates = (const char*[]) { + "Off", "On" + }, + .nStates = 2 + }; #endif size_t i; const char* mapNames[GUI_MAX_INPUTS + 1]; From 7d821d4f117d974cf0921b5526a5e92270f04f2c Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Fri, 7 Jun 2019 12:26:40 -0700 Subject: [PATCH 50/50] mGUI: Remmeber name and position of last loaded game --- CHANGES | 1 + include/mgba-util/gui/file-select.h | 2 +- src/feature/gui/gui-config.c | 8 ++++---- src/feature/gui/gui-runner.c | 10 +++++++++- src/util/gui/file-select.c | 17 ++++++++++------- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/CHANGES b/CHANGES index c2d7397ec..bb3eb9e80 100644 --- a/CHANGES +++ b/CHANGES @@ -54,6 +54,7 @@ Misc: - Switch: Dynamic display resizing - Qt: Make mute menu option also toggle fast-forward mute (fixes mgba.io/i/1424) - Vita: L2/R2 and L3/R3 can now be mapped on PSTV (fixes mgba.io/i/1292) + - mGUI: Remember name and position of last loaded game 0.7.2: (2019-05-25) Emulation fixes: diff --git a/include/mgba-util/gui/file-select.h b/include/mgba-util/gui/file-select.h index 2b679b05a..fcbb7b789 100644 --- a/include/mgba-util/gui/file-select.h +++ b/include/mgba-util/gui/file-select.h @@ -14,7 +14,7 @@ CXX_GUARD_START struct VFile; -bool GUISelectFile(struct GUIParams*, char* outPath, size_t outLen, bool (*filterName)(const char* name), bool (*filterContents)(struct VFile*)); +bool GUISelectFile(struct GUIParams*, char* outPath, size_t outLen, bool (*filterName)(const char* name), bool (*filterContents)(struct VFile*), const char* preselect); CXX_GUARD_END diff --git a/src/feature/gui/gui-config.c b/src/feature/gui/gui-config.c index 37b239ba3..ee14a019c 100644 --- a/src/feature/gui/gui-config.c +++ b/src/feature/gui/gui-config.c @@ -303,7 +303,7 @@ void mGUIShowConfig(struct mGUIRunner* runner, struct GUIMenuItem* extra, size_t } if (!strcmp(item->data, "gba.bios")) { // TODO: show box if failed - if (!GUISelectFile(&runner->params, gbaBiosPath, sizeof(gbaBiosPath), _biosNamed, GBAIsBIOS)) { + if (!GUISelectFile(&runner->params, gbaBiosPath, sizeof(gbaBiosPath), _biosNamed, GBAIsBIOS, NULL)) { gbaBiosPath[0] = '\0'; } continue; @@ -311,21 +311,21 @@ void mGUIShowConfig(struct mGUIRunner* runner, struct GUIMenuItem* extra, size_t #ifdef M_CORE_GB if (!strcmp(item->data, "gb.bios")) { // TODO: show box if failed - if (!GUISelectFile(&runner->params, gbBiosPath, sizeof(gbBiosPath), _biosNamed, GBIsBIOS)) { + if (!GUISelectFile(&runner->params, gbBiosPath, sizeof(gbBiosPath), _biosNamed, GBIsBIOS, NULL)) { gbBiosPath[0] = '\0'; } continue; } if (!strcmp(item->data, "gbc.bios")) { // TODO: show box if failed - if (!GUISelectFile(&runner->params, gbcBiosPath, sizeof(gbcBiosPath), _biosNamed, GBIsBIOS)) { + if (!GUISelectFile(&runner->params, gbcBiosPath, sizeof(gbcBiosPath), _biosNamed, GBIsBIOS, NULL)) { gbcBiosPath[0] = '\0'; } continue; } if (!strcmp(item->data, "sgb.bios")) { // TODO: show box if failed - if (!GUISelectFile(&runner->params, sgbBiosPath, sizeof(sgbBiosPath), _biosNamed, GBIsBIOS)) { + if (!GUISelectFile(&runner->params, sgbBiosPath, sizeof(sgbBiosPath), _biosNamed, GBIsBIOS, NULL)) { sgbBiosPath[0] = '\0'; } continue; diff --git a/src/feature/gui/gui-runner.c b/src/feature/gui/gui-runner.c index 60a34d79e..038d30a3c 100644 --- a/src/feature/gui/gui-runner.c +++ b/src/feature/gui/gui-runner.c @@ -628,10 +628,18 @@ void mGUIRunloop(struct mGUIRunner* runner) { } while (true) { char path[PATH_MAX]; - if (!GUISelectFile(&runner->params, path, sizeof(path), _testExtensions, NULL)) { + const char* preselect = mCoreConfigGetValue(&runner->config, "lastGame"); + if (preselect) { + preselect = strrchr(preselect, '/'); + } + if (preselect) { + ++preselect; + } + if (!GUISelectFile(&runner->params, path, sizeof(path), _testExtensions, NULL, preselect)) { break; } mCoreConfigSetValue(&runner->config, "lastDirectory", runner->params.currentPath); + mCoreConfigSetValue(&runner->config, "lastGame", path); mCoreConfigSave(&runner->config); mGUIRun(runner, path); } diff --git a/src/util/gui/file-select.c b/src/util/gui/file-select.c index 5043a860c..2e6abaed5 100644 --- a/src/util/gui/file-select.c +++ b/src/util/gui/file-select.c @@ -47,7 +47,7 @@ static int _strpcmp(const void* a, const void* b) { return strcasecmp(((const struct GUIMenuItem*) a)->title, ((const struct GUIMenuItem*) b)->title); } -static bool _refreshDirectory(struct GUIParams* params, const char* currentPath, struct GUIMenuItemList* currentFiles, bool (*filterName)(const char* name), bool (*filterContents)(struct VFile*)) { +static bool _refreshDirectory(struct GUIParams* params, const char* currentPath, struct GUIMenuItemList* currentFiles, bool (*filterName)(const char* name), bool (*filterContents)(struct VFile*), const char* preselect) { _cleanFiles(currentFiles); struct VDir* dir = VDirOpen(currentPath); @@ -144,6 +144,9 @@ static bool _refreshDirectory(struct GUIParams* params, const char* currentPath, free((char*) testItem->title); GUIMenuItemListShift(currentFiles, item, 1); } else { + if (preselect && strncmp(testItem->title, preselect, PATH_MAX) == 0) { + params->fileIndex = item; + } ++item; } } @@ -152,14 +155,14 @@ static bool _refreshDirectory(struct GUIParams* params, const char* currentPath, return true; } -bool GUISelectFile(struct GUIParams* params, char* outPath, size_t outLen, bool (*filterName)(const char* name), bool (*filterContents)(struct VFile*)) { +bool GUISelectFile(struct GUIParams* params, char* outPath, size_t outLen, bool (*filterName)(const char* name), bool (*filterContents)(struct VFile*), const char* preselect) { struct GUIMenu menu = { .title = "Select file", .subtitle = params->currentPath, - .index = params->fileIndex, }; GUIMenuItemListInit(&menu.items, 0); - _refreshDirectory(params, params->currentPath, &menu.items, filterName, filterContents); + _refreshDirectory(params, params->currentPath, &menu.items, filterName, filterContents, preselect); + menu.index = params->fileIndex; while (true) { struct GUIMenuItem* item; @@ -174,7 +177,7 @@ bool GUISelectFile(struct GUIParams* params, char* outPath, size_t outLen, bool continue; } _upDirectory(params->currentPath); - if (!_refreshDirectory(params, params->currentPath, &menu.items, filterName, filterContents)) { + if (!_refreshDirectory(params, params->currentPath, &menu.items, filterName, filterContents, NULL)) { break; } } else { @@ -187,7 +190,7 @@ bool GUISelectFile(struct GUIParams* params, char* outPath, size_t outLen, bool struct GUIMenuItemList newFiles; GUIMenuItemListInit(&newFiles, 0); - if (!_refreshDirectory(params, outPath, &newFiles, filterName, filterContents)) { + if (!_refreshDirectory(params, outPath, &newFiles, filterName, filterContents, NULL)) { _cleanFiles(&newFiles); GUIMenuItemListDeinit(&newFiles); _cleanFiles(&menu.items); @@ -208,7 +211,7 @@ bool GUISelectFile(struct GUIParams* params, char* outPath, size_t outLen, bool break; } _upDirectory(params->currentPath); - if (!_refreshDirectory(params, params->currentPath, &menu.items, filterName, filterContents)) { + if (!_refreshDirectory(params, params->currentPath, &menu.items, filterName, filterContents, NULL)) { break; } params->fileIndex = 0;