Compare commits

...

92 Commits

Author SHA1 Message Date
Lior Halphon 8a234a25a3 Add silence detection to eliminate rounding artifacts when playing nothing but frequencies above Nyquist. Fixes #394 2025-08-18 21:27:01 +03:00
Lior Halphon 5b88346537 Work around an MSVCRT limitation, fixes cheat import on SDL Windows, fixes #716 2025-08-03 16:54:04 +03:00
Lior Halphon 20e5e18122 Update version to v1.0.2 2025-08-01 00:49:49 +03:00
Lior Halphon b948a1f3fd Fix Solarium beta 4 regressions 2025-08-01 00:49:41 +03:00
Lior Halphon 8215c03d62 Update assets.car, screw this format 2025-07-29 22:49:27 +03:00
Lior Halphon 8b2af8adf1 Replace non-car iOS icons with the correct versions 2025-07-27 19:09:35 +03:00
Lior Halphon 00f772b29d Merge branch 'master' of github.com:LIJI32/SameBoy 2025-07-27 13:23:58 +03:00
Lior Halphon 4107198548
Merge pull request #715 from MaddTheSane/patch-2
Update GBViewController.m
2025-07-27 12:26:34 +03:00
C.W. Betts e997ce0ce7
Update GBViewController.m
-[GBViewController didRotateFromInterfaceOrientation:] seems to want [super ...] called to it.
2025-07-26 23:14:45 -06:00
Lior Halphon 58c946f249 Fix accelerometer auto-rotating on iOS 16 and newer 2025-07-27 03:10:17 +03:00
Lior Halphon 0b57886491 Add an icon to hotswap 2025-07-23 23:42:34 +03:00
Lior Halphon 2db60c2b3f Add the missing camera entitlement even on unnotarized releases that don't use the hardened runtime 2025-07-23 23:42:19 +03:00
Lior Halphon f54bfae01f Make headers a target 2025-07-23 00:18:51 +03:00
Lior Halphon c40221abb6 Allow using the DISABLE_* flags when building specific subsets of the lib target 2025-07-22 23:32:00 +03:00
Lior Halphon eb38034b76 Ask for confirmation before reset 2025-07-22 00:10:19 +03:00
Lior Halphon c6a968ed74 Make sure to display an alert if saving a state failed 2025-07-22 00:03:37 +03:00
Lior Halphon e69f6b8579 Bug fix: GB_save_state always returned success 2025-07-21 23:53:54 +03:00
Lior Halphon cfbc7b481a An attempt to mitigate #703, don't reload the ROM or save states if not required; don't assume a new ROM if state failed to load 2025-07-21 23:45:06 +03:00
Lior Halphon 1dfcdffa71 Properly handled Inbox and Boot ROMs in GBROMManager 2025-07-20 22:44:09 +03:00
Lior Halphon 5b983bc7ad Minor optimization 2025-07-20 21:58:36 +03:00
Lior Halphon 634b90e4fc Fix rounding errors in calls to band_limited_update, tweak BL synth parameters. Should greatly improve audio quality, fixes #713 2025-07-18 13:53:02 +03:00
Lior Halphon b31cca77be Use a sinc filter 2025-07-18 13:32:25 +03:00
Lior Halphon 5b17b41e07 Escape translocation on launch so we can update ourselves. 2025-07-12 04:16:31 +03:00
Lior Halphon 239e0462c3 Use proper hooks 2025-07-12 01:25:40 +03:00
Lior Halphon 8505f00cdf Fix BL-synth master wave generation, that wasn't a square step... affects #713 2025-07-12 01:17:47 +03:00
Lior Halphon 08d58aa992 Fix a bug where controller-triggered rewind doesn't resume after reaching the buffer end 2025-07-11 23:14:03 +03:00
Lior Halphon 8cce6f7b13 Another macOS Regressionville™ bugfix 2025-07-11 18:19:19 +03:00
Lior Halphon ec8baa6329 Allow specifying a custom ibtool path 2025-07-11 18:03:36 +03:00
Lior Halphon 9bd84978cf Work around more Solarium bugs 2025-07-11 17:59:32 +03:00
Lior Halphon aa0fe30d5c Don't update the icon after an update on macOS 26, it's broken 2025-07-11 17:59:13 +03:00
Lior Halphon 6d6aafe887 Fix scrolling view in newer macOS versions 2025-07-11 14:46:16 +03:00
Lior Halphon 003e8914b1
Merge pull request #711 from MaddTheSane/patch-2
Update GBViewController.m
2025-07-08 00:06:08 +03:00
Lior Halphon d7e1672ae7 Always define typeof to __typeof__, unless targeting C23. Fixes #710 2025-07-08 00:04:07 +03:00
C.W. Betts 1157ff0f36
Update GBViewController.m
Use CGDataProviderCreateWithCFData instead of CGDataProviderCreateWithData.
2025-07-07 13:52:02 -06:00
Lior Halphon bdd4522ca9 Migrate the Windows build from GNUWin to Git Bash and ezwinports's Make 2025-07-07 22:34:45 +03:00
Lior Halphon 4abb5f3539 Fix AGB mixing – the bias should only be applied if the channel is connected to a terminal 2025-07-05 16:37:48 +03:00
Lior Halphon aff7f1706c Add turbo cap options to the core and frontends, improve frame skipping, replace iOS' turbo speed option with the new turbo cap. Addresses #708. 2025-07-04 14:41:34 +03:00
Lior Halphon 33d237706e
Merge pull request #709 from joshrad-dev/master
Add ability to save to photos on iOS
2025-06-29 23:05:20 +03:00
Lior Halphon a39efd31cf Change the default macOS palette to be the same as the other frontends 2025-06-29 22:42:45 +03:00
Lior Halphon 69bb4c3e95 Fix various iPadOS bugs 2025-06-29 20:17:05 +03:00
Lior Halphon ae2d68aaf3 iPad menu support 2025-06-29 18:48:47 +03:00
Lior Halphon e381ebd1ea Refine the tip animation 2025-06-28 21:29:26 +03:00
Lior Halphon 34c29b052e UI fixes in the cheats screen 2025-06-28 20:55:30 +03:00
Lior Halphon 4ebb108ae1 Fix it harder, remove UIGlassEffect as it's still very buggy 2025-06-28 15:58:39 +03:00
Lior Halphon d1da91da7c Fix tip rotation 2025-06-28 15:46:02 +03:00
Lior Halphon 490d63b26f Fix build break 2025-06-28 15:44:16 +03:00
Lior Halphon be63d7eaa3 Revert this hack, broken on iPhones. Maybe Apple will fix this regression eventually (lulz no) 2025-06-28 15:13:05 +03:00
Lior Halphon 15588a065f Fixed a bug where ROMs were moved instead of copied and vice versa, fixes #701 2025-06-28 15:04:14 +03:00
Lior Halphon d52a50353d Update the tips visuals 2025-06-28 14:40:42 +03:00
Lior Halphon 5d70f93920 Very basic iPad menu support 2025-06-28 13:58:43 +03:00
Lior Halphon f3cbc1990e Allow building on older SDKs 2025-06-28 13:06:38 +03:00
Lior Halphon 583c234953 Various iOS Ui improvements, especially on iOS 26 2025-06-28 00:11:46 +03:00
Lior Halphon 3f744254fd Update iOS settings icons, add liquid glass overlay 2025-06-27 17:02:55 +03:00
Lior Halphon 7abedaed4c Fix a bug where cheat search remains open after closing a ROM 2025-06-26 21:36:26 +03:00
Lior Halphon 42ffbd18d0 Fix a bug where a GBS file will not correctly play the first track unless explicitly switching to it. Reloading a GBS file is disabled because it's not supported. 2025-06-26 20:57:47 +03:00
Lior Halphon 8508eb7b7c Fix Mavericks regressions 2025-06-24 22:44:39 +03:00
Lior Halphon 00000971d7 Describe addresses correctly in unbanked portions of RAM and ROM 2025-06-24 21:53:18 +03:00
Lior Halphon 58bd40b833 Work around gazillion Solarium bugs 2025-06-24 21:29:58 +03:00
Lior Halphon cbaf5c4c4a Avoid non-main-thread calls 2025-06-24 21:29:05 +03:00
Lior Halphon 67d338164b Make text field insets work on macOS 26 2025-06-22 23:34:41 +03:00
Jawshoeadan 7bf8145a91 Add save to photos key in info.plist for GB camera pictures 2025-06-21 13:22:31 -07:00
Lior Halphon e043279500 Update the Cocoa UI to support Solarium (Memory viewer still not updated due to AppKit regression, let's see what the next beta has to say) 2025-06-21 14:50:12 +03:00
Lior Halphon 9d6f378d21 Add Icon Composer based icon for macOS 26 2025-06-13 22:54:10 +03:00
Lior Halphon bfb1092cbb Fix cheat search crashing on carts without RAM 2025-06-10 01:19:29 +03:00
Lior Halphon 282140822e Fix compatibility issues with macOS 26 NIB loading 2025-06-10 00:22:14 +03:00
Lior Halphon 19a1e3ec1a Add a vsync as an option to SDL, fixes #335 2025-06-08 20:19:09 +03:00
Lior Halphon 1ad8bad18c Add iOS rapid buttons, closes #702 2025-06-08 19:28:42 +03:00
Lior Halphon 9577cbce85 Added the option to force integer scaling to the Cocoa port (SDL had it for ages), closes #699 2025-06-08 11:51:34 +03:00
Lior Halphon 6dd2f609f2 Make the debugger console larger by default, fix misaligned line 2025-06-08 01:43:17 +03:00
Lior Halphon 976fe7a337 The sideview shouldn't automatically scroll 2025-06-08 01:30:17 +03:00
Lior Halphon 1400bd40e8 Slightly alter the behavior of the print command so values don't get zero-padding by default, fixes #687 2025-06-08 01:25:30 +03:00
Lior Halphon 6a97192e8c Slightly refine the last fix 2025-06-07 18:58:31 +03:00
Lior Halphon 42732b20eb Restore Alt+Zoom behavior 2025-06-07 18:49:45 +03:00
Lior Halphon f0a672c39e Make sure the CPU graph advanced correctly even in turbo mode 2025-06-07 14:24:35 +03:00
Lior Halphon d211120312 Make 100% CPU frames appear red 2025-06-07 14:11:10 +03:00
Lior Halphon 6ab1be654b Add CPU load graph to Cocoa, closes #654 2025-06-06 23:10:57 +03:00
Lior Halphon f706988171 Add CPU usage command (#654) 2025-06-06 19:26:25 +03:00
Lior Halphon bed9f8220c
Merge pull request #706 from Estus-Dev/Specify-size-of-block-header's-length-field
Specify size of block header's length field
2025-06-04 20:33:23 +03:00
Estus 750112f6dd
docs: specify size of block header's length field 2025-06-04 11:21:48 -06:00
Lior Halphon 63a02d90bc
Merge pull request #568 from hitomi-nakayama/warn-sdl-init-error
Warn user about SDL initialization failure
2025-06-02 23:40:54 +03:00
Lior Halphon 795fba1320
Merge branch 'master' into warn-sdl-init-error 2025-06-02 23:40:40 +03:00
Lior Halphon 1923c324d9 Slightly alter iOS behavior 2025-06-02 21:21:39 +03:00
Lior Halphon 6a24b9206f Remove the navigation string since it doesn't fit 2025-06-02 21:21:23 +03:00
Marcus Ziadé 30a8c4bf42 Add Vim menu navigation 2025-05-26 21:46:31 +03:00
Lior Halphon 152e242485 Fixed incorrect processing of GameShark codes 2025-05-26 00:51:16 +03:00
Lior Halphon c4e6161959
Merge pull request #681 from Jan200101/PR/mime-location
correct mimetype location to follow the shared mime info specification
2025-05-25 12:33:20 +03:00
Jan200101 1951df3476
correct mimetype location to follow the shared mime info specification 2025-04-27 16:16:13 +02:00
Lior Halphon 1cf84a5436 Ubuntu 20.04 is dead soon, replace with 22.04 2025-04-08 20:43:21 +03:00
Lior Halphon 81c29fa371 Make gb.h compatible with ANSI C++, fixes #698 2025-04-08 20:39:12 +03:00
Lior Halphon 8b27952680
Merge pull request #697 from bentley/libdl
Don’t look for libdl on OpenBSD
2025-04-05 13:26:43 +03:00
Anthony J. Bentley 3c58deb46f Don’t look for libdl on OpenBSD 2025-04-05 03:18:25 -06:00
Hitomi Nakayama d267d83cec Added warning for SDL init failure 2023-10-12 17:57:33 -07:00
130 changed files with 2585 additions and 769 deletions

View File

@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, ubuntu-20.04]
os: [macos-latest, ubuntu-latest, ubuntu-22.04]
cc: [gcc, clang]
include:
- os: macos-latest

View File

@ -22,9 +22,9 @@ BESS works by appending a detectable footer at the end of an existing save state
BESS uses a block format where each block contains the following header:
| Offset | Content |
|--------|---------------------------------------|
| 0 | A four-letter ASCII identifier |
| 4 | Length of the block, excluding header |
|--------|-----------------------------------------------------------|
| 0 | A four-letter ASCII identifier |
| 4 | Length of the block as a 32-bit integer, excluding header |
Every block is followed by another block, until the END block is reached. If an implementation encounters an unsupported block, it should be completely ignored (Should not have any effect and should not trigger a failure).
@ -256,4 +256,4 @@ Other than previously specified required fail conditions, an implementation is f
* An invalid length of MBC (not a multiple of 3)
* A write outside the $0000-$7FFF and $A000-$BFFF ranges in the MBC block
* An SGB block on a save state targeting another model
* An END block with non-zero length
* An END block with non-zero length

BIN
Cocoa/Assets.car Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 B

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 420 B

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -103,7 +103,7 @@
</tableHeaderView>
</scrollView>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KdM-lW-WbP">
<rect key="frame" x="371" y="25" width="96" height="32"/>
<rect key="frame" x="371" y="24" width="96" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="push" title="Search" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="t4Y-Ud-mJm">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -113,30 +113,30 @@
<action selector="search:" target="-2" id="7pG-JY-vEF"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fTv-nr-5FT">
<rect key="frame" x="6" y="96" width="124" height="16"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fTv-nr-5FT">
<rect key="frame" x="6" y="95" width="124" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Search Condition:" id="9C2-Xp-JIA">
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" alignment="right" title="Search Condition:" id="9C2-Xp-JIA">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vMd-zb-8jT">
<rect key="frame" x="6" y="67" width="124" height="16"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vMd-zb-8jT">
<rect key="frame" x="6" y="64" width="124" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Expression:" id="Jgg-sA-jjs">
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" alignment="right" title="Expression:" id="Jgg-sA-jjs">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5Db-vP-S60">
<rect key="frame" x="133" y="119" width="197" height="25"/>
<rect key="frame" x="133" y="120" width="197" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<popUpButtonCell key="cell" type="push" title="8-Bit" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="al4-Jb-OJB" id="dkg-V5-wsX">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<font key="font" metaFont="message"/>
<menu key="menu" id="e5r-qR-pg7">
<items>
<menuItem title="8-Bit" state="on" id="al4-Jb-OJB"/>
@ -151,7 +151,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<popUpButtonCell key="cell" type="push" title="Is Equal To…" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Vfb-Dg-Jkb" id="DPm-QO-c64">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<font key="font" metaFont="message"/>
<menu key="menu" id="Kg9-Rd-1GQ">
<items>
<menuItem title="Any" id="cEg-eI-4hb"/>
@ -173,7 +173,7 @@
<action selector="conditionChanged:" target="-2" id="KF9-vz-yNC"/>
</connections>
</popUpButton>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Q42-Vu-TJW">
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Q42-Vu-TJW">
<rect key="frame" x="334" y="93" width="126" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" continuous="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="$0" drawsBackground="YES" usesSingleLineMode="YES" id="WDs-GG-7Lc">
@ -189,8 +189,8 @@
<outlet property="delegate" destination="-2" id="1bO-hp-igc"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XN7-BO-THS">
<rect key="frame" x="136" y="64" width="324" height="21"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XN7-BO-THS">
<rect key="frame" x="136" y="62" width="324" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" enabled="NO" sendsActionOnEndEditing="YES" borderStyle="bezel" title="new == ($0)" drawsBackground="YES" usesSingleLineMode="YES" id="Krh-8w-4ug">
<font key="font" metaFont="system"/>
@ -205,7 +205,7 @@
</connections>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tBa-6c-9AY">
<rect key="frame" x="13" y="25" width="96" height="32"/>
<rect key="frame" x="13" y="24" width="96" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="push" title="Reset" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Pge-SU-Y1n">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -215,7 +215,7 @@
<action selector="reset:" target="-2" id="KCy-Ob-tlg"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wbN-MX-lEy">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wbN-MX-lEy">
<rect key="frame" x="-3" y="4" width="486" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="clipping" alignment="center" title="Status" id="CM3-4U-qao">
@ -225,7 +225,7 @@
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="o1I-5D-V4k">
<rect key="frame" x="264" y="25" width="110" height="32"/>
<rect key="frame" x="264" y="24" width="110" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="push" title="Add Cheat" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="GGV-nm-ASn">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -235,10 +235,10 @@
<action selector="addCheat:" target="-2" id="7ax-kM-TeV"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="veO-Qn-0Sz">
<rect key="frame" x="6" y="126" width="124" height="16"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="veO-Qn-0Sz">
<rect key="frame" x="6" y="126" width="124" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Data Type:" id="KuT-rz-eHm">
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Data Type:" id="KuT-rz-eHm">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 B

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 448 B

View File

@ -3,6 +3,7 @@
#import "GBImageView.h"
#import "GBSplitView.h"
#import "GBVisualizerView.h"
#import "GBCPUView.h"
#import "GBOSDView.h"
#import "GBDebuggerButton.h"
@ -84,6 +85,8 @@ enum model {
@property IBOutlet NSScrollView *debuggerScrollView;
@property IBOutlet NSView *debugBar;
@property IBOutlet GBCPUView *cpuView;
@property IBOutlet NSTextField *cpuCounter;
+ (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale;
- (void) performAtomicBlock: (void (^)())block;

View File

@ -288,6 +288,7 @@ static void debuggerReloadCallback(GB_gameboy_t *gb)
GB_set_user_data(&_gb, (__bridge void *)(self));
GB_set_boot_rom_load_callback(&_gb, (GB_boot_rom_load_callback_t)boot_rom_load);
GB_set_vblank_callback(&_gb, (GB_vblank_callback_t) vblank);
GB_set_enable_skipped_frame_vblank_callbacks(&_gb, true);
GB_set_log_callback(&_gb, (GB_log_callback_t) consoleLog);
GB_set_input_callback(&_gb, (GB_input_callback_t) consoleInput);
GB_set_async_input_callback(&_gb, (GB_input_callback_t) asyncConsoleInput);
@ -345,6 +346,12 @@ static void debuggerReloadCallback(GB_gameboy_t *gb)
[self observeStandardDefaultsKey:@"GBDebuggerFontSize" withBlock:^(NSString *value) {
[weakSelf updateFonts];
}];
[self observeStandardDefaultsKey:@"GBTurboCap" withBlock:^(NSNumber *value) {
if (!_master) {
GB_set_turbo_cap(gb, value.doubleValue);
}
}];
}
- (void)updateMinSize
@ -359,11 +366,29 @@ static void debuggerReloadCallback(GB_gameboy_t *gb)
- (void)vblankWithType:(GB_vblank_type_t)type
{
if (type == GB_VBLANK_TYPE_SKIPPED_FRAME) {
double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb);
[_cpuView addSample:frameUsage];
return;
}
if (_gbsVisualizer) {
dispatch_async(dispatch_get_main_queue(), ^{
[_gbsVisualizer setNeedsDisplay:true];
});
}
double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb);
[_cpuView addSample:frameUsage];
if (self.consoleWindow.visible) {
double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
dispatch_async(dispatch_get_main_queue(), ^{
[_cpuView setNeedsDisplay:true];
_cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
});
}
if (type != GB_VBLANK_TYPE_REPEAT) {
[self.view flip];
if (_borderModeChanged) {
@ -730,11 +755,12 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency)
if (old_width != GB_get_screen_width(&_gb)) {
[self.view screenSizeChanged];
}
[self updateMinSize];
[self start];
if (_gbsTracks) {
[self changeGBSTrack:sender];
}
if (_hexController) {
/* Verify bank sanity, especially when switching models. */
@ -859,7 +885,7 @@ again:;
{
[super windowControllerDidLoadNib:aController];
// Interface Builder bug?
[self.consoleWindow setContentSize:self.consoleWindow.minSize];
[self.consoleWindow setContentSize:self.consoleWindow.frame.size];
/* Close Open Panels, if any */
for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
if ([window isKindOfClass:[NSOpenPanel class]]) {
@ -897,9 +923,16 @@ again:;
[self.vramWindow setFrame:vram_window_rect display:true animate:false];
self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console %@", [self.fileURL.path lastPathComponent]];
self.memoryWindow.title = [NSString stringWithFormat:@"Memory %@", [self.fileURL.path lastPathComponent]];
self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer %@", [self.fileURL.path lastPathComponent]];
if (@available(macOS 11.0, *)) {
self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent];
self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent];
self.vramWindow.subtitle = [self.fileURL.path lastPathComponent];
}
else {
self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console %@", [self.fileURL.path lastPathComponent]];
self.memoryWindow.title = [NSString stringWithFormat:@"Memory %@", [self.fileURL.path lastPathComponent]];
self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer %@", [self.fileURL.path lastPathComponent]];
}
self.consoleWindow.level = NSNormalWindowLevel;
@ -1152,6 +1185,17 @@ again:;
if (@available(macOS 10.10, *)) {
_mainWindow.titlebarAppearsTransparent = true;
}
if (@available(macOS 26.0, *)) {
// There's a new minimum width for segmented controls in Solarium
NSRect frame = _gbsNextPrevButton.frame;
frame.origin.x -= 16;
_gbsNextPrevButton.frame = frame;
frame = _gbsTracks.frame;
frame.size.width -= 16;
_gbsTracks.frame = frame;
}
}
- (bool)isCartContainer
@ -1316,6 +1360,7 @@ static bool is_path_writeable(const char *path)
[self.vramWindow close];
[self.printerFeedWindow close];
[self.cheatsWindow close];
[_cheatSearchController.window close];
[super close];
}
@ -1325,6 +1370,8 @@ static bool is_path_writeable(const char *path)
GB_debugger_break(&_gb);
[self start];
[self.consoleWindow makeKeyAndOrderFront:nil];
double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
_cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
[self.consoleInput becomeFirstResponder];
}
@ -1406,6 +1453,9 @@ static bool is_path_writeable(const char *path)
else if ([anItem action] == @selector(decreaseWindowSize:)) {
return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionDecrease];
}
else if ([anItem action] == @selector(reloadROM:)) {
return !_gbsTracks;
}
return [super validateUserInterfaceItem:anItem];
}
@ -1547,7 +1597,9 @@ enum GBWindowResizeAction
[self reloadVRAMData: nil];
[textView.textStorage appendAttributedString:_pendingConsoleOutput];
[textView scrollToEndOfDocument:nil];
if (!_logToSideView) {
[textView scrollToEndOfDocument:nil];
}
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
[self.consoleWindow orderFront:nil];
}
@ -1605,7 +1657,9 @@ enum GBWindowResizeAction
- (IBAction)showConsoleWindow:(id)sender
{
[self.consoleWindow orderBack:nil];
[self.consoleWindow orderFront:nil];
double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
_cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
}
- (void)queueDebuggerCommand:(NSString *)command
@ -1955,124 +2009,130 @@ enum GBWindowResizeAction
- (IBAction)hexGoTo:(id)sender
{
NSString *expression = [sender stringValue];
__block uint16_t addr = 0;
__block uint16_t bank = 0;
__block bool fail = false;
NSString *error = [self captureOutputForBlock:^{
uint16_t addr;
uint16_t bank;
if (GB_debugger_evaluate(&_gb, [[sender stringValue] UTF8String], &addr, &bank)) {
return;
if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) {
fail = true;
}
if (bank != (typeof(bank))-1) {
GB_memory_mode_t mode = [(GBMemoryByteArray *)(_hexController.byteArray) mode];
if (addr < 0x4000) {
if (bank == 0) {
if (mode != GBMemoryROM && mode != GBMemoryEntireSpace) {
mode = GBMemoryEntireSpace;
}
}
else {
addr |= 0x4000;
mode = GBMemoryROM;
}
}
else if (addr < 0x8000) {
mode = GBMemoryROM;
}
else if (addr < 0xA000) {
mode = GBMemoryVRAM;
}
else if (addr < 0xC000) {
mode = GBMemoryExternalRAM;
}
else if (addr < 0xD000) {
if (mode != GBMemoryRAM && mode != GBMemoryEntireSpace) {
mode = GBMemoryEntireSpace;
}
}
else if (addr < 0xE000) {
mode = GBMemoryRAM;
}
else {
mode = GBMemoryEntireSpace;
}
[_memorySpaceButton selectItemAtIndex:mode];
[self hexUpdateSpace:_memorySpaceButton.cell];
[_memoryBankInput setStringValue:[NSString stringWithFormat:@"$%02x", bank]];
[self hexUpdateBank:_memoryBankInput];
}
addr -= _lineRep.valueOffset;
if (addr >= _hexController.byteArray.length) {
GB_log(&_gb, "Value $%04x is out of range.\n", addr);
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]];
[_hexController _ensureVisibilityOfLocation:addr];
for (HFRepresenter *representer in _hexController.representers) {
if ([representer isKindOfClass:[HFHexTextRepresenter class]]) {
[self.memoryWindow makeFirstResponder:representer.view];
break;
}
}
});
}];
if (error) {
NSBeep();
[GBWarningPopover popoverWithContents:error onView:sender];
}
if (fail) return;
if (bank != (typeof(bank))-1) {
GB_memory_mode_t mode = [(GBMemoryByteArray *)(_hexController.byteArray) mode];
if (addr < 0x4000) {
if (bank == 0) {
if (mode != GBMemoryROM && mode != GBMemoryEntireSpace) {
mode = GBMemoryEntireSpace;
}
}
else {
addr |= 0x4000;
mode = GBMemoryROM;
}
}
else if (addr < 0x8000) {
mode = GBMemoryROM;
}
else if (addr < 0xA000) {
mode = GBMemoryVRAM;
}
else if (addr < 0xC000) {
mode = GBMemoryExternalRAM;
}
else if (addr < 0xD000) {
if (mode != GBMemoryRAM && mode != GBMemoryEntireSpace) {
mode = GBMemoryEntireSpace;
}
}
else if (addr < 0xE000) {
mode = GBMemoryRAM;
}
else {
mode = GBMemoryEntireSpace;
}
[_memorySpaceButton selectItemAtIndex:mode];
[self hexUpdateSpace:_memorySpaceButton.cell];
[_memoryBankInput setStringValue:[NSString stringWithFormat:@"$%02x", bank]];
[self hexUpdateBank:_memoryBankInput];
}
addr -= _lineRep.valueOffset;
if (addr >= _hexController.byteArray.length) {
GB_log(&_gb, "Value $%04x is out of range.\n", addr);
return;
}
[_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]];
[_hexController _ensureVisibilityOfLocation:addr];
for (HFRepresenter *representer in _hexController.representers) {
if ([representer isKindOfClass:[HFHexTextRepresenter class]]) {
[self.memoryWindow makeFirstResponder:representer.view];
break;
}
}
}
- (void)hexUpdateBank:(NSControl *)sender ignoreErrors: (bool)ignore_errors
{
NSString *expression = [sender stringValue];
__block uint16_t addr, bank;
__block bool fail = false;
NSString *error = [self captureOutputForBlock:^{
uint16_t addr, bank;
if (GB_debugger_evaluate(&_gb, [[sender stringValue] UTF8String], &addr, &bank)) {
if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) {
fail = true;
return;
}
if (bank == (uint16_t) -1) {
bank = addr;
}
uint16_t n_banks = 1;
switch ([(GBMemoryByteArray *)(_hexController.byteArray) mode]) {
case GBMemoryROM: {
size_t rom_size;
GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL);
n_banks = rom_size / 0x4000;
break;
}
case GBMemoryVRAM:
n_banks = GB_is_cgb(&_gb) ? 2 : 1;
break;
case GBMemoryExternalRAM: {
size_t ram_size;
GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL);
n_banks = (ram_size + 0x1FFF) / 0x2000;
break;
}
case GBMemoryRAM:
n_banks = GB_is_cgb(&_gb) ? 8 : 1;
break;
case GBMemoryEntireSpace:
break;
}
bank %= n_banks;
[sender setStringValue:[NSString stringWithFormat:@"$%x", bank]];
[(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:bank];
_statusRep.bankForDescription = bank;
dispatch_async(dispatch_get_main_queue(), ^{
[_hexController reloadData];
});
}];
if (error && !ignore_errors) {
NSBeep();
[GBWarningPopover popoverWithContents:error onView:sender];
}
if (fail) return;
if (bank == (uint16_t) -1) {
bank = addr;
}
uint16_t n_banks = 1;
switch ([(GBMemoryByteArray *)(_hexController.byteArray) mode]) {
case GBMemoryROM: {
size_t rom_size;
GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL);
n_banks = rom_size / 0x4000;
break;
}
case GBMemoryVRAM:
n_banks = GB_is_cgb(&_gb) ? 2 : 1;
break;
case GBMemoryExternalRAM: {
size_t ram_size;
GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL);
n_banks = (ram_size + 0x1FFF) / 0x2000;
break;
}
case GBMemoryRAM:
n_banks = GB_is_cgb(&_gb) ? 8 : 1;
break;
case GBMemoryEntireSpace:
break;
}
bank %= n_banks;
[(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:bank];
_statusRep.bankForDescription = bank;
[sender setStringValue:[NSString stringWithFormat:@"$%x", bank]];
[_hexController reloadData];
}
- (IBAction)hexUpdateBank:(NSControl *)sender
@ -2110,9 +2170,8 @@ enum GBWindowResizeAction
}
byteArray.selectedBank = bank;
_statusRep.bankForDescription = bank;
if (bank != (uint16_t)-1) {
[self.memoryBankInput setStringValue:[NSString stringWithFormat:@"$%x", byteArray.selectedBank]];
}
[self.memoryBankInput setStringValue:(bank == (uint16_t)-1)? @"" :
[NSString stringWithFormat:@"$%x", byteArray.selectedBank]];
[_hexController reloadData];
for (NSView *view in self.memoryView.subviews) {
@ -2452,9 +2511,16 @@ enum GBWindowResizeAction
- (void)setFileURL:(NSURL *)fileURL
{
[super setFileURL:fileURL];
self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console %@", [[fileURL path] lastPathComponent]];
self.memoryWindow.title = [NSString stringWithFormat:@"Memory %@", [[fileURL path] lastPathComponent]];
self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer %@", [[fileURL path] lastPathComponent]];
if (@available(macOS 11.0, *)) {
self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent];
self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent];
self.vramWindow.subtitle = [self.fileURL.path lastPathComponent];
}
else {
self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console %@", [self.fileURL.path lastPathComponent]];
self.memoryWindow.title = [NSString stringWithFormat:@"Memory %@", [self.fileURL.path lastPathComponent]];
self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer %@", [self.fileURL.path lastPathComponent]];
}
}
- (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview;
@ -2526,6 +2592,8 @@ enum GBWindowResizeAction
}
GB_set_turbo_mode(&_gb, false, false);
GB_set_turbo_mode(&partner->_gb, false, false);
GB_set_turbo_cap(&_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]);
GB_set_turbo_cap(&partner->_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]);
}
}
@ -2541,6 +2609,7 @@ enum GBWindowResizeAction
GB_set_turbo_mode(&partner->_gb, true, true);
_slave = partner;
partner->_master = self;
GB_set_turbo_cap(&partner->_gb, 0);
_linkOffset = 0;
GB_set_serial_transfer_bit_start_callback(&_gb, _linkCableBitStart);
GB_set_serial_transfer_bit_start_callback(&partner->_gb, _linkCableBitStart);

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="System colors introduced in macOS 10.14" minToolsVersion="10.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -15,6 +15,8 @@
<outlet property="consoleInput" destination="l22-S8-uji" id="Heu-am-YgB"/>
<outlet property="consoleOutput" destination="doS-dM-hnl" id="Gn5-ju-Wb0"/>
<outlet property="consoleWindow" destination="21F-Ah-yHX" id="eQ4-ug-LsT"/>
<outlet property="cpuCounter" destination="xdx-GC-Tb8" id="oSn-U3-qvg"/>
<outlet property="cpuView" destination="aBf-yU-8M0" id="RIb-Fj-lvt"/>
<outlet property="debugBar" destination="sah-kv-6KJ" id="24d-aM-rs5"/>
<outlet property="debuggerBackstepButton" destination="E87-Uq-f2l" id="PI7-Wu-f0v"/>
<outlet property="debuggerContinueButton" destination="ybQ-jy-NgI" id="fRF-S3-xSh"/>
@ -54,7 +56,7 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" tabbingMode="disallowed" id="xOd-HO-29H" userLabel="Window">
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" tabbingMode="disallowed" id="xOd-HO-29H" userLabel="Window" customClass="GBWindow">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="160" height="144"/>
@ -91,38 +93,38 @@
<window title="Debug Console" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="21F-Ah-yHX" customClass="GBPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES" utility="YES" HUD="YES"/>
<windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="921" height="400"/>
<rect key="contentRect" x="0.0" y="0.0" width="921" height="480"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<value key="minSize" type="size" width="921" height="400"/>
<view key="contentView" id="dCP-E5-7Fi" customClass="GBOptionalVisualEffectView">
<rect key="frame" x="0.0" y="0.0" width="921" height="400"/>
<rect key="frame" x="0.0" y="0.0" width="921" height="480"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<box horizontalHuggingPriority="750" boxType="separator" id="7bR-gM-1At">
<rect key="frame" x="590" y="0.0" width="5" height="401"/>
<rect key="frame" x="590" y="0.0" width="5" height="481"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" heightSizable="YES"/>
</box>
<splitView dividerStyle="thin" vertical="YES" id="pUc-ZN-vl5" customClass="GBSplitView">
<rect key="frame" x="0.0" y="0.0" width="921" height="401"/>
<rect key="frame" x="0.0" y="0.0" width="921" height="481"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<customView fixedFrame="YES" id="2rj-7i-kxc">
<rect key="frame" x="0.0" y="0.0" width="591" height="401"/>
<rect key="frame" x="0.0" y="0.0" width="591" height="481"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView fixedFrame="YES" borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oTo-zx-o6N">
<rect key="frame" x="0.0" y="52" width="591" height="349"/>
<rect key="frame" x="0.0" y="52" width="591" height="429"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="EQe-Ad-L7S">
<rect key="frame" x="0.0" y="0.0" width="591" height="349"/>
<rect key="frame" x="0.0" y="0.0" width="591" height="429"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" baseWritingDirection="leftToRight" findStyle="bar" allowsNonContiguousLayout="YES" id="doS-dM-hnl">
<rect key="frame" x="0.0" y="0.0" width="591" height="349"/>
<rect key="frame" x="0.0" y="0.0" width="591" height="429"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.25" colorSpace="custom" customColorSpace="sRGB"/>
<size key="minSize" width="591" height="349"/>
<size key="minSize" width="591" height="429"/>
<size key="maxSize" width="1160" height="10000000"/>
<color key="insertionPointColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<allowedInputSourceLocales>
@ -136,7 +138,7 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="cwi-6E-rbh">
<rect key="frame" x="575" y="0.0" width="16" height="349"/>
<rect key="frame" x="575" y="0.0" width="16" height="429"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
@ -160,7 +162,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ybQ-jy-NgI" customClass="GBDebuggerButton">
<rect key="frame" x="0.0" y="0.0" width="26" height="26"/>
<rect key="frame" x="4" y="0.0" width="26" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="bevel" title="Interrupt" alternateTitle="interrupt" bezelStyle="rounded" image="InterruptTemplate" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="Yd7-kY-21r">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -173,7 +175,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jdD-yP-Nr6" customClass="GBDebuggerButton">
<rect key="frame" x="76" y="0.0" width="26" height="26"/>
<rect key="frame" x="80" y="0.0" width="26" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="bevel" title="Step Out" alternateTitle="finish" bezelStyle="rounded" image="FinishTemplate" imagePosition="only" alignment="center" enabled="NO" imageScaling="proportionallyDown" inset="2" id="16t-ix-lOh">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -185,7 +187,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tTP-Zs-Ohu" customClass="GBDebuggerButton">
<rect key="frame" x="26" y="0.0" width="26" height="26"/>
<rect key="frame" x="30" y="0.0" width="26" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="bevel" title="Step Over" alternateTitle="next" bezelStyle="rounded" image="NextTemplate" imagePosition="only" alignment="center" enabled="NO" imageScaling="proportionallyDown" inset="2" id="835-qy-CNq">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -197,7 +199,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="E87-Uq-f2l" customClass="GBDebuggerButton">
<rect key="frame" x="100" y="0.0" width="26" height="26"/>
<rect key="frame" x="104" y="0.0" width="26" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="bevel" title="Step Backward" alternateTitle="backstep" bezelStyle="rounded" image="BackstepTemplate" imagePosition="only" alignment="center" enabled="NO" imageScaling="proportionallyDown" inset="2" id="yr5-aU-Fli">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -209,7 +211,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fsQ-dD-A8C" customClass="GBDebuggerButton">
<rect key="frame" x="52" y="0.0" width="26" height="26"/>
<rect key="frame" x="56" y="0.0" width="26" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="bevel" title="Step Into" alternateTitle="step" bezelStyle="rounded" image="StepTemplate" imagePosition="only" alignment="center" enabled="NO" imageScaling="proportionallyDown" inset="2" id="lau-41-TYH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -229,7 +231,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</box>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Jt-TO-8CM" customClass="GBDebuggerButton">
<rect key="frame" x="565" y="0.0" width="26" height="26"/>
<rect key="frame" x="561" y="0.0" width="26" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="bevel" title="Help" alternateTitle="help" bezelStyle="rounded" image="HelpTemplate" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="fVh-bT-eYs">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -245,11 +247,11 @@
</subviews>
</customView>
<customView fixedFrame="YES" id="4Z2-33-dYY" customClass="GBOptionalVisualEffectView">
<rect key="frame" x="592" y="0.0" width="329" height="401"/>
<rect key="frame" x="592" y="0.0" width="329" height="481"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView fixedFrame="YES" borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" scrollerKnobStyle="dark" translatesAutoresizingMaskIntoConstraints="NO" id="V9U-Ua-F4z">
<rect key="frame" x="0.0" y="338" width="329" height="63"/>
<rect key="frame" x="0.0" y="418" width="329" height="63"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="YHx-TM-zIC">
<rect key="frame" x="0.0" y="0.0" width="329" height="63"/>
@ -279,22 +281,22 @@
</scroller>
</scrollView>
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="5qI-qZ-nkh">
<rect key="frame" x="0.0" y="336" width="329" height="5"/>
<rect key="frame" x="0.0" y="415" width="329" height="5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
</box>
<scrollView fixedFrame="YES" borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vts-CC-ZjQ">
<rect key="frame" x="0.0" y="0.0" width="329" height="338"/>
<rect key="frame" x="0.0" y="78" width="329" height="339"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="Cs9-3x-ATg">
<rect key="frame" x="0.0" y="0.0" width="329" height="338"/>
<rect key="frame" x="0.0" y="0.0" width="329" height="339"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView editable="NO" drawsBackground="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" baseWritingDirection="leftToRight" findStyle="bar" allowsNonContiguousLayout="YES" spellingCorrection="YES" id="JgV-7E-iwp">
<rect key="frame" x="0.0" y="0.0" width="329" height="338"/>
<rect key="frame" x="0.0" y="0.0" width="329" height="339"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" red="0.14901960780000001" green="0.14901960780000001" blue="0.14901960780000001" alpha="1" colorSpace="calibratedRGB"/>
<size key="minSize" width="329" height="338"/>
<size key="minSize" width="329" height="339"/>
<size key="maxSize" width="1160" height="10000000"/>
<color key="insertionPointColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<allowedInputSourceLocales>
@ -309,10 +311,38 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="4jm-Gm-D2R">
<rect key="frame" x="313" y="0.0" width="16" height="338"/>
<rect key="frame" x="313" y="0.0" width="16" height="339"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="wC1-WG-WRf">
<rect key="frame" x="0.0" y="75" width="329" height="5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</box>
<customView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="aBf-yU-8M0" customClass="GBCPUView">
<rect key="frame" x="0.0" y="0.0" width="329" height="77"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="F9b-AT-CdX">
<rect key="frame" x="2" y="59" width="100" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="CPU Load" id="ai6-8E-Let">
<font key="font" metaFont="smallSystemBold"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="xdx-GC-Tb8">
<rect key="frame" x="2" y="2" width="100" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" title="100%" id="set-AM-fVX">
<font key="font" metaFont="smallSystemBold"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</customView>
</subviews>
</customView>
</subviews>
@ -340,20 +370,20 @@
<rect key="frame" x="0.0" y="0.0" width="528" height="320"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<toolbar key="toolbar" implicitIdentifier="D857E961-E523-4295-83F8-0849316E827C" autosavesConfiguration="NO" allowsUserCustomization="NO" displayMode="iconAndLabel" sizeMode="regular" id="82v-uB-RPi">
<toolbar key="toolbar" implicitIdentifier="D857E961-E523-4295-83F8-0849316E827C" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="82v-uB-RPi">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="WUk-8p-S6B"/>
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="E3z-um-6KG"/>
<toolbarItem implicitItemIdentifier="4F6AAE25-1E9D-4111-9E5B-91F0792E56CD" label="Address Space" paletteLabel="Address Space" id="VTy-lj-K0H">
<nil key="toolTip"/>
<size key="minSize" width="100" height="25"/>
<size key="maxSize" width="130" height="25"/>
<size key="minSize" width="160" height="25"/>
<size key="maxSize" width="160" height="25"/>
<popUpButton key="view" verticalHuggingPriority="750" id="vfJ-vu-gqJ">
<rect key="frame" x="0.0" y="14" width="128" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bpD-j9-omo">
<rect key="frame" x="0.0" y="14" width="160" height="25"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<popUpButtonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bpD-j9-omo" customClass="GBToolbarPopUpButtonCell">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<font key="font" metaFont="message"/>
<menu key="menu" id="gTX-6Z-mOH">
<items>
<menuItem title="Entire Space" id="Zp5-J9-dd3"/>
@ -372,31 +402,29 @@
<toolbarItem implicitItemIdentifier="D16C64D2-2F0D-4033-A1EC-A1E699522ECE" label="Bank" paletteLabel="Bank" id="bWC-FW-IYP">
<nil key="toolTip"/>
<size key="minSize" width="64" height="22"/>
<size key="maxSize" width="64" height="22"/>
<textField key="view" verticalHuggingPriority="750" id="rdV-q6-hc6">
<size key="maxSize" width="80" height="22"/>
<textField key="view" focusRingType="none" verticalHuggingPriority="750" id="rdV-q6-hc6">
<rect key="frame" x="0.0" y="14" width="64" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" id="JCn-Y1-eHS">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" state="on" borderStyle="bezel" placeholderString="Bank" id="JCn-Y1-eHS" customClass="GBToolbarFieldCell">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<action selector="hexUpdateBank:" target="-2" id="Mx9-WI-wgO"/>
</connections>
</textField>
</toolbarItem>
<toolbarItem implicitItemIdentifier="F9723DA8-D79F-43AB-876B-783DD0204AA6" label="Go to" paletteLabel="Go to" id="rLO-D7-zRG">
<toolbarItem implicitItemIdentifier="F9723DA8-D79F-43AB-876B-783DD0204AA6" label="Go To" paletteLabel="Go To" id="rLO-D7-zRG">
<nil key="toolTip"/>
<size key="minSize" width="96" height="22"/>
<size key="maxSize" width="128" height="22"/>
<textField key="view" verticalHuggingPriority="750" id="EJd-jG-hmH">
<rect key="frame" x="0.0" y="14" width="96" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" state="on" borderStyle="bezel" bezelStyle="round" id="vg5-Nn-abb">
<size key="minSize" width="160" height="22"/>
<size key="maxSize" width="160" height="22"/>
<textField key="view" focusRingType="none" verticalHuggingPriority="750" id="EJd-jG-hmH">
<rect key="frame" x="0.0" y="14" width="160" height="22"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" state="on" borderStyle="bezel" placeholderString="Address" bezelStyle="round" id="vg5-Nn-abb" customClass="GBToolbarFieldCell">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<action selector="hexGoTo:" target="-2" id="7WG-8C-SK8"/>
@ -422,7 +450,7 @@
<windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="512" height="432"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="GYW-dv-Um1">
<view key="contentView" misplaced="YES" id="GYW-dv-Um1">
<rect key="frame" x="0.0" y="0.0" width="512" height="432"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
@ -430,7 +458,7 @@
<rect key="frame" x="0.0" y="406" width="512" height="5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6vK-IP-PmP">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6vK-IP-PmP">
<rect key="frame" x="-2" y="4" width="516" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" id="umk-4r-VNg">
@ -563,7 +591,7 @@
</connections>
</popUpButton>
<popUpButton focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YIJ-Qc-SIZ">
<rect key="frame" x="135" y="412" width="96" height="17"/>
<rect key="frame" x="135" y="412" width="100" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="roundRect" title="Effective Tilemap" bezelStyle="roundedRect" alignment="left" controlSize="mini" lineBreakMode="truncatingTail" state="on" borderStyle="border" focusRingType="none" imageScaling="proportionallyDown" inset="2" selectedItem="XRF-Vj-3gs" id="3W1-Db-wDn">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -581,7 +609,7 @@
</connections>
</popUpButton>
<popUpButton focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k4c-Vg-MBu">
<rect key="frame" x="235" y="412" width="96" height="17"/>
<rect key="frame" x="239" y="412" width="100" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="roundRect" title="Effective Tileset" bezelStyle="roundedRect" alignment="left" controlSize="mini" lineBreakMode="truncatingTail" state="on" borderStyle="border" focusRingType="none" imageScaling="proportionallyDown" inset="2" selectedItem="CRe-dX-rzY" id="h53-sb-Odg">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -649,21 +677,19 @@
</tabView>
</subviews>
</view>
<toolbar key="toolbar" implicitIdentifier="7FF8B5E5-F61B-408C-9634-A5D496FE5E70" autosavesConfiguration="NO" allowsUserCustomization="NO" displayMode="iconOnly" sizeMode="regular" id="SVR-To-n7n">
<toolbar key="toolbar" implicitIdentifier="7FF8B5E5-F61B-408C-9634-A5D496FE5E70" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconOnly" sizeMode="regular" id="SVR-To-n7n">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="hnI-48-dYt"/>
<toolbarItem implicitItemIdentifier="B9D3CF98-8020-4389-9372-F99361440794" label="" paletteLabel="" id="fmK-Jl-Koj">
<toolbarItem implicitItemIdentifier="B9D3CF98-8020-4389-9372-F99361440794" label="" paletteLabel="" sizingBehavior="auto" id="fmK-Jl-Koj">
<nil key="toolTip"/>
<size key="minSize" width="100" height="25"/>
<size key="maxSize" width="268" height="25"/>
<segmentedControl key="view" verticalHuggingPriority="750" id="Aul-vO-dCK">
<rect key="frame" x="0.0" y="14" width="268" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<autoresizingMask key="autoresizingMask"/>
<segmentedCell key="cell" borderStyle="border" alignment="left" style="texturedSquare" trackingMode="selectOne" id="HhR-ky-5NN">
<font key="font" metaFont="system"/>
<segments>
<segment label="Tileset" selected="YES"/>
<segment label="Tilemap" tag="1"/>
<segment label="Tilemap"/>
<segment label="Objects"/>
<segment label="Palettes"/>
</segments>
@ -686,7 +712,7 @@
</connections>
<point key="canvasLocation" x="182" y="760"/>
</window>
<window title="Printer Feed" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="NdE-0B-WCf" customClass="GBPanel">
<window title="Printer" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="NdE-0B-WCf" customClass="GBPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="320" height="288"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
@ -702,7 +728,7 @@
</imageView>
</subviews>
</view>
<toolbar key="toolbar" implicitIdentifier="1FF86A2B-6637-4EE6-A25A-7298D79AE84E" autosavesConfiguration="NO" allowsUserCustomization="NO" displayMode="iconAndLabel" sizeMode="regular" id="gH3-SH-7il">
<toolbar key="toolbar" implicitIdentifier="1FF86A2B-6637-4EE6-A25A-7298D79AE84E" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="gH3-SH-7il">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="15EB8D49-8C6E-42F2-9F7F-F7D7A0BBDAAF" label="Save" paletteLabel="Save" tag="-1" image="NSFolder" id="CBz-1N-o0Q">
<size key="minSize" width="22" height="22"/>
@ -754,7 +780,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" id="mzf-yu-RID">
<rect key="frame" x="1" y="1" width="604" height="257"/>
<autoresizingMask key="autoresizingMask"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="none" alternatingRowBackgroundColors="YES" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="24" headerView="pvX-uJ-qK5" id="tA3-8T-bxb">
<rect key="frame" x="0.0" y="0.0" width="604" height="240"/>
@ -834,8 +860,8 @@
<rect key="frame" x="-1" y="0.0" width="308" height="135"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="hqi-ob-NW9">
<rect key="frame" x="20" y="51" width="176" height="19"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="hqi-ob-NW9">
<rect key="frame" x="20" y="52" width="176" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="To value:" id="Ycx-oE-aA4">
<font key="font" metaFont="system"/>
@ -844,7 +870,7 @@
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Kq8-6F-9GK">
<rect key="frame" x="42" y="21" width="152" height="19"/>
<rect key="frame" x="42" y="22" width="152" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES"/>
<buttonCell key="cell" type="check" title="Only if old value was: " bezelStyle="regularSquare" imagePosition="left" alignment="right" state="on" inset="2" id="LkB-WQ-9Qd">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
@ -854,8 +880,8 @@
<action selector="updateCheat:" target="v7q-gT-jHT" id="kNc-cj-bmF"/>
</connections>
</button>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="C6E-oI-hDC">
<rect key="frame" x="22" y="112" width="276" height="21"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="C6E-oI-hDC">
<rect key="frame" x="22" y="113" width="276" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Description" drawsBackground="YES" usesSingleLineMode="YES" id="2uR-9N-hBb">
<font key="font" metaFont="system"/>
@ -866,7 +892,7 @@
<outlet property="delegate" destination="v7q-gT-jHT" id="zyw-h0-hRP"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" mirrorLayoutDirectionWhenInternationalizing="never" translatesAutoresizingMaskIntoConstraints="NO" id="qHx-1z-daR">
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" mirrorLayoutDirectionWhenInternationalizing="never" translatesAutoresizingMaskIntoConstraints="NO" id="qHx-1z-daR">
<rect key="frame" x="202" y="82" width="96" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" state="on" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="edq-46-JeP" customClass="GBCheatTextFieldCell">
@ -881,7 +907,7 @@
<outlet property="delegate" destination="v7q-gT-jHT" id="79v-33-R1X"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" mirrorLayoutDirectionWhenInternationalizing="never" translatesAutoresizingMaskIntoConstraints="NO" id="N3I-PP-X85">
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" mirrorLayoutDirectionWhenInternationalizing="never" translatesAutoresizingMaskIntoConstraints="NO" id="N3I-PP-X85">
<rect key="frame" x="202" y="51" width="96" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" state="on" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="CV2-D9-WsB" customClass="GBCheatTextFieldCell">
@ -896,7 +922,7 @@
<outlet property="delegate" destination="v7q-gT-jHT" id="P69-nT-oOt"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" mirrorLayoutDirectionWhenInternationalizing="never" translatesAutoresizingMaskIntoConstraints="NO" id="S6O-LB-gSj">
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" mirrorLayoutDirectionWhenInternationalizing="never" translatesAutoresizingMaskIntoConstraints="NO" id="S6O-LB-gSj">
<rect key="frame" x="202" y="20" width="96" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" state="on" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="tpM-ys-MEO" customClass="GBCheatTextFieldCell">
@ -911,8 +937,8 @@
<outlet property="delegate" destination="v7q-gT-jHT" id="6RH-dg-SL7"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="uFo-ly-Veq">
<rect key="frame" x="18" y="82" width="178" height="19"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="uFo-ly-Veq">
<rect key="frame" x="18" y="83" width="178" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Change byte at address:" id="xwa-TF-eY1">
<font key="font" metaFont="system"/>
@ -922,8 +948,8 @@
</textField>
</subviews>
</customView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r5T-ol-Dod">
<rect key="frame" x="316" y="115" width="270" height="16"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r5T-ol-Dod">
<rect key="frame" x="316" y="116" width="270" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Import GameShark or Game Genie cheat:" id="0mf-EN-cKc">
<font key="font" metaFont="system"/>
@ -931,7 +957,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="X7K-nJ-alF">
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="X7K-nJ-alF">
<rect key="frame" x="351" y="82" width="233" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" placeholderString="Code" drawsBackground="YES" usesSingleLineMode="YES" id="2bz-dT-7Fi">
@ -946,7 +972,7 @@
<action selector="selectText:" target="KHj-uX-Wbk" id="11z-0U-tMA"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KHj-uX-Wbk">
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KHj-uX-Wbk">
<rect key="frame" x="351" y="51" width="233" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" placeholderString="Description" drawsBackground="YES" usesSingleLineMode="YES" id="50d-va-Cen">
@ -959,7 +985,7 @@
</connections>
</textField>
<button verticalHuggingPriority="750" id="C3V-Ep-bMj">
<rect key="frame" x="508" y="12" width="83" height="32"/>
<rect key="frame" x="508" y="13" width="83" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES"/>
<buttonCell key="cell" type="push" title="Import" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="mMP-KW-YNy">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
@ -970,7 +996,7 @@
</connections>
</button>
<box horizontalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="P90-u5-8ko">
<rect key="frame" x="304" y="12" width="5" height="123"/>
<rect key="frame" x="302" y="12" width="5" height="123"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
</box>
</subviews>
@ -1000,7 +1026,7 @@
<image name="HelpTemplate" width="14" height="14"/>
<image name="InterruptTemplate" width="14" height="14"/>
<image name="NSFolder" width="32" height="32"/>
<image name="NSStopProgressFreestandingTemplate" width="15" height="15"/>
<image name="NSStopProgressFreestandingTemplate" width="20" height="20"/>
<image name="NextTemplate" width="14" height="14"/>
<image name="StepTemplate" width="14" height="14"/>
<image name="printer" catalog="system" width="18" height="16"/>

View File

@ -8,9 +8,15 @@
#import <JoyKit/JoyKit.h>
#import <WebKit/WebKit.h>
#import <mach-o/dyld.h>
#include <sys/mount.h>
#include <sys/xattr.h>
#define UPDATE_SERVER "https://sameboy.github.io"
@interface NSToolbarItem(private)
- (NSButton *)_view;
@end
static uint32_t color_to_int(NSColor *color)
{
color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
@ -40,7 +46,14 @@ static uint32_t color_to_int(NSColor *color)
- (void) applicationDidFinishLaunching:(NSNotification *)notification
{
// Refresh icon if launched via a software update
[NSApplication sharedApplication].applicationIconImage = [NSImage imageNamed:@"AppIcon"];
if (@available(macOS 26.0, *)) {
// Severely broken on macOS 26
}
else {
NSImage *icon = [[NSWorkspace sharedWorkspace] iconForFile:[[NSBundle mainBundle] bundlePath]];
icon.size = [NSApplication sharedApplication].applicationIconImage.size;
[NSApplication sharedApplication].applicationIconImage = icon;
}
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
for (unsigned i = 0; i < GBKeyboardButtonCount; i++) {
@ -93,6 +106,9 @@ static uint32_t color_to_int(NSColor *color)
@"GBDebuggerFont": hasSFMono? @"SF Mono" : @"Menlo",
@"GBDebuggerFontSize": @12,
@"GBColorPalette": @1,
@"GBTurboCap": @0,
// Default themes
@"GBThemes": @{
@"Canyon": @{
@ -326,9 +342,8 @@ static uint32_t color_to_int(NSColor *color)
#ifndef UPDATE_SUPPORT
[_preferencesWindow.toolbar removeItemAtIndex:4];
#endif
if (@available(macOS 11.0, *)) {
[_preferencesWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier atIndex:0];
[_preferencesWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier atIndex:_preferencesWindow.toolbar.items.count];
for (unsigned i = _preferencesWindow.toolbar.items.count; i--;) {
[_preferencesWindow.toolbar.items[i] _view].imageScaling = NSImageScaleNone;
}
}
[_preferencesWindow makeKeyAndOrderFront:self];
@ -388,7 +403,12 @@ static uint32_t color_to_int(NSColor *color)
else {
self.updateChanges.preferences.standardFontFamily = @"Lucida Grande";
}
self.updateChanges.preferences.fixedFontFamily = @"Menlo";
if (@available(macOS 10.15, *)) {
self.updateChanges.preferences.fixedFontFamily = [NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular].displayName;
}
else {
self.updateChanges.preferences.fixedFontFamily = @"Menlo";
}
self.updateChanges.drawsBackground = false;
[self.updateChanges.mainFrame loadHTMLString:html baseURL:nil];
});
@ -644,6 +664,14 @@ static uint32_t color_to_int(NSColor *color)
}
[[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
[[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil];
// Remove the quarantine flag so we don't have to escape translocation
NSString *bundlePath = [NSBundle mainBundle].bundlePath;
removexattr(bundlePath.UTF8String, "com.apple.quarantine", 0);
for (NSString *path in [[NSFileManager defaultManager] enumeratorAtPath:bundlePath]) {
removexattr([bundlePath stringByAppendingPathComponent:path].UTF8String, "com.apple.quarantine", 0);
};
_downloadDirectory = nil;
atexit_b(^{
execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL);
@ -763,4 +791,27 @@ static uint32_t color_to_int(NSColor *color)
- (IBAction)nop:(id)sender
{
}
/* This runs before C constructors. If we need to escape translocation, we should
do it ASAP to minimize our launch time. */
+ (void)load
{
if (@available(macOS 10.12, *)) {
/* Detect and escape translocation so we can safely update ourselves */
if ([[[NSBundle mainBundle] bundlePath] containsString:@"/AppTranslocation/"]) {
const char *mountPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent].UTF8String;
struct statfs *mntbuf;
int mntsize = getmntinfo(&mntbuf, MNT_NOWAIT);
for (unsigned i = 0; i < mntsize; i++) {
if (strcmp(mntbuf[i].f_mntonname, mountPath) == 0) {
NSBundle *origBundle = [NSBundle bundleWithPath:@(mntbuf[i].f_mntfromname)];
execl(origBundle.executablePath.UTF8String, origBundle.executablePath.UTF8String, NULL);
break;
}
}
}
}
}
@end

5
Cocoa/GBCPUView.h Normal file
View File

@ -0,0 +1,5 @@
#import <Cocoa/Cocoa.h>
@interface GBCPUView : NSView
- (void)addSample:(double)sample;
@end

126
Cocoa/GBCPUView.m Normal file
View File

@ -0,0 +1,126 @@
#import "GBCPUView.h"
#define SAMPLE_COUNT 0x100 // ~4 seconds
@implementation GBCPUView
{
double _samples[SAMPLE_COUNT];
size_t _position;
}
- (void)drawRect:(NSRect)dirtyRect
{
CGRect bounds = self.bounds;
NSSize size = bounds.size;
unsigned factor = [[self.window screen] backingScaleFactor];
NSBitmapImageRep *maskBitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
pixelsWide:(unsigned)size.width * factor
pixelsHigh:(unsigned)size.height * factor
bitsPerSample:8
samplesPerPixel:2
hasAlpha:true
isPlanar:false
colorSpaceName:NSDeviceWhiteColorSpace
bytesPerRow:size.width * 2 * factor
bitsPerPixel:16];
NSGraphicsContext *mainContext = [NSGraphicsContext currentContext];
NSColor *greenColor, *redColor;
if (@available(macOS 10.10, *)) {
greenColor = [NSColor systemGreenColor];
redColor = [NSColor systemRedColor];
}
else {
greenColor = [NSColor colorWithRed:3.0 / 16 green:0.5 blue:5.0 / 16 alpha:1.0];
redColor = [NSColor colorWithRed:13.0 / 16 green:0.25 blue:0.25 alpha:1.0];
}
NSBitmapImageRep *colorBitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
pixelsWide:SAMPLE_COUNT
pixelsHigh:1
bitsPerSample:8
samplesPerPixel:3
hasAlpha:false
isPlanar:false
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:SAMPLE_COUNT * 4
bitsPerPixel:32];
unsigned lastFill = 0;
NSBezierPath *line = [NSBezierPath bezierPath];
bool isRed = false;
{
double sample = _samples[_position % SAMPLE_COUNT];
[line moveToPoint:NSMakePoint(0,
(sample * (size.height - 1) + 0.5) * factor)];
isRed = sample == 1;
}
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:colorBitmap]];
for (unsigned i = 1; i < SAMPLE_COUNT; i++) {
double sample = _samples[(i + _position) % SAMPLE_COUNT];
[line lineToPoint:NSMakePoint(size.width * i * factor / (SAMPLE_COUNT - 1),
(sample * (size.height - 1) + 0.5) * factor)];
if (isRed != (sample == 1)) {
// Color changed
[(isRed? redColor : greenColor) setFill];
NSRectFill(CGRectMake(lastFill, 0, i - lastFill, 1));
lastFill = i;
isRed ^= true;
}
}
[(isRed? redColor : greenColor) setFill];
NSRectFill(CGRectMake(lastFill, 0, SAMPLE_COUNT - lastFill, 1));
NSBezierPath *fill = [line copy];
[fill lineToPoint:NSMakePoint(size.width * factor, 0)];
[fill lineToPoint:NSMakePoint(0, 0)];
NSColor *strokeColor = [NSColor whiteColor];
NSColor *fillColor = [strokeColor colorWithAlphaComponent:1 / 3.0];
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:maskBitmap]];
[fillColor setFill];
[fill fill];
[strokeColor setStroke];
[line setLineWidth:factor];
[line stroke];
CGContextRef maskContext = CGContextRetain([NSGraphicsContext currentContext].graphicsPort);
[NSGraphicsContext setCurrentContext:mainContext];
CGContextSaveGState(mainContext.graphicsPort);
CGImageRef maskImage = CGBitmapContextCreateImage(maskContext);
CGContextClipToMask(mainContext.graphicsPort, bounds, maskImage);
CGImageRelease(maskImage);
NSImage *colors = [[NSImage alloc] initWithSize:NSMakeSize(SAMPLE_COUNT, 1)];
[colors addRepresentation:colorBitmap];
[colors drawInRect:bounds];
CGContextRestoreGState(mainContext.graphicsPort);
CGContextRelease(maskContext);
[super drawRect:dirtyRect];
}
- (void)addSample:(double)sample
{
_samples[_position++] = sample;
if (_position == SAMPLE_COUNT) {
_position = 0;
}
}
@end

5
Cocoa/GBColorTextCell.h Normal file
View File

@ -0,0 +1,5 @@
#import <Cocoa/Cocoa.h>
@interface GBColorTextCell : NSTextFieldCell
@end

19
Cocoa/GBColorTextCell.m Normal file
View File

@ -0,0 +1,19 @@
#import "GBColorTextCell.h"
@implementation GBColorTextCell
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
[self.backgroundColor set];
NSRectFill(cellFrame);
NSBezierPath *path = [NSBezierPath bezierPathWithRect:cellFrame];
path.lineWidth = 2;
[[NSColor colorWithWhite:0 alpha:0.25] setStroke];
[path addClip];
[path stroke];
[self drawInteriorWithFrame:cellFrame inView:controlView];
}
@end

5
Cocoa/GBDisabledButton.h Normal file
View File

@ -0,0 +1,5 @@
#import <Cocoa/Cocoa.h>
@interface GBDisabledButton : NSButton
@end

8
Cocoa/GBDisabledButton.m Normal file
View File

@ -0,0 +1,8 @@
#import "GBDisabledButton.h"
@implementation GBDisabledButton
- (void)mouseDown:(NSEvent *)event
{
}
@end

View File

@ -136,7 +136,7 @@
if (!_gb) {
return [NSString stringWithFormat:@"$%llX", offset];
}
return @(GB_debugger_describe_address(_gb, offset + _baseAddress, _bankForDescription, false, isRangeEnd));
return @(GB_debugger_describe_address(_gb, offset + _baseAddress, offset < 0x4000? -1 :_bankForDescription, false, isRangeEnd));
}

View File

@ -46,17 +46,29 @@
-(void)drawKnob:(NSRect)knobRect
{
[super drawKnob:knobRect];
NSRect peekRect = knobRect;
peekRect.size.width /= 2;
peekRect.size.height = peekRect.size.width;
peekRect.origin.x += peekRect.size.width / 2;
peekRect.origin.y += peekRect.size.height / 2;
NSBezierPath *path = nil;
if (@available(macos 26.0, *)) {
NSRect peekRect = knobRect;
peekRect.size.height /= 2;
peekRect.size.width -= peekRect.size.height;
peekRect.origin.x += peekRect.size.height / 2;
peekRect.origin.y += peekRect.size.height / 2;
path = [NSBezierPath bezierPathWithRoundedRect:peekRect xRadius:peekRect.size.height / 2 yRadius:peekRect.size.height / 2];
}
else {
NSRect peekRect = knobRect;
peekRect.size.width /= 2;
peekRect.size.height = peekRect.size.width;
peekRect.origin.x += peekRect.size.width / 2;
peekRect.origin.y += peekRect.size.height / 2;
path = [NSBezierPath bezierPathWithOvalInRect:peekRect];
}
NSColor *color = self.colorValue;
if (!self.enabled) {
color = [color colorWithAlphaComponent:0.5];
}
[color setFill];
NSBezierPath *path = [NSBezierPath bezierPathWithOvalInRect:peekRect];
[path fill];
[[NSColor colorWithWhite:0 alpha:0.25] setStroke];
[path setLineWidth:0.5];

View File

@ -44,7 +44,8 @@
}
if (parent.displayScrollRect) {
NSBezierPath *path = [NSBezierPath bezierPathWithRect:CGRectInfinite];
// CGRectInfinite in NSBezierPath is broken in newer macOS versions
NSBezierPath *path = [NSBezierPath bezierPathWithRect:CGRectMake(-0x4000, -0x4000, 0x8000, 0x8000)];
for (unsigned x = 0; x < 2; x++) {
for (unsigned y = 0; y < 2; y++) {
NSRect rect = parent.scrollRect;

View File

@ -16,7 +16,7 @@
static GBJoyConManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[self alloc] _init];
manager = [[super allocWithZone:nil] _init];
});
return manager;
}
@ -32,6 +32,16 @@
return ret;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [self sharedInstance];
}
+ (instancetype)alloc
{
return [self sharedInstance];
}
- (instancetype)init
{
return [self.class sharedInstance];

View File

@ -399,10 +399,16 @@ static double blend(double from, double to, double position)
[sender.window.sheetParent endSheet:sender.window];
}
- (instancetype)init
+ (instancetype)alloc
{
static id singleton = nil;
if (singleton) return singleton;
return (singleton = [super init]);
return (singleton = [super allocWithZone:nil]);
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [self alloc];
}
@end

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -22,44 +22,44 @@
<rect key="frame" x="0.0" y="0.0" width="512" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ypt-t4-Mf3">
<rect key="frame" x="131" y="0.0" width="96" height="24"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ypt-t4-Mf3">
<rect key="frame" x="131" y="0.0" width="95" height="24"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" borderStyle="border" alignment="center" title="$7FFF" drawsBackground="YES" id="sKu-Uy-2LG">
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" borderStyle="border" alignment="center" title="$7FFF" drawsBackground="YES" id="sKu-Uy-2LG" customClass="GBColorTextCell">
<font key="font" size="13" name="Menlo-Regular"/>
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KkX-Z8-Sqi">
<rect key="frame" x="226" y="0.0" width="96" height="24"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KkX-Z8-Sqi">
<rect key="frame" x="226" y="0.0" width="95" height="24"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" borderStyle="border" alignment="center" title="$7FFF" drawsBackground="YES" id="9LH-TF-W1L">
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" borderStyle="border" alignment="center" title="$7FFF" drawsBackground="YES" id="9LH-TF-W1L" customClass="GBColorTextCell">
<font key="font" size="13" name="Menlo-Regular"/>
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jDk-Ej-4yI">
<rect key="frame" x="321" y="0.0" width="96" height="24"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jDk-Ej-4yI">
<rect key="frame" x="321" y="0.0" width="95" height="24"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" borderStyle="border" alignment="center" title="$7FFF" drawsBackground="YES" id="arE-i5-zCC">
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" borderStyle="border" alignment="center" title="$7FFF" drawsBackground="YES" id="arE-i5-zCC" customClass="GBColorTextCell">
<font key="font" size="13" name="Menlo-Regular"/>
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7PI-YE-fTk">
<rect key="frame" x="416" y="0.0" width="96" height="24"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7PI-YE-fTk">
<rect key="frame" x="416" y="0.0" width="95" height="24"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" borderStyle="border" alignment="center" title="$7FFF" drawsBackground="YES" id="ZbU-nE-FsE">
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" borderStyle="border" alignment="center" title="$7FFF" drawsBackground="YES" id="ZbU-nE-FsE" customClass="GBColorTextCell">
<font key="font" size="13" name="Menlo-Regular"/>
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NOK-yI-LKh">
<rect key="frame" x="4" y="0.0" width="121" height="20"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NOK-yI-LKh">
<rect key="frame" x="8" y="0.0" width="121" height="20"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Background 0" id="qM4-cY-SDE">
<font key="font" usesAppearanceFont="YES"/>

View File

@ -19,6 +19,9 @@
@property IBOutlet NSPopUpButton *colorPalettePopupButton;
@property IBOutlet NSPopUpButton *hotkey1PopupButton;
@property IBOutlet NSPopUpButton *hotkey2PopupButton;
@property IBOutlet NSButton *turboCapButton;
@property IBOutlet NSSlider *turboCapSlider;
@property IBOutlet NSTextField *turboCapLabel;
@property IBOutlet GBTitledPopUpButton *fontPopupButton;
@property IBOutlet NSStepper *fontSizeStepper;

View File

@ -20,7 +20,7 @@
- (NSWindowToolbarStyle)toolbarStyle
{
return NSWindowToolbarStyleExpanded;
return NSWindowToolbarStylePreference;
}
- (void)close
@ -342,6 +342,13 @@ static inline NSString *keyEquivalentString(NSMenuItem *item)
_fontSizeStepper.intValue = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"];
[self updateFonts];
double cap = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"];
if (cap) {
_turboCapSlider.intValue = round(cap * 100);
_turboCapButton.state = NSOnState;
}
[self turboCapToggled:_turboCapButton];
}
- (IBAction)fontSizeChanged:(id)sender
@ -555,4 +562,35 @@ static inline NSString *keyEquivalentString(NSMenuItem *item)
[GBJoyConManager sharedInstance].arrangementMode = false;
}
- (IBAction)turboCapToggled:(NSButton *)sender
{
if (sender.state) {
_turboCapSlider.enabled = true;
[self turboCapChanged:_turboCapSlider];
if (@available(macOS 10.10, *)) {
_turboCapLabel.textColor = [NSColor labelColor];
}
else {
_turboCapLabel.textColor = [NSColor blackColor];
}
}
else {
_turboCapSlider.enabled = false;
_turboCapLabel.enabled = false;
[[NSUserDefaults standardUserDefaults] setDouble:0 forKey:@"GBTurboCap"];
if (@available(macOS 10.10, *)) {
_turboCapLabel.textColor = [NSColor disabledControlTextColor];
}
else {
_turboCapLabel.textColor = [NSColor colorWithWhite:0 alpha:0.25];
}
}
}
- (IBAction)turboCapChanged:(NSSlider *)sender
{
_turboCapLabel.stringValue = [NSString stringWithFormat:@"%d%%", sender.intValue];
[[NSUserDefaults standardUserDefaults] setDouble:sender.doubleValue / 100.0 forKey:@"GBTurboCap"];
}
@end

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14868" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14868"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -25,7 +25,7 @@
<rect key="frame" x="0.0" y="0.0" width="332" height="221"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="H3v-X3-48q">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="H3v-X3-48q">
<rect key="frame" x="18" y="192" width="296" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Title" id="BwZ-Zj-sP6">
@ -34,7 +34,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="gaD-ZH-Beh">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="gaD-ZH-Beh">
<rect key="frame" x="18" y="166" width="296" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Author" id="IgT-r1-T38">
@ -59,7 +59,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" alignment="center" lineBreakMode="truncatingTail" borderStyle="border" focusRingType="none" imageScaling="proportionallyDown" inset="2" id="YJh-dI-A5D">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<font key="font" metaFont="message"/>
<menu key="menu" id="Knp-Ok-Pb4"/>
</popUpButtonCell>
<connections>
@ -67,7 +67,7 @@
</connections>
</popUpButton>
<segmentedControl verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="SRS-M5-VVL">
<rect key="frame" x="247" y="129" width="68" height="24"/>
<rect key="frame" x="247" y="128" width="68" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<segmentedCell key="cell" borderStyle="border" alignment="left" style="rounded" trackingMode="momentary" id="cmq-I8-cFL">
<font key="font" metaFont="system"/>
@ -94,7 +94,7 @@
</customView>
</subviews>
</customView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="2dl-dH-E3J">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="2dl-dH-E3J">
<rect key="frame" x="18" y="5" width="296" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="clipping" alignment="center" title="Copyright" id="nM9-oF-OV9">

View File

@ -11,23 +11,23 @@
@implementation GBTerminalTextFieldCell
{
GBTerminalTextView *field_editor;
GBTerminalTextView *_fieldEditor;
}
- (NSTextView *)fieldEditorForView:(NSTextField *)controlView
{
if (field_editor) {
field_editor.gb = self.gb;
return field_editor;
if (_fieldEditor) {
_fieldEditor.gb = self.gb;
return _fieldEditor;
}
field_editor = [[GBTerminalTextView alloc] init];
[field_editor setFieldEditor:true];
field_editor.gb = self.gb;
field_editor->_field = (NSTextField *)controlView;
_fieldEditor = [[GBTerminalTextView alloc] init];
[_fieldEditor setFieldEditor:true];
_fieldEditor.gb = self.gb;
_fieldEditor->_field = (NSTextField *)controlView;
((NSTextFieldCell *)controlView.cell).textInset =
field_editor.textContainerInset =
NSMakeSize(0, 2);
return field_editor;
_fieldEditor.textContainerInset =
NSMakeSize(7, 2);
return _fieldEditor;
}
@end

View File

@ -0,0 +1,5 @@
#import <Cocoa/Cocoa.h>
@interface GBToolbarFieldCell : NSSearchFieldCell
@end

View File

@ -0,0 +1,40 @@
#import "GBToolbarFieldCell.h"
#import <objc/runtime.h>
@interface NSTextFieldCell()
- (void)textDidChange:(id)sender;
@end
@implementation GBToolbarFieldCell
- (void)textDidChange:(id)sender
{
IMP imp = [NSTextFieldCell instanceMethodForSelector:_cmd];
method_setImplementation(class_getInstanceMethod([GBToolbarFieldCell class], _cmd), imp);
((void(*)(id, SEL, id))imp)(self, _cmd, sender);
}
- (void)endEditing:(NSText *)textObj
{
IMP imp = [NSTextFieldCell instanceMethodForSelector:_cmd];
method_setImplementation(class_getInstanceMethod([GBToolbarFieldCell class], _cmd), imp);
((void(*)(id, SEL, id))imp)(self, _cmd, textObj);
}
- (void)resetSearchButtonCell
{
}
- (void)resetCancelButtonCell
{
}
// We only need this hack on Solarium, avoid regressions
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (@available(macOS 26.0, *)) {
return [super allocWithZone:zone];
}
return (id)[NSTextFieldCell allocWithZone:zone];
}
@end

View File

@ -0,0 +1,5 @@
#import <Cocoa/Cocoa.h>
@interface GBToolbarPopUpButtonCell : NSPopUpButtonCell
@end

View File

@ -0,0 +1,17 @@
#import "GBToolbarPopUpButtonCell.h"
@implementation GBToolbarPopUpButtonCell
-(void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
[super drawInteriorWithFrame:CGRectInset(cellFrame, 5, 0) inView:controlView];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
if (@available(macOS 26.0, *)) {
return [super allocWithZone:zone];
}
return (id)[NSPopUpButtonCell allocWithZone:zone];
}
@end

View File

@ -148,6 +148,9 @@ static const uint8_t workboy_vk_to_key[] = {
[self observeStandardDefaultsKey:@"GBAspectRatioUnkept" withBlock:^(id newValue) {
[weakSelf setFrame:weakSelf.superview.frame];
}];
[self observeStandardDefaultsKey:@"GBForceIntegerScale" withBlock:^(id newValue) {
[weakSelf setFrame:weakSelf.superview.frame];
}];
[self observeStandardDefaultsKey:@"JoyKitDefaultControllers" withBlock:^(id newValue) {
[weakSelf reassignControllers];
}];
@ -282,7 +285,9 @@ static const uint8_t workboy_vk_to_key[] = {
- (void)setFrame:(NSRect)frame
{
frame = self.superview.frame;
NSView *superview = self.superview;
if (GB_unlikely(!superview)) return;
frame = superview.frame;
if (_gb && ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAspectRatioUnkept"]) {
double ratio = frame.size.width / frame.size.height;
double width = GB_get_screen_width(_gb);
@ -300,6 +305,19 @@ static const uint8_t workboy_vk_to_key[] = {
frame.origin.x = 0;
}
}
if (_gb && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBForceIntegerScale"]) {
double factor = self.window.backingScaleFactor;
double width = GB_get_screen_width(_gb) / factor;
double height = GB_get_screen_height(_gb) / factor;
double new_width = floor(frame.size.width / width) * width;
double new_height = floor(frame.size.height / height) * height;
frame.origin.x += floor((frame.size.width - new_width) / 2);
frame.origin.y += floor((frame.size.height - new_height) / 2);
frame.size.width = new_width;
frame.size.height = new_height;
}
[super setFrame:frame];
}

5
Cocoa/GBWindow.h Normal file
View File

@ -0,0 +1,5 @@
#import <Cocoa/Cocoa.h>
@interface GBWindow : NSWindow
@end

22
Cocoa/GBWindow.m Normal file
View File

@ -0,0 +1,22 @@
#import "GBWindow.h"
@interface NSWindow(private)
- (void)_zoomFill:(id)sender;
@end
/*
For some reason, Apple replaced the alt + zoom button behavior to be "fill" rather than zoom.
I don't like that. It prevents SameBoy's integer scaling from working. Let's restore it.
*/
@implementation GBWindow
- (void)_zoomFill:(id)sender
{
if (sender == [self standardWindowButton:NSWindowZoomButton] &&
((self.currentEvent.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagOption)) {
[self zoom:sender];
return;
}
[super _zoomFill:sender];
}
@end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -113,7 +113,9 @@
<key>CFBundleExecutable</key>
<string>SameBoy</string>
<key>CFBundleIconFile</key>
<string>AppIcon.icns</string>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>com.github.liji32.sameboy</string>
<key>LSApplicationCategoryType</key>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

After

Width:  |  Height:  |  Size: 461 B

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.9" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.9"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -82,7 +82,7 @@
<action selector="openDocument:" target="-1" id="bVn-NM-KNZ"/>
</connections>
</menuItem>
<menuItem title="Hot Swap Cartridge…" keyEquivalent="o" id="Rik-p8-QCf">
<menuItem title="Hot Swap Cartridge…" secondaryImage="arrow.down.left.arrow.up.right" catalog="system" keyEquivalent="o" id="Rik-p8-QCf">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="cartSwap:" target="-1" id="iXS-Ol-IS0"/>
@ -102,7 +102,7 @@
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
<menuItem title="New Cartridge Instance…" keyEquivalent="n" id="Vld-be-NZu">
<menuItem title="New Cartridge Instance…" secondaryImage="plus.square.on.square" catalog="system" keyEquivalent="n" id="Vld-be-NZu">
<connections>
<action selector="newCartridgeInstance:" target="-1" id="GJc-xU-ZEr"/>
</connections>
@ -158,7 +158,7 @@
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="Aru-vr-frG"/>
<menuItem title="Find" id="efg-jw-GVP">
<menuItem title="Find" secondaryImage="text.page.badge.magnifyingglass" catalog="system" id="efg-jw-GVP">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="4R6-IU-Jq6">
<items>
@ -197,23 +197,23 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Emulation" id="HyV-fh-RgO">
<items>
<menuItem title="Reset" keyEquivalent="r" id="p0i-Lt-sTg">
<menuItem title="Reset" secondaryImage="arrow.trianglehead.2.counterclockwise.rotate.90" catalog="system" keyEquivalent="r" id="p0i-Lt-sTg">
<connections>
<action selector="reset:" target="-1" id="DKW-Bd-h3v"/>
</connections>
</menuItem>
<menuItem title="Quick Reset" tag="-1" alternate="YES" keyEquivalent="r" id="uPG-01-49E">
<menuItem title="Quick Reset" secondaryImage="arrow.trianglehead.2.counterclockwise.rotate.90" catalog="system" tag="-1" alternate="YES" keyEquivalent="r" id="uPG-01-49E">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="reset:" target="-1" id="VV1-VP-L7g"/>
</connections>
</menuItem>
<menuItem title="Reload ROM" alternate="YES" keyEquivalent="R" id="eQP-Fb-nkz">
<menuItem title="Reload ROM" secondaryImage="arrow.trianglehead.2.counterclockwise.rotate.90" catalog="system" alternate="YES" keyEquivalent="R" id="eQP-Fb-nkz">
<connections>
<action selector="reloadROM:" target="-1" id="BpN-8V-Csg"/>
</connections>
</menuItem>
<menuItem title="Pause" keyEquivalent="p" id="4K4-hw-R7Q">
<menuItem title="Pause" secondaryImage="pause" catalog="system" keyEquivalent="p" id="4K4-hw-R7Q">
<connections>
<action selector="togglePause:" target="-1" id="osW-wt-QAa"/>
</connections>
@ -263,7 +263,7 @@
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="QIS-av-Byy"/>
<menuItem title="Save State" id="Hdz-ut-okE">
<menuItem title="Save State" secondaryImage="square.and.arrow.down" catalog="system" id="Hdz-ut-okE">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Save State" id="Mxx-u1-M9D">
<items>
@ -320,7 +320,7 @@
</items>
</menu>
</menuItem>
<menuItem title="Load State" id="EXD-SL-cz4">
<menuItem title="Load State" secondaryImage="square.and.arrow.up" catalog="system" id="EXD-SL-cz4">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Load State" id="l9D-Ej-sh2">
<items>
@ -388,7 +388,7 @@
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="5GS-tt-E0a"/>
<menuItem title="Save Screenshot" keyEquivalent="s" id="0J3-yf-iXs">
<menuItem title="Save Screenshot" secondaryImage="photo" catalog="system" keyEquivalent="s" id="0J3-yf-iXs">
<connections>
<action selector="saveScreenshot:" target="-1" id="gJd-ml-J8p"/>
</connections>
@ -405,12 +405,12 @@
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="DPb-Sh-5tg"/>
<menuItem title="Start Audio Recording…" keyEquivalent="A" id="1UK-8n-QPP">
<menuItem title="Start Audio Recording…" secondaryImage="record.circle" catalog="system" keyEquivalent="A" id="1UK-8n-QPP">
<connections>
<action selector="toggleAudioRecording:" target="-1" id="YE5-mi-Yzd"/>
</connections>
</menuItem>
<menuItem title="Mute Sound" keyEquivalent="m" id="zo0-Rh-wTu">
<menuItem title="Mute Sound" secondaryImage="speaker.slash" catalog="system" keyEquivalent="m" id="zo0-Rh-wTu">
<connections>
<action selector="mute:" target="-1" id="eK3-ea-ExJ"/>
</connections>
@ -433,7 +433,7 @@
<action selector="showCheats:" target="-1" id="stn-Ei-aFE"/>
</connections>
</menuItem>
<menuItem title="Search Cheats" id="LZV-QK-YXi">
<menuItem title="Search Cheats" secondaryImage="text.page.badge.magnifyingglass" catalog="system" id="LZV-QK-YXi">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="showCheatSearch:" target="-1" id="eI1-x0-ykn"/>
@ -442,9 +442,9 @@
</items>
</menu>
</menuItem>
<menuItem title="Connectivity" id="IcW-ZC-4wb">
<menuItem title="Connect" id="IcW-ZC-4wb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Connectivity" id="BDM-Cv-BOm">
<menu key="submenu" title="Connect" id="BDM-Cv-BOm">
<items>
<menuItem title="None" id="SiH-Q4-OBY">
<modifierMask key="keyEquivalentModifierMask"/>
@ -452,7 +452,7 @@
<action selector="disconnectAllAccessories:" target="-1" id="5hY-9U-nRn"/>
</connections>
</menuItem>
<menuItem title="Game Link Cable &amp; Infrared" id="V4S-Fo-xJK">
<menuItem title="Game Link Cable &amp; Infrared" secondaryImage="cable.connector.horizontal" catalog="system" id="V4S-Fo-xJK">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Game Link Cable &amp; Infrared" id="6sJ-Wz-QLj">
<connections>
@ -463,13 +463,13 @@
<action selector="nop:" target="-3" id="Bpa-0C-lkN"/>
</connections>
</menuItem>
<menuItem title="Game Boy Printer" id="zHR-Ha-pOR">
<menuItem title="Game Boy Printer" secondaryImage="printer" catalog="system" id="zHR-Ha-pOR">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="connectPrinter:" target="-1" id="tl1-CL-tAw"/>
</connections>
</menuItem>
<menuItem title="Workboy" id="lo9-CX-BJj">
<menuItem title="Workboy" secondaryImage="keyboard" catalog="system" id="lo9-CX-BJj">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="connectWorkboy:" target="-1" id="6vS-bq-wAX"/>
@ -482,14 +482,14 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Develop" id="UVb-cc-at0">
<items>
<menuItem title="Developer Mode" id="Qx6-Tt-zZR">
<menuItem title="Developer Mode" secondaryImage="wrench.and.screwdriver" catalog="system" id="Qx6-Tt-zZR">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleDeveloperMode:" target="-3" id="PIc-o3-bzb"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="66c-T0-8pW"/>
<menuItem title="Show Console" id="Wse-UY-Y9l">
<menuItem title="Show Console" secondaryImage="apple.terminal" catalog="system" id="Wse-UY-Y9l">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="showConsoleWindow:" target="-1" id="mFf-4i-jLG"/>
@ -501,14 +501,14 @@
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="3If-Yf-U7B"/>
<menuItem title="Break Debugger" keyEquivalent="c" id="uBD-GY-Doi">
<menuItem title="Break Debugger" secondaryImage="pause" catalog="system" keyEquivalent="c" id="uBD-GY-Doi">
<modifierMask key="keyEquivalentModifierMask" control="YES"/>
<connections>
<action selector="interrupt:" target="-1" id="ZmB-wG-fTs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="M6n-8G-LZS"/>
<menuItem title="Audio Channels" id="Cib-LN-Y4o">
<menuItem title="Audio Channels" secondaryImage="speaker.wave.3" catalog="system" id="Cib-LN-Y4o">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Audio Channels" id="l22-4p-yTK">
<items>
@ -540,13 +540,13 @@
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="v5c-ri-BoZ"/>
<menuItem title="Show Background and Window" state="on" id="yfD-Qd-zoz">
<menuItem title="Show Background and Window" state="on" secondaryImage="mountain.2" catalog="system" id="yfD-Qd-zoz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleDisplayBackground:" target="-1" id="p5b-1n-SPR"/>
</connections>
</menuItem>
<menuItem title="Show Objects" state="on" id="OWx-a0-vQk">
<menuItem title="Show Objects" state="on" secondaryImage="cube" catalog="system" id="OWx-a0-vQk">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleDisplayObjects:" target="-1" id="8ie-ey-739"/>
@ -620,4 +620,24 @@
<point key="canvasLocation" x="140" y="260"/>
</menu>
</objects>
<resources>
<image name="apple.terminal" catalog="system" width="18" height="14"/>
<image name="arrow.down.left.arrow.up.right" catalog="system" width="18" height="17"/>
<image name="arrow.trianglehead.2.counterclockwise.rotate.90" catalog="system" width="16" height="15"/>
<image name="cable.connector.horizontal" catalog="system" width="19" height="7"/>
<image name="cube" catalog="system" width="16" height="17"/>
<image name="keyboard" catalog="system" width="19" height="13"/>
<image name="mountain.2" catalog="system" width="25" height="14"/>
<image name="pause" catalog="system" width="9" height="13"/>
<image name="photo" catalog="system" width="18" height="14"/>
<image name="plus.square.on.square" catalog="system" width="17" height="16"/>
<image name="printer" catalog="system" width="18" height="16"/>
<image name="record.circle" catalog="system" width="15" height="15"/>
<image name="speaker.slash" catalog="system" width="14" height="16"/>
<image name="speaker.wave.3" catalog="system" width="22" height="15"/>
<image name="square.and.arrow.down" catalog="system" width="15" height="17"/>
<image name="square.and.arrow.up" catalog="system" width="15" height="18"/>
<image name="text.page.badge.magnifyingglass" catalog="system" width="15" height="18"/>
<image name="wrench.and.screwdriver" catalog="system" width="19" height="18"/>
</resources>
</document>

View File

@ -3,8 +3,8 @@
#import <objc/runtime.h>
@interface NSTextFieldCell ()
- (CGRect)_textLayerDrawingRectForCellFrame:(CGRect)rect;
@property NSSize textInset;
- (bool)_isEditingInView:(NSView *)view;
@end
@implementation NSTextFieldCell (Inset)
@ -19,21 +19,59 @@
return [objc_getAssociatedObject(self, _cmd) sizeValue];
}
- (CGRect)_textLayerDrawingRectForCellFrameHook:(CGRect)rect
- (void)drawWithFrameHook:(NSRect)cellFrame inView:(NSView *)controlView
{
CGRect ret = [self _textLayerDrawingRectForCellFrameHook:rect];
NSSize inset = self.textInset;
ret.origin.x += inset.width;
ret.origin.y += inset.height;
ret.size.width -= inset.width;
ret.size.height -= inset.height;
return ret;
if (self.drawsBackground) {
[self.backgroundColor setFill];
if ([self _isEditingInView:controlView]) {
NSRectFill(cellFrame);
}
else {
NSRectFill(NSMakeRect(cellFrame.origin.x, cellFrame.origin.y,
cellFrame.size.width, inset.height));
NSRectFill(NSMakeRect(cellFrame.origin.x, cellFrame.origin.y + cellFrame.size.height - inset.height,
cellFrame.size.width, inset.height));
NSRectFill(NSMakeRect(cellFrame.origin.x, cellFrame.origin.y + inset.height,
inset.width, cellFrame.size.height - inset.height * 2));
NSRectFill(NSMakeRect(cellFrame.origin.x + cellFrame.size.width - inset.width, cellFrame.origin.y + inset.height,
inset.width, cellFrame.size.height - inset.height * 2));
}
}
cellFrame.origin.x += inset.width;
cellFrame.origin.y += inset.height;
cellFrame.size.width -= inset.width * 2;
cellFrame.size.height -= inset.height * 2;
[self drawWithFrameHook:cellFrame inView:controlView];
}
+ (void)load
{
method_exchangeImplementations(class_getInstanceMethod(self, @selector(_textLayerDrawingRectForCellFrame:)),
class_getInstanceMethod(self, @selector(_textLayerDrawingRectForCellFrameHook:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(drawWithFrame:inView:)),
class_getInstanceMethod(self, @selector(drawWithFrameHook:inView:)));
}
@end
@implementation NSTextField (Inset)
- (bool)wantsUpdateLayerHook
{
CGSize inset = ((NSTextFieldCell *)self.cell).textInset;
if (inset.width || inset.height) return false;
return [self wantsUpdateLayerHook];
}
+ (void)load
{
Method method = class_getInstanceMethod(self, @selector(wantsUpdateLayer));
if (class_addMethod(self, @selector(wantsUpdateLayer), method_getImplementation(method), method_getTypeEncoding(method))) {
method = class_getInstanceMethod(self, @selector(wantsUpdateLayer));
}
method_exchangeImplementations(method,
class_getInstanceMethod(self, @selector(wantsUpdateLayerHook)));
}
@end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>

168
Cocoa/SolariumFixer.m Normal file
View File

@ -0,0 +1,168 @@
#import <Cocoa/Cocoa.h>
#import <objc/runtime.h>
// Comment out to debug
#define NSLog(...)
// Solarium has weird proportions, we need to fix them.
@implementation NSControl (SolariumFixer)
- (void)awakeFromNib
{
if (@available(macOS 26.0, *)) {
if ([self.superview isKindOfClass:objc_getClass("NSToolbarItemViewer")]) return;
if ([self isKindOfClass:[NSStepper class]]) {
NSLog(@"Stepper needs adjustment: %s in window %@", sel_getName(self.action), self.window.title);
CGRect frame = self.frame;
frame.origin.y += 1;
self.frame = frame;
return;
}
if (self.controlSize != NSControlSizeRegular) return;
if ([self isKindOfClass:[NSPopUpButton class]] ) {
NSLog(@"Popup button needs adjustment: %@ in window %@", ((NSButton *)self).title, self.window.title);
CGRect frame = self.frame;
if (frame.size.height != 25) {
NSLog(@"%@ in window %@ has the wrong height!", ((NSButton *)self).title, self.window.title);
return;
}
frame.size.width -= 2 + 5; // Remove 5 from the right and 2 from the left
frame.origin.x += 2;
frame.origin.y += 2;
self.frame = frame;
}
else if ([self isKindOfClass:[NSSegmentedControl class]] ) {
NSLog(@"Segmented button needs adjustment: %s in window %@", sel_getName(self.action), self.window.title);
CGRect frame = self.frame;
if (frame.size.height != 25) {
NSLog(@"%s in window %@ has the wrong height!", sel_getName(self.action), self.window.title);
return;
}
frame.origin.x += 8;
frame.origin.y += 1;
self.frame = frame;
}
else if ([self isKindOfClass:[NSTextField class]]) {
NSTextField *field = (id)self;
if (![field isBezeled]) return;
NSLog(@"Text field needs adjustment: %@ in window %@", ((NSTextField *)self).placeholderString, self.window.title);
CGRect frame = self.frame;
if (frame.size.height == 21) {
frame.size.height = 24;
}
else {
NSLog(@"%@ in window %@ has the wrong height!", ((NSTextField *)self).placeholderString, self.window.title);
return;
}
frame.size.width -= 1 + 1; // Remove 1 from the right and 1 from the left
frame.origin.x += 1;
frame.origin.y -= 1;
self.frame = frame;
}
else if ([self isKindOfClass:[NSButton class]]) {
NSLog(@"Button: %@ in window %@", @(sel_getName(self.action)), self.window.title);
NSButton *button = (id)self;
if (!button.isBordered) return;
if (button.bezelStyle == NSBezelStylePush) {
NSLog(@"Button needs adjustment: %@ in window %@", @(sel_getName(self.action)), self.window.title);
CGRect frame = self.frame;
frame.size.width -= 7 + 7; // Remove 7 from the right and 7 from the left
frame.origin.x += 7;
frame.origin.y += 5;
if (frame.size.height == 32) {
frame.size.height = 25;
}
else {
NSLog(@"%@ in window %@ has the wrong height!", @(sel_getName(self.action)), self.window.title);
} self.frame = frame;
}
else if (button.bezelStyle == NSBezelStyleRegularSquare) {
CGRect frame = self.frame;
if (frame.size.height != 18) return;
NSLog(@"Check/Radio needs adjustment: %@ in window %@", ((NSButton *)self).title, self.window.title);
frame.size.width -= 2;
frame.origin.x += 2;
frame.origin.y += 1;
frame.size.height = 16;
self.frame = frame;
}
else if (button.bezelStyle == NSBezelStyleHelpButton) {
CGRect frame = self.frame;
NSLog(@"Help button needs adjustment: %@ in window %@", ((NSButton *)self).title, self.window.title);
frame.origin.y += 2;
self.frame = frame;
}
}
else if ([self isKindOfClass:[NSSlider class]]) {
NSLog(@"Slider needs adjustment: %s in window %@", sel_getName(self.action), self.window.title);
CGRect frame = self.frame;
frame.origin.y += 3;
self.frame = frame;
}
}
}
@end
@implementation NSToolbarItem (SolariumFixer)
static CGSize minSizeHook(id self, SEL _cmd)
{
return CGSizeMake(8, 0);
}
- (void)awakeFromNib
{
if (@available(macOS 26.0, *)) {
NSLog(@"Toolbar item %@ has view %@", self.label, self.view);
if ([self.view isKindOfClass:[NSTextField class]]) {
NSLog(@"Handling (Text field)");
self.bordered = true;
NSSize maxSize = self.maxSize;
maxSize.height = 36;
self.maxSize = maxSize;
NSSize minSize = self.minSize;
minSize.height = 36;
self.minSize = minSize;
((NSTextField *)self.view).backgroundColor = [NSColor clearColor];
((NSTextField *)self.view).bezeled = false;
((NSTextField *)self.view).bordered = true;
// Work around even more AppKit bugs
self.toolbar.displayMode++;
self.toolbar.displayMode--;
}
else if ([self.view isKindOfClass:[NSPopUpButton class]]) {
NSLog(@"Handling (Pop up button)");
self.bordered = true;
NSSize maxSize = self.maxSize;
maxSize.height = 28;
self.maxSize = maxSize;
NSSize minSize = self.minSize;
minSize.height = 28;
self.minSize = minSize;
}
}
else if (@available(macOS 11.0, *)) { // While at it, make macOS 11-15 a bit more consistent
if ([self.view isKindOfClass:[NSTextField class]]) {
((NSTextField *)self.view).bezelStyle = NSTextFieldRoundedBezel;
}
}
}
+ (void)load
{
if (@available(macOS 26.0, *)) {
method_setImplementation(class_getInstanceMethod(objc_getClass("NSToolbarFlexibleSpaceItem"), @selector(minSize)),
(void *)minSizeHook);
}
}
@end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 B

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 B

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 B

After

Width:  |  Height:  |  Size: 543 B

View File

@ -6,7 +6,7 @@
#include <stdlib.h>
#include "gb.h"
/* Band limited synthesis based on: http://www.slack.net/~ant/bl-synth/ */
/* Band limited synthesis loosely based on: http://www.slack.net/~ant/bl-synth/ */
static int32_t band_limited_steps[GB_BAND_LIMITED_PHASES][GB_BAND_LIMITED_WIDTH];
static void __attribute__((constructor)) band_limited_init(void)
@ -15,37 +15,42 @@ static void __attribute__((constructor)) band_limited_init(void)
double *master = malloc(master_size * sizeof(*master));
memset(master, 0, master_size * sizeof(*master));
const unsigned sine_size = 256 * GB_BAND_LIMITED_PHASES + 2;
const unsigned max_harmonic = sine_size / 2 / GB_BAND_LIMITED_PHASES;
nounroll for (unsigned harmonic = 1; harmonic <= max_harmonic; harmonic += 2) {
double amplitude = 1.0 / harmonic / 2;
double to_angle = M_PI * 2 / sine_size * harmonic;
nounroll for (unsigned i = 0; i < master_size; i++) {
master[i] += sin(((signed)(i + 1) - (signed)master_size / 2) * to_angle) * amplitude;
}
const double lowpass = 15.0 / 16.0; // 1.0 means using Nyquist as the exact cutoff
const double to_angle = M_PI / GB_BAND_LIMITED_PHASES * lowpass;
double sum = 0;
nounroll for (signed i = 0; i < master_size; i++) {
// Exact Blackman window
const double a0 = 7938 / 18608.0;
const double a1 = 9240 / 18608.0;
const double a2 = 1430 / 18608.0;
double window_angle = (2.0 * M_PI * i) / (master_size);
double window = a0 - a1 * cos(window_angle) + a2 * cos(2 * window_angle);
double angle = (i - (signed)master_size / 2) * to_angle;
sum += master[i] = (angle == 0? 1 : sin(angle) / angle) * window;
}
// Normalize master waveform
nounroll for (unsigned i = 0; i < master_size - 1; i++) {
master[i] += master[master_size - 1];
master[i] /= master[master_size - 1] * 2;
nounroll for (signed i = 0; i < master_size; i++) {
master[i] /= sum;
}
master[master_size - 1] = 1;
nounroll for (unsigned phase = 0; phase < GB_BAND_LIMITED_PHASES; phase++) {
nounroll for (signed phase = 0; phase < GB_BAND_LIMITED_PHASES; phase++) {
int32_t error = GB_BAND_LIMITED_ONE;
int32_t prev = 0;
nounroll for (unsigned i = 0; i < GB_BAND_LIMITED_WIDTH; i++) {
int32_t cur = master[(GB_BAND_LIMITED_PHASES - 1 - phase) + i * GB_BAND_LIMITED_PHASES] * GB_BAND_LIMITED_ONE;
int32_t delta = cur - prev;
error = error - delta;
prev = cur;
band_limited_steps[phase][i] = delta;
nounroll for (signed i = 0; i < GB_BAND_LIMITED_WIDTH; i++) {
double sum = 0;
nounroll for (signed j = 0; j < GB_BAND_LIMITED_PHASES; j++) {
signed index = i * GB_BAND_LIMITED_PHASES - phase + j;
if (index >= 0) {
sum += master[index];
}
}
int32_t cur = sum * GB_BAND_LIMITED_ONE;
error -= cur;
band_limited_steps[phase][i] = cur;
}
// Make sure the deltas sum to 1.0
band_limited_steps[phase][GB_BAND_LIMITED_WIDTH / 2 - 1] += error / 2;
band_limited_steps[phase][0] += error - (error / 2);
band_limited_steps[phase][GB_BAND_LIMITED_WIDTH / 2] += error;
}
free(master);
}
@ -56,7 +61,9 @@ static void band_limited_update(GB_band_limited_t *band_limited, const GB_sample
unsigned delay = phase / GB_BAND_LIMITED_PHASES;
phase = phase & (GB_BAND_LIMITED_PHASES - 1);
GB_sample_t delta = {
struct {
signed left, right;
} delta = {
.left = input->left - band_limited->input.left,
.right = input->right - band_limited->input.right,
};
@ -73,7 +80,9 @@ static void band_limited_update_unfiltered(GB_band_limited_t *band_limited, cons
{
if (input->packed == band_limited->input.packed) return;
GB_sample_t delta = {
struct {
signed left, right;
} delta = {
.left = input->left - band_limited->input.left,
.right = input->right - band_limited->input.right,
};
@ -94,6 +103,39 @@ static void band_limited_read(GB_band_limited_t *band_limited, GB_sample_t *outp
output->left = band_limited->output.left * multiplier / GB_BAND_LIMITED_ONE;
output->right = band_limited->output.right * multiplier / GB_BAND_LIMITED_ONE;
/* This hueristic will mute the channel if it's only playing an amplitude of 1 or 2 units, usually
caused by rounding errors when the channel is playing a single frequency above Nyquist. */
unsigned diff = abs(output->left - band_limited->last_output.left);
if (diff > 4) {
band_limited->silence_detection = 0;
band_limited->last_output.packed = output->packed;
return;
}
diff = abs(output->right - band_limited->last_output.right);
if (diff > 4) {
band_limited->silence_detection = 0;
band_limited->last_output.packed = output->packed;
return;
}
if (band_limited->silence_detection == 4000) {
output->packed = band_limited->last_output.packed;
}
else {
band_limited->silence_detection++;
}
}
static inline uint32_t sample_fraction_multiply(GB_gameboy_t *gb, unsigned multiplier)
{
if (unlikely(multiplier == 0)) return 0;
if (likely(multiplier < GB_QUICK_MULTIPLY_COUNT + 1)) {
return gb->apu_output.quick_fraction_multiply_cache[multiplier - 1];
}
return gb->apu_output.quick_fraction_multiply_cache[0] * multiplier;
}
static const uint8_t duties[] = {
@ -164,7 +206,7 @@ static void update_sample(GB_gameboy_t *gb, GB_channel_t index, int8_t value, un
if (index == GB_WAVE) {
/* For some reason, channel 3 is inverted on the AGB, and has a different "silence" value */
value ^= 0xF;
silence = 7;
silence = 7 * 2;
}
uint8_t bias = agb_bias_for_channel(gb, index);
@ -173,8 +215,8 @@ static void update_sample(GB_gameboy_t *gb, GB_channel_t index, int8_t value, un
bool right = gb->io_registers[GB_IO_NR51] & (1 << index);
GB_sample_t output = {
.left = (0xF - (left? value : silence) * 2 + bias) * left_volume,
.right = (0xF - (right? value : silence) * 2 + bias) * right_volume
.left = (0xF - (left? value * 2 + bias : silence)) * left_volume,
.right = (0xF - (right? value * 2 + bias : silence)) * right_volume
};
if (unlikely(gb->apu_output.channel_muted[index])) {
@ -187,7 +229,7 @@ static void update_sample(GB_gameboy_t *gb, GB_channel_t index, int8_t value, un
else {
band_limited_update(&gb->apu_output.band_limited[index],
&output,
(gb->apu_output.cycles_since_render + cycles_offset) * GB_BAND_LIMITED_PHASES / gb->apu_output.max_cycles_per_sample);
(((gb->apu_output.sample_fraction + sample_fraction_multiply(gb, cycles_offset)) >> 8) * GB_BAND_LIMITED_PHASES) >> 20);
}
}
@ -222,7 +264,7 @@ static void update_sample(GB_gameboy_t *gb, GB_channel_t index, int8_t value, un
else {
band_limited_update(&gb->apu_output.band_limited[index],
&output,
(gb->apu_output.cycles_since_render + cycles_offset) * GB_BAND_LIMITED_PHASES / gb->apu_output.max_cycles_per_sample);
(((gb->apu_output.sample_fraction + sample_fraction_multiply(gb, cycles_offset)) >> 8) * GB_BAND_LIMITED_PHASES) >> 20);
}
}
}
@ -307,6 +349,12 @@ static void render(GB_gameboy_t *gb)
output.right += channel_output.right;
}
gb->apu_output.cycles_since_render = 0;
if (unlikely(gb->apu_output.sample_fraction < (1 << 28))) {
gb->apu_output.sample_fraction = 0;
}
else {
gb->apu_output.sample_fraction -= 1 << 28;
}
if (gb->sgb && gb->sgb->intro_animation < GB_SGB_INTRO_ANIMATION_LENGTH) return;
@ -964,6 +1012,8 @@ restart:;
if (gb->apu_output.sample_rate) {
gb->apu_output.cycles_since_render += cycles;
gb->apu_output.sample_fraction += sample_fraction_multiply(gb, cycles);
assert(gb->apu_output.sample_fraction < (4 << 28));
if (gb->apu_output.sample_cycles >= clock_rate) {
gb->apu_output.sample_cycles -= clock_rate;
@ -1760,6 +1810,10 @@ void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate)
if (sample_rate) {
gb->apu_output.highpass_rate = pow(0.999958, GB_get_clock_rate(gb) / (double)sample_rate);
gb->apu_output.max_cycles_per_sample = ceil(GB_get_clock_rate(gb) / 2.0 / sample_rate);
gb->apu_output.quick_fraction_multiply_cache[0] = round(sample_rate * 2.0 / GB_get_clock_rate(gb) * (1 << 28));
for (unsigned i = 1; i < GB_QUICK_MULTIPLY_COUNT; i++) {
gb->apu_output.quick_fraction_multiply_cache[i] = gb->apu_output.quick_fraction_multiply_cache[0] * (i + 1);
}
}
else {
gb->apu_output.max_cycles_per_sample = 0x400;
@ -1776,6 +1830,11 @@ void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample)
gb->apu_output.sample_rate = GB_get_clock_rate(gb) / cycles_per_sample * 2;
gb->apu_output.highpass_rate = pow(0.999958, cycles_per_sample);
gb->apu_output.max_cycles_per_sample = ceil(cycles_per_sample / 4);
gb->apu_output.quick_fraction_multiply_cache[0] = round(gb->apu_output.sample_rate * 2.0 / GB_get_clock_rate(gb) * (1 << 28));
for (unsigned i = 1; i < GB_QUICK_MULTIPLY_COUNT; i++) {
gb->apu_output.quick_fraction_multiply_cache[i] = gb->apu_output.quick_fraction_multiply_cache[0] * (i + 1);
}
}
unsigned GB_get_sample_rate(GB_gameboy_t *gb)

View File

@ -5,8 +5,10 @@
#include <stdio.h>
#include "defs.h"
#define GB_BAND_LIMITED_WIDTH 16
#define GB_BAND_LIMITED_PHASES 512
#define GB_BAND_LIMITED_WIDTH 64
#define GB_BAND_LIMITED_PHASES 256
#define GB_QUICK_MULTIPLY_COUNT 64
#ifdef GB_INTERNAL
#define GB_BAND_LIMITED_ONE 0x10000 // fixed point value equal to 1
@ -171,6 +173,8 @@ typedef struct {
} buffer[GB_BAND_LIMITED_WIDTH * 2], output;
uint8_t pos;
GB_sample_t input;
GB_sample_t last_output;
unsigned silence_detection;
} GB_band_limited_t;
typedef struct {
@ -180,6 +184,9 @@ typedef struct {
unsigned max_cycles_per_sample;
uint32_t cycles_since_render;
uint32_t sample_fraction; // Counter in 1 / sample_rate, in 4.28 fixed format
uint32_t quick_fraction_multiply_cache[GB_QUICK_MULTIPLY_COUNT];
GB_band_limited_t band_limited[GB_N_CHANNELS];
double dac_discharge[GB_N_CHANNELS];
bool channel_muted[GB_N_CHANNELS];

View File

@ -73,7 +73,7 @@ bool GB_cheat_search_filter(GB_gameboy_t *gb, const char *expression, GB_cheat_s
}
skip:;
old_data++;
if (new_data == gb->ram + gb->ram_size - 1) {
if (new_data == gb->ram + gb->ram_size - 1 && gb->mbc_ram_size) {
new_data = gb->mbc_ram;
}
else if (new_data == gb->mbc_ram + gb->mbc_ram_size - 1) {

View File

@ -155,15 +155,19 @@ const GB_cheat_t *GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const cha
uint8_t dummy;
/* GameShark */
if (strlen(cheat) == 8) {
#ifdef _WIN32
// The hh modifier is not supported on old MSVCRT, it's completely ignored
uint32_t bank = 0;
uint32_t value = 0;
#pragma GCC diagnostic ignored "-Wformat"
#else
uint8_t bank;
uint8_t value;
#endif
uint16_t address;
if (sscanf(cheat, "%02hhx%02hhx%04hx%c", &bank, &value, &address, &dummy) == 3) {
if (bank >= 0x80) {
bank &= 0xF;
}
address = __builtin_bswap16(address);
return GB_add_cheat(gb, description, address, bank, value, 0, false, enabled);
return GB_add_cheat(gb, description, address, bank == 1? GB_CHEAT_ANY_BANK : (bank & 0xF), value, 0, false, enabled);
}
}
@ -181,8 +185,13 @@ const GB_cheat_t *GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const cha
stripped_cheat[7] = stripped_cheat[8];
stripped_cheat[8] = 0;
#ifdef _WIN32
uint32_t old_value = 0;
uint32_t value = 0;
#else
uint8_t old_value;
uint8_t value;
#endif
uint16_t address;
if (strlen(stripped_cheat) != 8 && strlen(stripped_cheat) != 6) {
return NULL;

View File

@ -128,7 +128,7 @@ static inline void switch_banking_state(GB_gameboy_t *gb, uint16_t bank)
}
}
static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer_name, bool prefer_local)
static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer_name, bool prefer_local, bool prefer_no_padding)
{
static __thread char output[256];
const GB_bank_symbol_t *symbol = GB_debugger_find_symbol(gb, value, prefer_local);
@ -138,9 +138,13 @@ static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer
}
if (!symbol) {
snprintf(output, sizeof(output), "$%04x", value);
if (prefer_no_padding) {
snprintf(output, sizeof(output), "$%x", value);
}
else {
snprintf(output, sizeof(output), "$%04x", value);
}
}
else if (symbol->addr == value) {
if (prefer_name) {
snprintf(output, sizeof(output), "%s ($%04x)", symbol->name, value);
@ -171,7 +175,7 @@ static GB_symbol_map_t *get_symbol_map(GB_gameboy_t *gb, uint16_t bank)
static const char *debugger_value_to_string(GB_gameboy_t *gb, value_t value, bool prefer_name, bool prefer_local)
{
if (!value.has_bank) return value_to_string(gb, value.value, prefer_name, prefer_local);
if (!value.has_bank) return value_to_string(gb, value.value, prefer_name, prefer_local, false);
static __thread char output[256];
const GB_bank_symbol_t *symbol = GB_map_find_symbol(get_symbol_map(gb, value.bank), value.value, prefer_local);
@ -901,11 +905,11 @@ static bool registers(GB_gameboy_t *gb, char *arguments, char *modifiers, const
(gb->f & GB_HALF_CARRY_FLAG)? 'H' : '-',
(gb->f & GB_SUBTRACT_FLAG)? 'N' : '-',
(gb->f & GB_ZERO_FLAG)? 'Z' : '-');
GB_log(gb, "BC = %s\n", value_to_string(gb, gb->bc, false, false));
GB_log(gb, "DE = %s\n", value_to_string(gb, gb->de, false, false));
GB_log(gb, "HL = %s\n", value_to_string(gb, gb->hl, false, false));
GB_log(gb, "SP = %s\n", value_to_string(gb, gb->sp, false, false));
GB_log(gb, "PC = %s\n", value_to_string(gb, gb->pc, false, false));
GB_log(gb, "BC = %s\n", value_to_string(gb, gb->bc, false, false, false));
GB_log(gb, "DE = %s\n", value_to_string(gb, gb->de, false, false, false));
GB_log(gb, "HL = %s\n", value_to_string(gb, gb->hl, false, false, false));
GB_log(gb, "SP = %s\n", value_to_string(gb, gb->sp, false, false, false));
GB_log(gb, "PC = %s\n", value_to_string(gb, gb->pc, false, false, false));
GB_log(gb, "IME = %s\n", gb->ime? "Enabled" : "Disabled");
return true;
}
@ -1462,10 +1466,10 @@ static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu
return true;
}
if (!modifiers || !modifiers[0]) {
modifiers = "a";
if (!modifiers) {
modifiers = "";
}
else if (modifiers[1]) {
else if (modifiers[0] && modifiers[1]) {
print_usage(gb, command);
return true;
}
@ -1474,6 +1478,12 @@ static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu
value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL);
if (!error) {
switch (modifiers[0]) {
case '\0':
if (!result.has_bank) {
GB_log(gb, "=%s\n", value_to_string(gb, result.value, false, false, true));
break;
}
// fallthrough
case 'a':
GB_log(gb, "=%s\n", debugger_value_to_string(gb, result, false, false));
break;
@ -1503,6 +1513,7 @@ static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu
break;
}
default:
print_usage(gb, command);
break;
}
}
@ -1728,6 +1739,49 @@ static bool ticks(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu
return true;
}
double GB_debugger_get_frame_cpu_usage(GB_gameboy_t *gb)
{
if (gb->last_frame_busy_cycles || gb->last_frame_idle_cycles) {
return (double)gb->last_frame_busy_cycles / (gb->last_frame_busy_cycles + gb->last_frame_idle_cycles);
}
return 0;
}
double GB_debugger_get_second_cpu_usage(GB_gameboy_t *gb)
{
if (gb->last_second_busy_cycles || gb->last_second_idle_cycles) {
return (double)gb->last_second_busy_cycles / (gb->last_second_busy_cycles + gb->last_second_idle_cycles);
}
return 0;
}
double GB_debugger_get_second_cpu_usage(GB_gameboy_t *gb);
static bool usage(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
{
NO_MODIFIERS
if (strlen(lstrip(arguments))) {
print_usage(gb, command);
return true;
}
if (gb->last_frame_busy_cycles || gb->last_frame_idle_cycles) {
GB_log(gb, "CPU usage (last frame): %.2f%%\n", (double)gb->last_frame_busy_cycles / (gb->last_frame_busy_cycles + gb->last_frame_idle_cycles) * 100);
}
else {
GB_log(gb, "CPU usage (last frame): N/A\n");
}
if (gb->last_second_busy_cycles || gb->last_second_idle_cycles) {
GB_log(gb, "CPU usage (last 60 frames): %.2f%%\n", (double)gb->last_second_busy_cycles / (gb->last_second_busy_cycles + gb->last_second_idle_cycles) * 100);
}
else {
GB_log(gb, "CPU usage (last 60 frames): N/A\n");
}
return true;
}
static bool palettes(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
{
@ -2158,7 +2212,7 @@ static const debugger_command_t commands[] = {
{"backtrace", 2, backtrace, "Display the current call stack"},
{"bt", 2, }, /* Alias */
{"print", 1, print, "Evaluate and print an expression. "
"Use modifier to format as an address (a, default) or as a number in "
"Use modifier to format as an address (a) or as a number in "
"decimal (d), hexadecimal (x), octal (o) or binary (b).",
"<expression>", "format", .argument_completer = symbol_completer, .modifiers_completer = format_completer},
{"eval", 2, }, /* Alias */
@ -2184,6 +2238,7 @@ static const debugger_command_t commands[] = {
{"ticks", 2, ticks, "Display the number of CPU ticks since the last time 'ticks' was "
"used. Use 'keep' as an argument to display ticks without reseeting "
"the count.", "(keep)", .argument_completer = keep_completer},
{"usage", 2, usage, "Display CPU usage"},
{"cartridge", 2, mbc, "Display information about the MBC and cartridge"},
{"mbc", 3, }, /* Alias */
{"apu", 3, apu, "Display information about the current state of the audio processing unit",
@ -2330,10 +2385,10 @@ static void test_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t flags, uint
condition_ok:
GB_debugger_break(gb);
if (flags == WATCHPOINT_READ) {
GB_log(gb, "Watchpoint %u: [%s]\n", watchpoint->id, value_to_string(gb, addr, true, false));
GB_log(gb, "Watchpoint %u: [%s]\n", watchpoint->id, value_to_string(gb, addr, true, false, false));
}
else {
GB_log(gb, "Watchpoint %u: [%s] = $%02x\n", watchpoint->id, value_to_string(gb, addr, true, false), value);
GB_log(gb, "Watchpoint %u: [%s] = $%02x\n", watchpoint->id, value_to_string(gb, addr, true, false, false), value);
}
return;
}
@ -2533,7 +2588,7 @@ next_command:
unsigned breakpoint_id = 0;
if (gb->breakpoints && !gb->debug_stopped && (breakpoint_id = should_break(gb, gb->pc, false))) {
GB_debugger_break(gb);
GB_log(gb, "Breakpoint %u: PC = %s\n", breakpoint_id, value_to_string(gb, gb->pc, true, false));
GB_log(gb, "Breakpoint %u: PC = %s\n", breakpoint_id, value_to_string(gb, gb->pc, true, false, false));
GB_cpu_disassemble(gb, gb->pc, 5);
}
@ -2544,7 +2599,7 @@ next_command:
bool should_delete_state = true;
if (jump_to_result == JUMP_TO_BREAK) {
GB_debugger_break(gb);
GB_log(gb, "Jumping to breakpoint %u: %s\n", breakpoint_id, value_to_string(gb, address, true, false));
GB_log(gb, "Jumping to breakpoint %u: %s\n", breakpoint_id, value_to_string(gb, address, true, false, false));
GB_cpu_disassemble(gb, gb->pc, 5);
gb->non_trivial_jump_breakpoint_occured = false;
}
@ -2554,7 +2609,7 @@ next_command:
}
else {
gb->non_trivial_jump_breakpoint_occured = true;
GB_log(gb, "Jumping to breakpoint %u: %s\n", breakpoint_id, value_to_string(gb, gb->pc, true, false));
GB_log(gb, "Jumping to breakpoint %u: %s\n", breakpoint_id, value_to_string(gb, gb->pc, true, false, false));
GB_load_state_from_buffer(gb, gb->nontrivial_jump_state, -1);
GB_rewind_push(gb);
GB_cpu_disassemble(gb, gb->pc, 5);
@ -2720,6 +2775,9 @@ const char *GB_debugger_describe_address(GB_gameboy_t *gb,
if (bank == (uint16_t)-1) {
bank = bank_for_addr(gb, addr);
}
if ((addr >> 12) == 0xC) {
bank = 0;
}
if (exact_match) {
const GB_bank_symbol_t *symbol = GB_map_find_symbol(get_symbol_map(gb, bank), addr, prefer_local);
if (symbol && symbol->addr == addr) return symbol->name;

View File

@ -25,6 +25,9 @@ void GB_debugger_set_disabled(GB_gameboy_t *gb, bool disabled);
void GB_debugger_clear_symbols(GB_gameboy_t *gb);
void GB_debugger_set_reload_callback(GB_gameboy_t *gb, GB_debugger_reload_callback_t callback);
double GB_debugger_get_frame_cpu_usage(GB_gameboy_t *gb);
double GB_debugger_get_second_cpu_usage(GB_gameboy_t *gb);
#ifdef GB_INTERNAL
internal void GB_debugger_run(GB_gameboy_t *gb);
internal void GB_debugger_handle_async_commands(GB_gameboy_t *gb);

View File

@ -4,13 +4,18 @@
#define GB_unlikely(x) __builtin_expect((bool)(x), 0)
#define GB_inline_const(type, ...) (*({static const typeof(type) _= __VA_ARGS__; &_;}))
#if !defined(typeof)
#if defined(__cplusplus) || __STDC_VERSION__ < 202311
#define typeof __typeof__
#endif
#endif
#ifdef GB_INTERNAL
// "Keyword" definitions
#define likely(x) GB_likely(x)
#define unlikely(x) GB_unlikely(x)
#define inline_const GB_inline_const
#define typeof __typeof__
#if !defined(MIN)
#define MIN(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })

View File

@ -176,13 +176,34 @@ void GB_display_vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
gb->cycles_since_vblank_callback = 0;
gb->lcd_disabled_outside_of_vblank = false;
#ifndef GB_DISABLE_DEBUGGER
gb->last_frame_idle_cycles = gb->current_frame_idle_cycles;
gb->last_frame_busy_cycles = gb->current_frame_busy_cycles;
gb->current_frame_idle_cycles = 0;
gb->current_frame_busy_cycles = 0;
if (gb->usage_frame_count++ == 60) {
gb->last_second_idle_cycles = gb->current_second_idle_cycles;
gb->last_second_busy_cycles = gb->current_second_busy_cycles;
gb->current_second_idle_cycles = 0;
gb->current_second_busy_cycles = 0;
gb->usage_frame_count = 0;
}
#endif
/* TODO: Slow in turbo mode! */
if (GB_is_hle_sgb(gb)) {
GB_sgb_render(gb);
}
if (gb->turbo) {
#ifndef GB_DISABLE_DEBUGGER
if (unlikely(gb->backstep_instructions)) return;
#endif
if (GB_timing_sync_turbo(gb)) {
if (gb->vblank_callback && gb->enable_skipped_frame_vblank_callbacks) {
gb->vblank_callback(gb, GB_VBLANK_TYPE_SKIPPED_FRAME);
}
return;
}
}
@ -2470,3 +2491,8 @@ double GB_get_usual_frame_rate(GB_gameboy_t *gb)
{
return GB_get_clock_rate(gb) / (double)LCDC_PERIOD;
}
void GB_set_enable_skipped_frame_vblank_callbacks(GB_gameboy_t *gb, bool enable)
{
gb->enable_skipped_frame_vblank_callbacks = enable;
}

View File

@ -20,6 +20,7 @@ typedef enum {
GB_VBLANK_TYPE_LCD_OFF, // An artificial frame pushed while the LCD was off
GB_VBLANK_TYPE_ARTIFICIAL, // An artificial frame pushed for some other reason
GB_VBLANK_TYPE_REPEAT, // A frame that would not render on actual hardware, but the screen should retain the previous frame
GB_VBLANK_TYPE_SKIPPED_FRAME, // If enabled via GB_set_enable_skipped_frame_vblank_callbacks, called on skipped frames during turbo mode
} GB_vblank_type_t;
typedef void (*GB_vblank_callback_t)(GB_gameboy_t *gb, GB_vblank_type_t type);
@ -95,6 +96,7 @@ static const GB_color_correction_mode_t __attribute__((deprecated("Use GB_COLOR_
static const GB_color_correction_mode_t __attribute__((deprecated("Use GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST instead"))) GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS = GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST;
void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback);
void GB_set_enable_skipped_frame_vblank_callbacks(GB_gameboy_t *gb, bool enable);
void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback);
void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette);
const GB_palette_t *GB_get_palette(GB_gameboy_t *gb);

View File

@ -1222,8 +1222,10 @@ uint64_t GB_run_frame(GB_gameboy_t *gb)
/* Configure turbo temporarily, the user wants to handle FPS capping manually. */
bool old_turbo = gb->turbo;
bool old_dont_skip = gb->turbo_dont_skip;
double old_turbo_cap = gb->turbo_cap_multiplier;
gb->turbo = true;
gb->turbo_dont_skip = true;
gb->turbo_cap_multiplier = 0;
gb->cycles_since_last_sync = 0;
while (true) {
@ -1234,6 +1236,7 @@ uint64_t GB_run_frame(GB_gameboy_t *gb)
}
gb->turbo = old_turbo;
gb->turbo_dont_skip = old_dont_skip;
gb->turbo_cap_multiplier = old_turbo_cap;
return gb->cycles_since_last_sync * 1000000000LL / 2 / GB_get_clock_rate(gb); /* / 2 because we use 8MHz units */
}
@ -1418,6 +1421,11 @@ void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip)
gb->turbo_dont_skip = no_frame_skip;
}
void GB_set_turbo_cap(GB_gameboy_t *gb, double multiplier)
{
gb->turbo_cap_multiplier = multiplier;
}
void GB_set_rendering_disabled(GB_gameboy_t *gb, bool disabled)
{
gb->disable_rendering = disabled;

View File

@ -697,6 +697,7 @@ struct GB_gameboy_internal_s {
/* Timing */
uint64_t last_sync;
uint64_t last_render;
uint64_t cycles_since_last_sync; // In 8MHz units
GB_rtc_mode_t rtc_mode;
uint32_t rtc_second_length;
@ -782,6 +783,14 @@ struct GB_gameboy_internal_s {
/* Callbacks */
GB_debugger_reload_callback_t debugger_reload_callback;
/* CPU usage */
uint32_t current_frame_idle_cycles, current_frame_busy_cycles;
uint32_t last_frame_idle_cycles, last_frame_busy_cycles;
uint32_t current_second_idle_cycles, current_second_busy_cycles;
uint32_t last_second_idle_cycles, last_second_busy_cycles;
uint8_t usage_frame_count;
#endif
#ifndef GB_DISABLE_REWIND
@ -822,6 +831,8 @@ struct GB_gameboy_internal_s {
/* Misc */
bool turbo;
bool turbo_dont_skip;
double turbo_cap_multiplier;
bool enable_skipped_frame_vblank_callbacks;
bool disable_rendering;
uint8_t boot_rom[0x900];
bool vblank_just_occured; // For slow operations involving syscalls; these should only run once per vblank
@ -941,6 +952,7 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t
int GB_load_battery(GB_gameboy_t *gb, const char *path);
void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip);
void GB_set_turbo_cap(GB_gameboy_t *gb, double multiplier); // Use 0 to use no cap
void GB_set_rendering_disabled(GB_gameboy_t *gb, bool disabled);
void GB_log(GB_gameboy_t *gb, const char *fmt, ...) __printflike(2, 3);

View File

@ -551,6 +551,7 @@ static const uint8_t *get_header_bank(GB_gameboy_t *gb)
static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool append_bess)
{
errno = 0;
if (file->write(file, GB_GET_SECTION(gb, header), GB_SECTION_SIZE(header)) != GB_SECTION_SIZE(header)) goto error;
if (!DUMP_SECTION(gb, file, core_state)) goto error;
if (!DUMP_SECTION(gb, file, dma )) goto error;
@ -838,8 +839,10 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe
goto error;
}
return 0;;
error:
return 0;
if (errno == 0) return EIO;
return errno;
}
int GB_save_state(GB_gameboy_t *gb, const char *path)

View File

@ -47,10 +47,10 @@ bool GB_timing_sync_turbo(GB_gameboy_t *gb)
#endif
if (!gb->turbo_dont_skip) {
int64_t nanoseconds = get_nanoseconds();
if (nanoseconds <= gb->last_sync + (1000000000LL * LCDC_PERIOD / GB_get_clock_rate(gb))) {
if (nanoseconds <= gb->last_render + 1000000000 / 60) {
return true;
}
gb->last_sync = nanoseconds;
gb->last_render = nanoseconds;
}
return false;
}
@ -63,23 +63,32 @@ void GB_timing_sync(GB_gameboy_t *gb)
/* Prevent syncing if not enough time has passed.*/
if (gb->cycles_since_last_sync < LCDC_PERIOD / 3) return;
unsigned target_clock_rate;
if (gb->turbo) {
gb->cycles_since_last_sync = 0;
if (gb->update_input_hint_callback) {
gb->update_input_hint_callback(gb);
if (gb->turbo_cap_multiplier) {
target_clock_rate = GB_get_clock_rate(gb) * gb->turbo_cap_multiplier;
}
return;
else {
gb->cycles_since_last_sync = 0;
if (gb->update_input_hint_callback) {
gb->update_input_hint_callback(gb);
}
return;
}
}
else {
target_clock_rate = GB_get_clock_rate(gb);
}
uint64_t target_nanoseconds = gb->cycles_since_last_sync * 1000000000LL / 2 / GB_get_clock_rate(gb); /* / 2 because we use 8MHz units */
uint64_t target_nanoseconds = gb->cycles_since_last_sync * 1000000000LL / 2 / target_clock_rate; /* / 2 because we use 8MHz units */
int64_t nanoseconds = get_nanoseconds();
int64_t time_to_sleep = target_nanoseconds + gb->last_sync - nanoseconds;
if (time_to_sleep > 0 && time_to_sleep < LCDC_PERIOD * 1200000000LL / GB_get_clock_rate(gb)) { // +20% to be more forgiving
if (time_to_sleep > 0 && time_to_sleep < LCDC_PERIOD * 1200000000LL / target_clock_rate) { // +20% to be more forgiving
nsleep(time_to_sleep);
gb->last_sync += target_nanoseconds;
}
else {
if (time_to_sleep < 0 && -time_to_sleep < LCDC_PERIOD * 1200000000LL / GB_get_clock_rate(gb)) {
if (time_to_sleep < 0 && -time_to_sleep < LCDC_PERIOD * 1200000000LL / target_clock_rate) {
// We're running a bit too slow, but the difference is small enough,
// just skip this sync and let it even out
return;
@ -472,6 +481,8 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles)
#ifndef GB_DISABLE_DEBUGGER
gb->absolute_debugger_ticks += cycles;
*((gb->halted || gb->stopped)? &gb->current_frame_idle_cycles : &gb->current_frame_busy_cycles) += cycles;
*((gb->halted || gb->stopped)? &gb->current_second_idle_cycles : &gb->current_second_busy_cycles) += cycles;
#endif
// Not affected by speed boost

View File

@ -473,7 +473,7 @@ static inline Class preferredByteArrayClass(void) {
}
- (void)_setSingleSelectedContentsRange:(HFRange)newSelection {
HFASSERT(HFRangeIsSubrangeOfRange(newSelection, HFRangeMake(0, [self contentsLength])));
if (!HFRangeIsSubrangeOfRange(newSelection, HFRangeMake(0, [self contentsLength]))) return;
BOOL selectionChanged;
if ([selectedContentsRanges count] == 1) {
selectionChanged = ! HFRangeEqualsRange([selectedContentsRanges[0] HFRange], newSelection);

View File

@ -36,10 +36,14 @@ else
DEFAULT := sdl
endif
NULL := /dev/null
ifeq ($(PLATFORM),windows32)
ifneq ($(shell echo /dev/null*),/dev/null)
# Windows shell is not "aware" of /dev/null, use NUL and pray
NULL := NUL
endif
endif
PREFIX ?= /usr/local
ifneq ($(shell which xdg-open 2> $(NULL))$(FREEDESKTOP),)
@ -96,15 +100,8 @@ else
CPPP_FLAGS += -UGB_DISABLE_CHEAT_SEARCH
endif
ifneq ($(CORE_FILTER)$(DISABLE_TIMEKEEPING),)
ifneq ($(MAKECMDGOALS),lib)
$(error SameBoy features can only be disabled when compiling the 'lib' target)
endif
endif
CPPP_FLAGS += -UGB_INTERNAL
include version.mk
COPYRIGHT_YEAR := $(shell grep -oE "20[2-9][0-9]" LICENSE)
export VERSION
@ -117,6 +114,12 @@ LIBDIR := build/lib
PKGCONF_DIR := $(LIBDIR)/pkgconfig
PKGCONF_FILE := $(PKGCONF_DIR)/sameboy.pc
ifneq ($(CORE_FILTER)$(DISABLE_TIMEKEEPING),)
ifneq ($(filter-out lib headers $(LIBDIR)/% $(INC)/%,$(MAKECMDGOALS)),)
$(error SameBoy features can only be disabled when compiling the 'lib' target)
endif
endif
BOOTROMS_DIR ?= $(BIN)/BootROMs
ifdef DATA_DIR
@ -132,6 +135,8 @@ CC := clang
endif
endif
IBTOOL ?= ibtool
# Find libraries with pkg-config if available.
ifneq (, $(shell which pkg-config 2> $(NULL)))
# But not on macOS, it's annoying, and not on Haiku, where OpenGL is broken
@ -210,9 +215,10 @@ CFLAGS += -DUPDATE_SUPPORT
endif
ifeq (,$(PKG_CONFIG))
ifneq ($(PLATFORM),windows32)
SDL_CFLAGS := $(shell sdl2-config --cflags)
SDL_LDFLAGS := $(shell sdl2-config --libs) -lpthread
endif
ifeq ($(PLATFORM),Darwin)
SDL_LDFLAGS += -framework AppKit
endif
@ -282,11 +288,13 @@ sdl: $(BIN)/SDL/xaudio2_9redist.dll
endif
else
LDFLAGS += -lc -lm
# libdl is not available as a standalone library in Haiku
# libdl is not available as a standalone library in Haiku or OpenBSD
ifneq ($(PLATFORM),Haiku)
ifneq ($(PLATFORM),OpenBSD)
LDFLAGS += -ldl
endif
endif
endif
ifeq ($(MAKECMDGOALS),_ios)
OBJ := build/obj-ios
@ -429,7 +437,8 @@ SDL_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(SDL_SOURCES))
TESTER_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(TESTER_SOURCES))
XDG_THUMBNAILER_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(XDG_THUMBNAILER_SOURCES)) $(OBJ)/XdgThumbnailer/resources.c.o
lib: $(PUBLIC_HEADERS)
lib: headers
headers: $(PUBLIC_HEADERS)
# Automatic dependency generation
@ -541,9 +550,10 @@ $(OBJ)/installer: iOS/installer.m
# Cocoa Port
$(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \
$(shell ls Cocoa/*.icns Cocoa/*.png) \
$(shell ls Cocoa/*.icns Cocoa/*.png Cocoa/*.car) \
Cocoa/License.html \
Cocoa/Info.plist \
Cocoa/SameBoy.entitlements \
Misc/registers.sym \
$(BIN)/SameBoy.app/Contents/Resources/dmg_boot.bin \
$(BIN)/SameBoy.app/Contents/Resources/mgb_boot.bin \
@ -558,15 +568,13 @@ $(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \
$(BIN)/SameBoy.app/Contents/PlugIns/Previewer.appex \
Shaders
$(MKDIR) -p $(BIN)/SameBoy.app/Contents/Resources
cp Cocoa/*.icns Cocoa/*.png Misc/registers.sym $(BIN)/SameBoy.app/Contents/Resources/
cp Cocoa/*.icns Cocoa/*.png Cocoa/*.car Misc/registers.sym $(BIN)/SameBoy.app/Contents/Resources/
sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < Cocoa/Info.plist > $(BIN)/SameBoy.app/Contents/Info.plist
sed "s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < Cocoa/License.html > $(BIN)/SameBoy.app/Contents/Resources/Credits.html
$(MKDIR) -p $(BIN)/SameBoy.app/Contents/Resources/Shaders
cp Shaders/*.fsh Shaders/*.metal $(BIN)/SameBoy.app/Contents/Resources/Shaders
$(MKDIR) -p $(BIN)/SameBoy.app/Contents/Library/QuickLook/
ifeq ($(CONF), release)
$(CODESIGN) $@
endif
$(CODESIGN) $@ --entitlements Cocoa/SameBoy.entitlements
# We place the dylib inside the Quick Look plugin, because Quick Look plugins run in a very strict sandbox
@ -583,10 +591,10 @@ ifeq ($(CONF), release)
endif
$(BIN)/SameBoy.app/Contents/Resources/%.nib: Cocoa/%.xib
ibtool --target-device mac --minimum-deployment-target 10.9 --compile $@ $^ 2>&1 | cat -
$(IBTOOL) --target-device mac --minimum-deployment-target 10.9 --compile $@ $^ 2>&1 | cat -
$(BIN)/SameBoy-iOS.app/%.storyboardc: iOS/%.storyboard
ibtool --target-device iphone --target-device ipad --minimum-deployment-target $(IOS_MIN) --compile $@ $^ 2>&1 | cat -
$(IBTOOL) --target-device iphone --target-device ipad --minimum-deployment-target $(IOS_MIN) --compile $@ $^ 2>&1 | cat -
# Quick Look generators
@ -666,11 +674,11 @@ $(BIN)/SDL/sameboy.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(OBJ)/Windows/resources.
$(BIN)/SDL/sameboy_debugger.txt:
echo Looking for sameboy_debugger.exe? > $@
echo\>> $@
echo >> $@
echo Starting with SameBoy v1.0.1, sameboy.exe and sameboy_debugger.exe >> $@
echo have been merged into a single executable. You can open a debugger >> $@
echo console at any time by pressing Ctrl+C to interrupt the currently >> $@
echo open ROM. Once you're done debugging, you can close the debugger >> $@
echo open ROM. Once you\'re done debugging, you can close the debugger >> $@
echo console and resume normal execution. >> $@
ifneq ($(USE_WINDRES),)
@ -683,13 +691,13 @@ $(OBJ)/%.res: %.rc
rc /fo $@ /dVERSION=\"$(VERSION)\" /dCOPYRIGHT_YEAR=\"$(COPYRIGHT_YEAR)\" $^
%.o: %.res
cvtres /OUT:"$@" $^
cvtres /MACHINE:X64 /OUT:"$@" $^
endif
# Copy required DLL files for the Windows port
$(BIN)/SDL/%.dll:
-@$(MKDIR) -p $(dir $@)
@$(eval MATCH := $(shell where $$LIB:$(notdir $@)))
@$(eval MATCH := $(shell where "$(lib)":$(notdir $@)))
cp "$(MATCH)" $@
# Tester
@ -781,7 +789,7 @@ install: $(BIN)/XdgThumbnailer/sameboy-thumbnailer sdl $(shell find FreeDesktop)
install -d $(DESTDIR)$(DATA_DIR)/BootROMs
install -d $(DESTDIR)$(PREFIX)/bin
install -d $(DESTDIR)$(PREFIX)/share/thumbnailers
install -d $(DESTDIR)$(PREFIX)/share/mime
install -d $(DESTDIR)$(PREFIX)/share/mime/packages
install -d $(DESTDIR)$(PREFIX)/share/applications
(cd $(BIN)/SDL && find . \! -name sameboy -type f -exec install -m 644 {} "$(abspath $(DESTDIR))$(DATA_DIR)/{}" \; )
@ -797,7 +805,7 @@ ifeq ($(DESTDIR),)
xdg-icon-resource install --novendor --theme hicolor --size $$size --context mimetypes FreeDesktop/ColorCartridge/$${size}x$${size}.png x-gameboy-color-rom; \
done
else
install -m 644 FreeDesktop/sameboy.xml $(DESTDIR)$(PREFIX)/share/mime/sameboy.xml
install -m 644 FreeDesktop/sameboy.xml $(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml
install -m 644 FreeDesktop/sameboy.desktop $(DESTDIR)$(PREFIX)/share/applications/sameboy.desktop
for size in 16x16 32x32 64x64 128x128 256x256 512x512; do \
install -d $(DESTDIR)$(PREFIX)/share/icons/hicolor/$$size/apps; \

View File

@ -47,8 +47,9 @@ SameBoy requires the following tools and libraries to build:
On Windows, SameBoy also requires:
* Visual Studio (For headers, etc.)
* [GnuWin](http://gnuwin32.sourceforge.net/)
* Running vcvars64 before running make. Make sure all required tools and libraries are in %PATH% and %lib%, respectively. (see [Build FAQ](https://github.com/LIJI32/SameBoy/blob/master/build-faq.md) for more details on Windows compilation)
* [Git Bash](https://git-scm.com/downloads/win) or another distribution of basic Unix utilities
* Git Bash does not include make, you can get it [here](https://sourceforge.net/projects/ezwinports/files/make-4.4.1-without-guile-w32-bin.zip/download).
* Running `vcvars64.bat` or `vcvarsx86_amd64.bat` before running make. Make sure all required tools, libraries, and headers are in %PATH%, %lib%, and %include%`, respectively. (see [Build FAQ](https://github.com/LIJI32/SameBoy/blob/master/build-faq.md) for more details on Windows compilation)
To compile, simply run `make`. The targets are:
* `cocoa` (Default for macOS)

View File

@ -161,6 +161,10 @@ typedef struct {
/* v1.0.1 */
bool disable_rounded_corners; // Windows only
bool use_faux_analog_inputs;
/* v1.0.2 */
int8_t vsync_mode;
uint8_t turbo_cap;
};
} configuration_t;

View File

@ -1173,6 +1173,45 @@ static const char *current_agb_revision_string(unsigned index)
return "CPU AGB A (AGB)";
}
static void cycle_turbo_cap(unsigned index)
{
if (configuration.turbo_cap >= 16) { // 400%
configuration.turbo_cap = 0; // uncapped
}
else if (configuration.turbo_cap == 0) { // uncapped
configuration.turbo_cap = 6; // 150%
}
else {
configuration.turbo_cap++;
}
}
static void cycle_turbo_cap_backwards(unsigned index)
{
if (configuration.turbo_cap == 0) { // uncapped
configuration.turbo_cap = 16; // 400%
}
else if (configuration.turbo_cap == 6) { // 150%
configuration.turbo_cap = 0; // uncapped
}
else {
configuration.turbo_cap--;
}
}
static const char *current_turbo_cap_string(unsigned index)
{
if (configuration.turbo_cap == 0) {
return "Uncapped";
}
static char ret[5];
snprintf(ret, sizeof(ret), "%d%%", configuration.turbo_cap * 25);
return ret;
}
static const struct menu_item emulation_menu[] = {
{"Emulated Model:", cycle_model, current_model_string, cycle_model_backwards},
{"SGB Revision:", cycle_sgb_revision, current_sgb_revision_string, cycle_sgb_revision_backwards},
@ -1181,6 +1220,7 @@ static const struct menu_item emulation_menu[] = {
{"Boot ROMs Folder:", toggle_bootrom, current_bootrom_string, toggle_bootrom},
{"Rewind Length:", cycle_rewind, current_rewind_string, cycle_rewind_backwards},
{"Real Time Clock:", toggle_rtc_mode, current_rtc_mode_string, toggle_rtc_mode},
{"Turbo speed cap:", cycle_turbo_cap, current_turbo_cap_string, cycle_turbo_cap_backwards},
{"Back", enter_options_menu},
{NULL,}
};
@ -1598,6 +1638,40 @@ static const char *current_osd_mode(unsigned index)
return configuration.osd? "Enabled" : "Disabled";
}
static const char *current_vsync_mode(unsigned index)
{
switch (configuration.vsync_mode) {
default:
case 0: return "Disabled";
case 1: return "Enabled";
case -1: return "Adaptive";
}
}
static void cycle_vsync(unsigned index)
{
retry:
configuration.vsync_mode++;
if (configuration.vsync_mode == 2) {
configuration.vsync_mode = -1;
}
if (SDL_GL_SetSwapInterval(configuration.vsync_mode) && configuration.vsync_mode != 0) {
goto retry;
}
}
static void cycle_vsync_backwards(unsigned index)
{
retry:
configuration.vsync_mode--;
if (configuration.vsync_mode == -2) {
configuration.vsync_mode = 1;
}
if (SDL_GL_SetSwapInterval(configuration.vsync_mode) && configuration.vsync_mode != 0) {
goto retry;
}
}
#ifdef _WIN32
// Don't use the standard header definitions because we might not have the newest headers
@ -1652,6 +1726,7 @@ static const struct menu_item graphics_menu[] = {
{"Mono Palette:", cycle_palette, current_palette, cycle_palette_backwards},
{"Display Border:", cycle_border_mode, current_border_mode, cycle_border_mode_backwards},
{"On-Screen Display:", toggle_osd, current_osd_mode, toggle_osd},
{"Vsync Mode:", cycle_vsync, current_vsync_mode, cycle_vsync_backwards},
#ifdef _WIN32
{"Window Corners:", toggle_corners, current_corner_mode, toggle_corners},
#endif
@ -2316,6 +2391,10 @@ void run_gui(bool is_running)
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_UP:
case SDL_SCANCODE_DOWN:
case SDL_SCANCODE_H:
case SDL_SCANCODE_J:
case SDL_SCANCODE_K:
case SDL_SCANCODE_L:
break;
default:
@ -2703,12 +2782,16 @@ void run_gui(bool is_running)
}
}
else if (gui_state == SHOWING_MENU) {
if (event.key.keysym.scancode == SDL_SCANCODE_DOWN && current_menu[current_selection + 1].string) {
if ((event.key.keysym.scancode == SDL_SCANCODE_DOWN ||
event.key.keysym.scancode == SDL_SCANCODE_J) &&
current_menu[current_selection + 1].string) {
current_selection++;
mouse_scroling = false;
should_render = true;
}
else if (event.key.keysym.scancode == SDL_SCANCODE_UP && current_selection) {
else if ((event.key.keysym.scancode == SDL_SCANCODE_UP ||
event.key.keysym.scancode == SDL_SCANCODE_K) &&
current_selection) {
current_selection--;
mouse_scroling = false;
should_render = true;
@ -2732,11 +2815,15 @@ void run_gui(bool is_running)
return;
}
}
else if (event.key.keysym.scancode == SDL_SCANCODE_RIGHT && current_menu[current_selection].backwards_handler) {
else if ((event.key.keysym.scancode == SDL_SCANCODE_RIGHT ||
event.key.keysym.scancode == SDL_SCANCODE_L) &&
current_menu[current_selection].backwards_handler) {
current_menu[current_selection].handler(current_selection);
should_render = true;
}
else if (event.key.keysym.scancode == SDL_SCANCODE_LEFT && current_menu[current_selection].backwards_handler) {
else if ((event.key.keysym.scancode == SDL_SCANCODE_LEFT ||
event.key.keysym.scancode == SDL_SCANCODE_H) &&
current_menu[current_selection].backwards_handler) {
current_menu[current_selection].backwards_handler(current_selection);
should_render = true;
}

View File

@ -277,6 +277,7 @@ static void open_menu(void)
GB_set_highpass_filter_mode(&gb, configuration.highpass_mode);
GB_set_rewind_length(&gb, configuration.rewind_length);
GB_set_rtc_mode(&gb, configuration.rtc_mode);
GB_set_turbo_cap(&gb, configuration.turbo_cap / 4.0);
if (previous_width != GB_get_screen_width(&gb)) {
signed current_window_width, current_window_height;
SDL_GetWindowSize(window, &current_window_width, &current_window_height);
@ -381,6 +382,7 @@ static void handle_events(GB_gameboy_t *gb)
GB_audio_clear_queue();
turbo_down = event.type == SDL_JOYBUTTONDOWN;
GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down);
SDL_GL_SetSwapInterval(turbo_down? 0 : configuration.vsync_mode);
}
else if (button == JOYPAD_BUTTON_SLOW_MOTION) {
underclock_down = event.type == SDL_JOYBUTTONDOWN;
@ -391,6 +393,7 @@ static void handle_events(GB_gameboy_t *gb)
rewind_paused = false;
}
GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down);
SDL_GL_SetSwapInterval(turbo_down? 0 : configuration.vsync_mode);
}
else if (button == JOYPAD_BUTTON_MENU && event.type == SDL_JOYBUTTONDOWN) {
open_menu();
@ -610,6 +613,7 @@ static void handle_events(GB_gameboy_t *gb)
turbo_down = event.type == SDL_KEYDOWN;
GB_audio_clear_queue();
GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down);
SDL_GL_SetSwapInterval(turbo_down? 0 : configuration.vsync_mode);
}
else if (event.key.keysym.scancode == configuration.keys_2[GB_CONF_KEYS2_REWIND]) {
rewind_down = event.type == SDL_KEYDOWN;
@ -617,6 +621,7 @@ static void handle_events(GB_gameboy_t *gb)
rewind_paused = false;
}
GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down);
SDL_GL_SetSwapInterval(turbo_down? 0 : configuration.vsync_mode);
}
else if (event.key.keysym.scancode == configuration.keys_2[GB_CONF_KEYS2_UNDERCLOCK]) {
underclock_down = event.type == SDL_KEYDOWN;
@ -737,15 +742,23 @@ static void debugger_reset(int ignore)
#endif
static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample)
{
{
if (turbo_down) {
static unsigned skip = 0;
skip++;
if (skip == GB_audio_get_frequency() / 8) {
skip = 0;
}
if (skip > GB_audio_get_frequency() / 16) {
return;
if (configuration.turbo_cap) {
if (skip > GB_audio_get_frequency() / 8 * 4 / configuration.turbo_cap) {
return;
}
}
else {
if (skip > GB_audio_get_frequency() / 16) {
return;
}
}
}
@ -1095,6 +1108,7 @@ restart:;
GB_set_highpass_filter_mode(&gb, configuration.highpass_mode);
GB_set_rewind_length(&gb, configuration.rewind_length);
GB_set_rtc_mode(&gb, configuration.rtc_mode);
GB_set_turbo_cap(&gb, configuration.turbo_cap / 4.0);
GB_set_update_input_hint_callback(&gb, handle_events);
GB_apu_set_sample_callback(&gb, gb_audio_callback);
@ -1382,7 +1396,9 @@ int main(int argc, char **argv)
signal(SIGUSR1, debugger_reset);
#endif
SDL_Init(SDL_INIT_EVERYTHING & ~SDL_INIT_AUDIO);
if (SDL_Init(SDL_INIT_EVERYTHING & ~SDL_INIT_AUDIO) < 0) {
fprintf(stderr, "Couldn't initialize SDL: %s\n", SDL_GetError());
}
// This is, essentially, best-effort.
// This function will not be called if the process is terminated in any way, anyhow.
atexit(SDL_Quit);
@ -1536,6 +1552,8 @@ int main(int argc, char **argv)
}
#endif
SDL_GL_SetSwapInterval(configuration.vsync_mode);
if (filename == NULL) {
stop_on_start = false;
run_gui(false);

View File

@ -26,9 +26,9 @@ The following examples will be referenced later:
After downloading [rgbds](https://github.com/gbdev/rgbds/releases/), ensure that it is added to the `%PATH%`. This may be done by adding it to the user's or SYSTEM's Environment Variables, or may be added to the command line at compilation time via `set path=%path%;C:\path\to\rgbds`.
### GnuWin
### Git Bash & Make
Ensure that the `gnuwin32\bin\` directory is included in `%PATH%`. Like rgbds above, this may instead be manually included on the command line before installation: `set path=%path%;C:\path\to\gnuwin32\bin`.
Ensure that the `Git\usr\bin` directory is included in `%PATH%`. Like rgbds above, this may instead be manually included on the command line before installation: `set path=%path%;C:\path\to\Git\usr\bin`. Similarly, make sure that the directory containing `make.exe` is also included.
## Building
@ -40,18 +40,5 @@ set lib=%lib%;C:\SDL2\lib\x64
set include=%include%;C:\SDL2\include
make
```
Please note that these directories (`C:\SDL2\*`) are the examples given within the "SDL Port" section above. Ensure that your `%PATH%` properly includes `rgbds` and `gnuwin32\bin`, and that the `lib` and `include` paths include the appropriate SDL2 directories.
On some versions of Visual Studio, you might need to use `vcvarsx86_amd64` instead of `vcvars64`. Please note that these directories (`C:\SDL2\*`) are the examples given within the "SDL Port" section above. Ensure that your `%PATH%` properly includes `rgbds` and `Git\usr\bin`, and that the `lib` and `include` paths include the appropriate SDL2 directories.
## Common Errors
### Error -1073741819
If encountering an error that appears as follows:
``` make: *** [build/bin/BootROMs/dmg_boot.bin] Error -1073741819```
Simply run `make` again, and the process will continue. This appears to happen occasionally with `build/bin/BootROMs/dmg_boot.bin` and `build/bin/BootROMs/sgb2_boot.bin`. It does not affect the compiled output. This appears to be an issue with GnuWin.
### The system cannot find the file specified (`usr/bin/mkdir`)
If errors arise (i.e., particularly with the `CREATE_PROCESS('usr/bin/mkdir')` calls, also verify that Git for Windows has not been installed with full Linux support. If it has, remove `C:\Program Files\Git\usr\bin` from the SYSTEM %PATH% until after compilation. This happens because the Git for Windows version of `which` is used instead of the GnuWin one, and it returns a Unix-style path instead of a Windows one.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

BIN
iOS/Cartridge64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

BIN
iOS/ColorCartridge64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -24,7 +24,8 @@
if (section == 0) return 1;
size_t count;
GB_get_cheats(_gb, &count);
self.toolbarItems.firstObject.enabled = count;
self.toolbarItems[0].enabled = count;
((UIButton *)(self.toolbarItems[0].customView.subviews[0])).enabled = count;
return count;
}
@ -91,11 +92,11 @@
}
else {
alertController.title = @"Invalid cheat code entered";
[self presentViewController:alertController animated:YES completion:nil];
[self presentViewController:alertController animated:true completion:nil];
}
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alertController animated:YES completion:nil];
[self presentViewController:alertController animated:true completion:nil];
}
+ (UIBarButtonItem *)buttonWithLabel:(NSString *)label
@ -119,9 +120,17 @@
[button sizeToFit];
CGRect frame = button.frame;
frame.size.width = ceil(frame.size.width + (label? 4 : 0));
if (@available(iOS 19.0, *)) {
if (label) {
frame.size.width += 12;
}
}
frame.size.height = 28;
button.frame = frame;
return [[UIBarButtonItem alloc] initWithCustomView:button];
UIView *wrapper = [[UIView alloc] initWithFrame:button.bounds];
[wrapper addSubview:button];
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:wrapper];
return item;
}
return [[UIBarButtonItem alloc] initWithTitle:label style:UIBarButtonItemStylePlain target:target action:action];
}
@ -153,9 +162,7 @@
UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:@[url]
applicationActivities:nil];
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
controller.popoverPresentationController.barButtonItem = self.toolbarItems.firstObject;
}
controller.popoverPresentationController.barButtonItem = self.toolbarItems.firstObject;
[self presentViewController:controller
animated:true
@ -178,28 +185,38 @@
hasSFSymbols = true;
}
self.toolbarItems = @[
hasSFSymbols?
[self.class buttonWithLabel:nil
imageWithName:@"square.and.arrow.up"
target:self
action:@selector(exportCheats)] :
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
target:self
action:@selector(exportCheats)],
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil
action:NULL],
[self.class buttonWithLabel:@"Import"
imageWithName:@"square.and.arrow.down"
UIBarButtonItem *export = hasSFSymbols?
[self.class buttonWithLabel:nil
imageWithName:@"square.and.arrow.up"
target:self
action:@selector(importCheats)],
[self.class buttonWithLabel:@"Add"
imageWithName:@"plus"
target:self
action:@selector(addCheat)],
];
action:@selector(exportCheats)] :
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
target:self
action:@selector(exportCheats)];
UIBarButtonItem *flexItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil
action:NULL];
UIBarButtonItem *import = [self.class buttonWithLabel:@"Import"
imageWithName:@"square.and.arrow.down"
target:self
action:@selector(importCheats)];
UIBarButtonItem *add = [self.class buttonWithLabel:@"Add"
imageWithName:@"plus"
target:self
action:@selector(addCheat)];
if (@available(iOS 19.0, *)) {
self.toolbarItems = @[export,
flexItem,
import, [UIBarButtonItem fixedSpaceItemOfWidth:0], add];
}
else {
self.toolbarItems = @[export,
flexItem,
import, add];
}
_gb = gb;
return self;

View File

@ -12,6 +12,9 @@ static double StatusBarHeight(void)
UIEdgeInsets insets = window.safeAreaInsets;
ret = MAX(MAX(insets.left, insets.right), MAX(insets.top, insets.bottom)) ?: 20;
[window setHidden:true];
if (!ret && [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
ret = 32; // iPadOS is buggy af
}
}
});
return ret;
@ -47,7 +50,7 @@ static bool HasHomeBar(void)
}
_minY = StatusBarHeight() * _factor;
_cutout = _minY <= 24 * _factor? 0 : _minY;
_cutout = (_minY <= 24 * _factor || [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)? 0 : _minY;
if (HasHomeBar()) {
_homeBar = 21 * _factor;

View File

@ -2,4 +2,6 @@
@interface GBMenuViewController : UIAlertController
+ (instancetype)menu;
@property (nonatomic) NSInteger selectedButtonIndex;
@property (nonatomic, strong) NSArray<UIButton *> *menuButtons;
@end

View File

@ -29,6 +29,7 @@ static NSString *const tips[] = {
{
UILabel *_tipLabel;
UIVisualEffectView *_effectView;
NSMutableArray<UIButton *> *_buttons;
}
+ (instancetype)menu
@ -41,6 +42,8 @@ static NSString *const tips[] = {
[ret addAction:[UIAlertAction actionWithTitle:@"Close"
style:UIAlertActionStyleCancel
handler:nil]];
ret.selectedButtonIndex = -1;
ret->_buttons = [[NSMutableArray alloc] init];
return ret;
}
@ -50,6 +53,7 @@ static NSString *const tips[] = {
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:true];
if (_effectView) return;
static const struct {
NSString *label;
NSString *image;
@ -80,6 +84,7 @@ static NSString *const tips[] = {
button.frame = CGRectMake(round(width * x), height * y, round(width), height);
button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
[self.view addSubview:button];
[_buttons addObject:button];
if (!buttons[i].selector) {
button.enabled = false;
@ -102,10 +107,23 @@ static NSString *const tips[] = {
[button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside];
}
_effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent]];
self.menuButtons = [_buttons copy];
[self updateSelectedButton];
UIVisualEffect *effect = nil;
/*
// Unfortunately, UIGlassEffect is still very buggy.
if (@available(iOS 19.0, *)) {
effect = [[objc_getClass("UIGlassEffect") alloc] init];
}
else */ {
effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent];
}
_effectView = [[UIVisualEffectView alloc] initWithEffect:nil];
_effectView.layer.cornerRadius = 8;
_effectView.layer.masksToBounds = true;
[self.view.superview addSubview:_effectView];
[self.view.window addSubview:_effectView];
_tipLabel = [[UILabel alloc] init];
unsigned tipIndex = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBTipIndex"];
_tipLabel.text = tips[tipIndex % (sizeof(tips) / sizeof(tips[0]))];
@ -113,20 +131,23 @@ static NSString *const tips[] = {
_tipLabel.textColor = [UIColor labelColor];
}
_tipLabel.font = [UIFont systemFontOfSize:14];
_tipLabel.alpha = 0.8;
_tipLabel.alpha = 0;
[[NSUserDefaults standardUserDefaults] setInteger:tipIndex + 1 forKey:@"GBTipIndex"];
_tipLabel.lineBreakMode = NSLineBreakByWordWrapping;
_tipLabel.numberOfLines = 3;
[_effectView.contentView addSubview:_tipLabel];
[self layoutTip];
_effectView.alpha = 0;
[UIView animateWithDuration:0.25 animations:^{
_effectView.alpha = 1.0;
_effectView.effect = effect;
_tipLabel.alpha = 0.8;
}];
}
- (void)layoutTip
{
[_effectView.superview addSubview:_effectView];
UIView *view = self.view.superview;
CGSize outerSize = view.frame.size;
CGSize size = [_tipLabel textRectForBounds:(CGRect){{0, 0},
@ -135,8 +156,12 @@ static NSString *const tips[] = {
limitedToNumberOfLines:3].size;
size.width = ceil(size.width);
_tipLabel.frame = (CGRect){{8, 8}, size};
unsigned topInset = view.window.safeAreaInsets.top;
if (!topInset && [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
topInset = 32; // iPadOS is buggy af
}
_effectView.frame = (CGRect) {
{round((outerSize.width - size.width - 16) / 2), view.window.safeAreaInsets.top + 12},
{round((outerSize.width - size.width - 16) / 2), topInset + 12},
{size.width + 16, size.height + 16}
};
}
@ -145,7 +170,10 @@ static NSString *const tips[] = {
- (void)viewWillDisappear:(BOOL)animated
{
[UIView animateWithDuration:0.25 animations:^{
_effectView.alpha = 0;
_effectView.effect = nil;
_tipLabel.alpha = 0;
} completion:^(BOOL finished) {
[_effectView removeFromSuperview];
}];
[super viewWillDisappear:animated];
}
@ -189,4 +217,116 @@ static NSString *const tips[] = {
return [[UIViewController alloc] init];
}
#pragma mark - Vim Navigation
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (NSArray<UIKeyCommand *> *)keyCommands
{
return @[
[UIKeyCommand keyCommandWithInput:@"h" modifierFlags:0 action:@selector(moveLeft)],
[UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:0 action:@selector(moveLeft)],
[UIKeyCommand keyCommandWithInput:@"j" modifierFlags:0 action:@selector(moveDown)],
[UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:0 action:@selector(moveDown)],
[UIKeyCommand keyCommandWithInput:@"k" modifierFlags:0 action:@selector(moveUp)],
[UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(moveUp)],
[UIKeyCommand keyCommandWithInput:@"l" modifierFlags:0 action:@selector(moveRight)],
[UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:0 action:@selector(moveRight)],
[UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(activateSelected)],
[UIKeyCommand keyCommandWithInput:@" " modifierFlags:0 action:@selector(activateSelected)],
[UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:0 action:@selector(dismissSelf)],
];
}
- (void)moveLeft
{
if (self.selectedButtonIndex == -1) {
self.selectedButtonIndex = 0;
}
else if (self.selectedButtonIndex % 4 > 0) {
self.selectedButtonIndex--;
}
[self updateSelectedButton];
}
- (void)moveRight
{
if (self.selectedButtonIndex == -1) {
self.selectedButtonIndex = 0;
}
else if (self.selectedButtonIndex % 4 < 3 && self.selectedButtonIndex + 1 < self.menuButtons.count) {
self.selectedButtonIndex++;
}
[self updateSelectedButton];
}
- (void)moveUp
{
if (self.selectedButtonIndex == -1) {
self.selectedButtonIndex = 0;
}
else if (self.selectedButtonIndex >= 4) {
self.selectedButtonIndex -= 4;
}
[self updateSelectedButton];
}
- (void)moveDown
{
if (self.selectedButtonIndex == -1) {
self.selectedButtonIndex = 0;
}
else if (self.selectedButtonIndex + 4 < self.menuButtons.count) {
self.selectedButtonIndex += 4;
}
[self updateSelectedButton];
}
- (void)activateSelected
{
if (self.selectedButtonIndex >= 0 && self.selectedButtonIndex < self.menuButtons.count) {
UIButton *button = self.menuButtons[self.selectedButtonIndex];
if (button.enabled) {
[button sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
}
- (void)updateSelectedButton
{
for (NSInteger i = 0; i < self.menuButtons.count; i++) {
UIButton *button = self.menuButtons[i];
if (i == self.selectedButtonIndex) {
button.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.3];
button.layer.borderWidth = 2.0;
button.layer.borderColor = [UIColor systemBlueColor].CGColor;
if (@available(iOS 19.0, *)) {
button.layer.cornerRadius = 32.0;
}
else {
button.layer.cornerRadius = 8.0;
}
}
else {
button.backgroundColor = [UIColor clearColor];
button.layer.borderWidth = 0.0;
button.layer.borderColor = [UIColor clearColor].CGColor;
}
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self becomeFirstResponder];
}
- (void)dismissSelf
{
[self.presentingViewController dismissViewControllerAnimated:true completion:nil];
}
@end

View File

@ -10,6 +10,7 @@
@property (readonly) NSString *batterySaveFile;
@property (readonly) NSString *autosaveStateFile;
@property (readonly) NSString *cheatsFile;
@property (readonly) NSArray <NSString *> *forbiddenNames;
@property (readonly) NSString *localRoot;
- (NSString *)stateFile:(unsigned)index;

View File

@ -25,12 +25,26 @@
if (!self) return nil;
self.currentROM = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBLastROM"];
_doneInitializing = true;
// Pre 1.0.2 versions might have kept temp files in there incorrectly
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBDeletedInbox"]) {
[[NSFileManager defaultManager] removeItemAtPath:[self.localRoot stringByAppendingPathComponent:@"Inbox"] error:nil];
[[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBDeletedInbox"];
}
return self;
}
- (NSArray<NSString *> *)forbiddenNames
{
return @[@"Inbox", @"Boot ROMs"];
}
- (void)setCurrentROM:(NSString *)currentROM
{
_romFile = nil;
if ([self.forbiddenNames containsObject:currentROM]) {
currentROM = nil;
}
_currentROM = currentROM;
bool foundROM = self.romFile;
@ -58,6 +72,9 @@
- (NSString *)romDirectoryForROM:(NSString *)romFile
{
if ([self.forbiddenNames containsObject:romFile]) {
return nil;
}
return [self.localRoot stringByAppendingPathComponent:romFile];
}
@ -71,12 +88,13 @@
- (NSString *)romFileForROM:(NSString *)rom
{
if ([rom isEqualToString:@"Inbox"]) return nil;
if ([rom isEqualToString:@"Boot ROMs"]) return nil;
if (rom == _currentROM) {
return self.romFile;
}
if ([self.forbiddenNames containsObject:rom]) {
return nil;
}
return [self romFileForDirectory:[self romDirectoryForROM:rom]];
}
@ -143,6 +161,9 @@
- (NSString *)makeNameUnique:(NSString *)name
{
if ([self.forbiddenNames containsObject:name]) {
name = @"Imported ROM";
}
NSString *root = self.localRoot;
if (![[NSFileManager defaultManager] fileExistsAtPath:[root stringByAppendingPathComponent:name]]) return name;
@ -195,9 +216,11 @@
[[NSFileManager defaultManager] removeItemAtPath:romFolder error:nil];
return nil;
}
}
// Remove the Inbox directory if empty after import
rmdir([self.localRoot stringByAppendingPathComponent:@"Inbox"].UTF8String);
return friendlyName;
}

View File

@ -247,8 +247,19 @@
if ([newName containsString:@"/"]) {
[self.tableView reloadData];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"You can't use a name that contains “/”. Please choose another name."
message:nil
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Invalid Name"
message:@"You can't use a name that contains “/”. Please choose another name."
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:alert animated:true completion:nil];
return;
}
if ([[GBROMManager sharedManager].forbiddenNames containsObject:newName]) {
[self.tableView reloadData];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Invalid Name"
message:@"This name is reserved by SameBoy or iOS. Please choose another name."
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleCancel

View File

@ -15,11 +15,10 @@ typedef enum {
GBTurbo,
GBRewind,
GBUnderclock,
GBRapidA,
GBRapidB,
// GBHotkey1, // Todo
// GBHotkey2, // Todo
GBTotalButtonCount,
GBKeyboardButtonCount = GBUnderclock + 1,
GBPerPlayerButtonCount = GBStart + 1,
GBUnusedButton = 0xFF,
} GBButton;

View File

@ -15,13 +15,34 @@ static NSString const *typeCheck = @"check";
static NSString const *typeDisabled = @"disabled";
static NSString const *typeSeparator = @"separator";
static NSString const *typeSlider = @"slider";
static NSString const *typeLightTemp = @"typeLightTemp";
static NSString const *typeLightTemp = @"lightTemp";
static NSString const *typeTurboSlider = @"turboSlider";
@implementation GBSettingsViewController
{
NSArray<NSDictionary *> *_structure;
UINavigationController *_detailsNavigation;
NSArray<NSArray<GBTheme *> *> *_themes; // For prewarming
bool _iPadRoot;
__weak UISlider *_turboSlider;
}
+ (UIImage *)settingsImageNamed:(NSString *)name
{
UIImage *base = [UIImage imageNamed:name];
UIGraphicsBeginImageContextWithOptions(base.size, false, base.scale);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:(CGRect){{0, 0}, base.size} cornerRadius:8];
CGContextSaveGState(UIGraphicsGetCurrentContext());
[path addClip];
[base drawInRect:path.bounds];
if (@available(iOS 19.0, *)) {
CGContextRestoreGState(UIGraphicsGetCurrentContext());
UIImage *overlay = [UIImage imageNamed:@"settingsOverlay"];
[overlay drawInRect:path.bounds];
}
UIImage *ret = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return ret;
}
+ (NSArray<NSDictionary *> *)rootStructure
@ -48,17 +69,32 @@ static NSString const *typeLightTemp = @"typeLightTemp";
]
},
@{
@"header": @"Turbo Speed",
@"header": @"Turbo Speed Cap",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBTurboSpeed", @"title": @"200%", @"value": @2,},
@{@"type": typeRadio, @"pref": @"GBTurboSpeed", @"title": @"400%", @"value": @4,},
@{@"type": typeRadio, @"pref": @"GBTurboSpeed", @"title": @"Uncapped", @"value": @1,},
@{@"type": typeCheck, @"pref": @"GBTurboCap", @"title": @"Cap Turbo Speed", @"block": ^void(GBSettingsViewController *controller) {
if ([[NSUserDefaults standardUserDefaults] floatForKey:@"GBTurboCap"] == 1.0) {
if (controller->_turboSlider) {
[[NSUserDefaults standardUserDefaults] setFloat:controller->_turboSlider.value forKey:@"GBTurboCap"];
controller->_turboSlider.enabled = true;
}
else {
[[NSUserDefaults standardUserDefaults] setFloat:2.0 forKey:@"GBTurboCap"];
}
}
else {
controller->_turboSlider.enabled = false;
}
}},
@{@"type": typeTurboSlider, @"pref": @"GBTurboCap", @"min": @1.5, @"max": @4, @"minImage": @"tortoise.fill", @"maxImage": @"hare.fill"},
],
@"footer": ^NSString *(){
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
return @"This setting will have no effect because horizontal swipes are configured to dynamically control speed in the “Controls” settings";
return @"This setting will have no effect because horizontal swipes are configured to dynamically control speed in the “Controls” settings.";
}
return @"";
if ([[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]) {
return [NSString stringWithFormat:@"Turbo speed is capped to %u%%", (unsigned)round([[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"] * 100)];
}
return @"Turbo speed is not capped";
},
},
@{
@ -169,7 +205,7 @@ static NSString const *typeLightTemp = @"typeLightTemp";
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"HQ2x", @"value": @"HQ2x"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"OmniScale", @"value": @"OmniScale"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"OmniScale Legacy", @"value": @"OmniScaleLegacy"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"AA OmniScale Legacy", @"value": @"AAOmniScaleLegacy"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Anti-aliased OmniScale Legacy", @"value": @"AAOmniScaleLegacy"},
]
},
]
@ -365,31 +401,31 @@ static NSString const *typeLightTemp = @"typeLightTemp";
@"title": @"Emulation",
@"type": typeSubmenu,
@"submenu": emulationMenu,
@"image": [UIImage imageNamed:@"emulationSettings"],
@"image": [self settingsImageNamed:@"emulationSettings"],
},
@{
@"title": @"Video",
@"type": typeSubmenu,
@"submenu": videoMenu,
@"image": [UIImage imageNamed:@"videoSettings"],
@"image": [self settingsImageNamed:@"videoSettings"],
},
@{
@"title": @"Audio",
@"type": typeSubmenu,
@"submenu": audioMenu,
@"image": [UIImage imageNamed:@"audioSettings"],
@"image": [self settingsImageNamed:@"audioSettings"],
},
@{
@"title": @"Controls",
@"type": typeSubmenu,
@"submenu": controlsMenu,
@"image": [UIImage imageNamed:@"controlsSettings"],
@"image": [self settingsImageNamed:@"controlsSettings"],
},
@{
@"title": @"Themes",
@"type": typeSubmenu,
@"class": [GBThemesViewController class],
@"image": [UIImage imageNamed:@"themeSettings"],
@"image": [self settingsImageNamed:@"themeSettings"],
},
];
@ -416,12 +452,20 @@ static NSString const *typeLightTemp = @"typeLightTemp";
return controller;
}
UISplitViewController *split = [[UISplitViewController alloc] init];
UISplitViewController *split = nil;
if (@available(iOS 14.5, *)) {
split = [[UISplitViewController alloc] initWithStyle:UISplitViewControllerStyleDoubleColumn];
split.displayModeButtonVisibility = UISplitViewControllerDisplayModeButtonVisibilityNever;
}
else {
split = [[UISplitViewController alloc] init];
}
UIViewController *blank = [[UIViewController alloc] init];
blank.view.backgroundColor = root.view.backgroundColor;
root->_detailsNavigation = [[UINavigationController alloc] initWithRootViewController:blank];
split.viewControllers = @[controller, root->_detailsNavigation];
split.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
root->_iPadRoot = true;
return split;
}
@ -569,6 +613,8 @@ static NSString *LocalizedNameForElement(GCControllerElement *element, GBControl
@{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"B", @"value": @(GBB)},
@{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Select", @"value": @(GBSelect)},
@{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Start", @"value": @(GBStart)},
@{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rapid A", @"value": @(GBRapidA)},
@{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rapid B", @"value": @(GBRapidB)},
@{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Turbo", @"value": @(GBTurbo)},
@{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rewind", @"value": @(GBRewind)},
@{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Slow-motion", @"value": @(GBUnderclock)},
@ -709,14 +755,14 @@ static id ValueForItem(NSDictionary *item)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *item = [self itemForIndexPath:indexPath];
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil];
cell.textLabel.text = item[@"title"];
if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu || item[@"type"] == typeBlock) {
NSString *type = item[@"type"];
if (type == typeSubmenu || type == typeOptionSubmenu || type == typeBlock) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.selectionStyle = UITableViewCellSelectionStyleBlue;
if (item[@"type"] == typeOptionSubmenu) {
if (type == typeOptionSubmenu) {
for (NSDictionary *section in item[@"submenu"]) {
for (NSDictionary *item in section[@"items"]) {
if (item[@"value"] && [ValueForItem(item) isEqual:item[@"value"]]) {
@ -730,14 +776,14 @@ static id ValueForItem(NSDictionary *item)
cell.detailTextLabel.text = [[NSUserDefaults standardUserDefaults] stringForKey:item[@"pref"]];
}
}
else if (item[@"type"] == typeRadio) {
else if (type == typeRadio) {
id settingValue = ValueForItem(item);
id itemValue = item[@"value"];
if (settingValue == itemValue || [settingValue isEqual:itemValue]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
}
else if (item[@"type"] == typeCheck) {
else if (type == typeCheck) {
UISwitch *button = [[UISwitch alloc] init];
cell.accessoryView = button;
if ([[NSUserDefaults standardUserDefaults] boolForKey:item[@"pref"]]) {
@ -747,6 +793,9 @@ static id ValueForItem(NSDictionary *item)
__weak typeof(self) weakSelf = self;
id block = ^(){
[[NSUserDefaults standardUserDefaults] setBool:button.on forKey:item[@"pref"]];
if (item[@"block"]) {
((void(^)(GBSettingsViewController *))item[@"block"])(self);
}
unsigned section = [indexPath indexAtPosition:0];
UITableViewHeaderFooterView *view = [weakSelf.tableView footerViewForSection:section];
view.textLabel.text = [weakSelf tableView:weakSelf.tableView titleForFooterInSection:section];
@ -761,7 +810,7 @@ static id ValueForItem(NSDictionary *item)
[button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
else if (item[@"type"] == typeDisabled) {
else if (type == typeDisabled) {
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if (@available(iOS 13.0, *)) {
cell.textLabel.textColor = [UIColor separatorColor];
@ -770,24 +819,34 @@ static id ValueForItem(NSDictionary *item)
cell.textLabel.textColor = [UIColor colorWithWhite:0 alpha:0.75];
}
}
else if (item[@"type"] == typeSeparator) {
else if (type == typeSeparator) {
cell.backgroundColor = [UIColor clearColor];
cell.separatorInset = UIEdgeInsetsZero;
}
else if (item[@"type"] == typeSlider ||
item[@"type"] == typeLightTemp) {
else if (type == typeSlider ||
type == typeLightTemp ||
type == typeTurboSlider) {
CGRect rect = cell.contentView.bounds;
rect.size.width -= 24;
rect.size.height -= 24;
rect.origin.x += 12;
rect.origin.y += 12;
UISlider *slider = [item[@"type"] == typeLightTemp? [GBSlider alloc] : [UISlider alloc] initWithFrame:rect];
UISlider *slider = [type == typeLightTemp? [GBSlider alloc] : [UISlider alloc] initWithFrame:rect];
slider.continuous = true;
slider.minimumValue = [item[@"min"] floatValue];
slider.maximumValue = [item[@"max"] floatValue];
slider.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[cell.contentView addSubview:slider];
slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]];
if (type == typeTurboSlider) {
slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]] ?: 2.0;
_turboSlider = slider;
if (![[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]]) {
slider.enabled = false;
}
}
else {
slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if (@available(iOS 13.0, *)) {
@ -798,8 +857,19 @@ static id ValueForItem(NSDictionary *item)
}
}
__weak typeof(self) weakSelf = self;
id block = ^(){
[[NSUserDefaults standardUserDefaults] setDouble:slider.value forKey:item[@"pref"]];
if (type == typeTurboSlider) {
unsigned section = [indexPath indexAtPosition:0];
UITableViewHeaderFooterView *view = [weakSelf.tableView footerViewForSection:section];
view.textLabel.text = [weakSelf tableView:weakSelf.tableView titleForFooterInSection:section];
[UIView setAnimationsEnabled:false];
[weakSelf.tableView beginUpdates];
[view sizeToFit];
[weakSelf.tableView endUpdates];
[UIView setAnimationsEnabled:true];
}
};
objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
@ -813,13 +883,21 @@ static id ValueForItem(NSDictionary *item)
cell.separatorInset = UIEdgeInsetsZero;
}
cell.imageView.image = item[@"image"];
if (@available(iOS 19.0, *)) {
if (_iPadRoot) {
cell.textLabel.textColor = [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) {
return cell.isSelected? [UIColor whiteColor] : [UIColor labelColor];
}];
}
}
return cell;
}
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *item = [self itemForIndexPath:indexPath];
if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu) {
NSString *type = item[@"type"];
if (type == typeSubmenu || type == typeOptionSubmenu) {
UITableViewStyle style = UITableViewStyleGrouped;
if (@available(iOS 13.0, *)) {
style = UITableViewStyleInsetGrouped;
@ -843,7 +921,7 @@ static id ValueForItem(NSDictionary *item)
}
return indexPath;
}
else if (item[@"type"] == typeRadio) {
else if (type == typeRadio) {
if (item[@"setter"]) {
((void(^)(id))item[@"setter"])(item[@"value"]);
}
@ -852,7 +930,7 @@ static id ValueForItem(NSDictionary *item)
}
[self.tableView reloadData];
}
else if (item[@"type"] == typeBlock) {
else if (type == typeBlock) {
if (((bool(^)(GBSettingsViewController *))item[@"block"])(self)) {
return indexPath;
}
@ -863,11 +941,13 @@ static id ValueForItem(NSDictionary *item)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *item = [self itemForIndexPath:indexPath];
if (item[@"type"] == typeSeparator) {
NSString *type = item[@"type"];
if (type == typeSeparator) {
return 8;
}
if (item[@"type"] == typeSlider ||
item[@"type"] == typeLightTemp) {
if (type == typeSlider ||
type == typeLightTemp ||
type == typeTurboSlider) {
return 63;
}
return [super tableView:tableView heightForRowAtIndexPath:indexPath];

View File

@ -1,4 +1,19 @@
#import "GBSlider.h"
#import <objc/runtime.h>
#if !__has_include(<UIKit/UISliderTrackConfiguration.h>)
/* Building with older SDKs */
API_AVAILABLE(ios(19.0))
@interface UISliderTrackConfiguration : NSObject
@property (nonatomic, readwrite) bool allowsTickValuesOnly;
@property (nonatomic, readwrite) float neutralValue;
+ (instancetype)configurationWithNumberOfTicks:(NSInteger)ticks;
@end
@interface UISlider (configuration)
@property(nonatomic, copy, nullable) UISliderTrackConfiguration *trackConfiguration API_AVAILABLE(ios(19.0));
@end
#endif
static inline void temperature_tint(double temperature, double *r, double *g, double *b)
{
@ -21,11 +36,15 @@ static inline void temperature_tint(double temperature, double *r, double *g, do
}
@implementation GBSlider
{
GBSliderStyle _style;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
[self addTarget:self action:@selector(valueChanged) forControlEvents:UIControlEventValueChanged];
self.style = GBSliderStyleTemperature;
return self;
}
@ -97,6 +116,10 @@ static inline void temperature_tint(double temperature, double *r, double *g, do
- (void)drawRect:(CGRect)rect
{
bool solarium = false;
if (@available(iOS 19.0, *)) {
solarium = true;
}
CGSize size = self.bounds.size;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2, round(size.height / 2 - 1.5), size.width - 4, 3) cornerRadius:4];
if (_style != GBSliderStyleHue) {
@ -107,7 +130,7 @@ static inline void temperature_tint(double temperature, double *r, double *g, do
[path appendPath:[UIBezierPath bezierPathWithRoundedRect:CGRectMake(size.width - 3, 12, 3, size.height - 24) cornerRadius:4]];
}
if (_style == GBSliderStyleHue) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(3, round(size.height / 2 - 1.5) + 1, size.width - 6, 1) cornerRadius:4];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(3, round(size.height / 2 - 1.5) + 1 - solarium, size.width - 6, solarium? 2 : 1) cornerRadius:8];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
[path addClip];
@ -124,11 +147,12 @@ static inline void temperature_tint(double temperature, double *r, double *g, do
};
CFArrayRef colorsArray = CFArrayCreate(NULL, (const void **)colors, 7, &kCFTypeArrayCallBacks);
CGGradientRef gradient = CGGradientCreateWithColors(colorspace, colorsArray, NULL);
unsigned spacing = solarium? 16 : 3;
CGContextDrawLinearGradient(context,
gradient,
(CGPoint){3, 0},
(CGPoint){size.width - 3, 0},
0);
(CGPoint){spacing, 0},
(CGPoint){size.width - spacing, 0},
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
CFRelease(gradient);
CFRelease(colorsArray);
CFRelease(colorspace);
@ -139,11 +163,44 @@ static inline void temperature_tint(double temperature, double *r, double *g, do
green:120 / 255.0
blue:130 / 255.0
alpha:70 / 255.0] set];
[path fill];
if (!solarium) {
[path fill];
}
[super drawRect:rect];
}
- (void)setStyle:(GBSliderStyle)style
{
_style = style;
if (@available(iOS 19.0, *)) {
switch (_style) {
case GBSliderStyleTemperature:
case GBSliderStyleTicks: {
UISliderTrackConfiguration *conf = [objc_getClass("UISliderTrackConfiguration") configurationWithNumberOfTicks:3];
conf.allowsTickValuesOnly = false;
conf.neutralValue = 0.5;
self.trackConfiguration = conf;
self.maximumTrackTintColor = nil;
self.minimumTrackTintColor = nil;
break;
}
case GBSliderStyleHue: {
UISliderTrackConfiguration *conf = [objc_getClass("UISliderTrackConfiguration") configurationWithNumberOfTicks:0];
conf.allowsTickValuesOnly = false;
self.trackConfiguration = conf;
self.minimumTrackTintColor = [UIColor clearColor];
break;
}
}
}
}
- (GBSliderStyle)style
{
return _style;
}
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];

View File

@ -39,8 +39,8 @@
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.bounds = CGRectMake(0, 0, 0x300, 0x300);
UIView *root = self.view;
UIView *root = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0x300, 0x300)];
[self.view addSubview:root];
for (unsigned i = 0; i < 9; i++) {
unsigned x = i % 3;
unsigned y = i / 3;
@ -114,6 +114,12 @@
[self presentViewController:controller animated:true completion:nil];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
self.view.subviews.firstObject.frame = [self.view.safeAreaLayoutGuide layoutFrame];
}
- (NSString *)title
{
return @"Save States";

View File

@ -65,8 +65,8 @@
- (void)showPopup
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Apply “%@” as the current theme?", _verticalLayout.theme.name]
message:nil
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Apply Theme"
message:[NSString stringWithFormat:@"Apply “%@” as the current theme?", _verticalLayout.theme.name]
preferredStyle:[UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet];
if (false) {

View File

@ -21,11 +21,85 @@
#import <sys/stat.h>
#import <CoreMotion/CoreMotion.h>
#import <dlfcn.h>
#import <objc/runtime.h>
#if !__has_include(<UIKit/UISliderTrackConfiguration.h>)
/* Building with older SDKs */
typedef NS_ENUM(NSInteger, UIMenuSystemElementGroupPreference) {
UIMenuSystemElementGroupPreferenceAutomatic = 0,
UIMenuSystemElementGroupPreferenceRemoved,
UIMenuSystemElementGroupPreferenceIncluded,
};
API_AVAILABLE(ios(19.0))
@interface UIMainMenuSystemConfiguration : NSObject <NSCopying>
@property (nonatomic, assign) UIMenuSystemElementGroupPreference newScenePreference;
@property (nonatomic, assign) UIMenuSystemElementGroupPreference documentPreference;
@property (nonatomic, assign) UIMenuSystemElementGroupPreference printingPreference;
@property (nonatomic, assign) UIMenuSystemElementGroupPreference findingPreference;
@property (nonatomic, assign) UIMenuSystemElementGroupPreference toolbarPreference;
@property (nonatomic, assign) UIMenuSystemElementGroupPreference sidebarPreference;
@property (nonatomic, assign) UIMenuSystemElementGroupPreference inspectorPreference;
@property (nonatomic, assign) UIMenuSystemElementGroupPreference textFormattingPreference;
@end
API_AVAILABLE(ios(19.0))
@interface UIMainMenuSystem : UIMenuSystem
@property (class, nonatomic, readonly) UIMainMenuSystem *sharedSystem;
- (void)setBuildConfiguration:(UIMainMenuSystemConfiguration *)configuration buildHandler:(void(^)(NSObject<UIMenuBuilder> *builder))buildHandler;
@end
API_AVAILABLE(ios(19.0))
@interface NSObject(UIMenuBuilder)
- (void)insertElements:(NSArray<UIMenuElement *> *)childElements atStartOfMenuForIdentifier:(UIMenuIdentifier)parentIdentifier;
@end
#endif
static UIImage *CreateMenuImage(NSString *name)
{
static const unsigned size = 20;
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(size, size)];
CGRect destRect = {0,};
UIImage *source = [UIImage imageNamed:name];
CGSize sourceSize = source.size;
if (sourceSize.width > sourceSize.height) {
destRect.size.width = size;
destRect.size.height = sourceSize.height * size / sourceSize.width;
destRect.origin.y = (size - destRect.size.height) / 2;
}
else {
destRect.size.height = size;
destRect.size.width = sourceSize.width * size / sourceSize.height;
destRect.origin.x = (size - destRect.size.width) / 2;
}
UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *myContext) {
[source drawInRect:destRect];
}];
return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
API_AVAILABLE(ios(13.0))
@implementation UIKeyCommand (KeyCommandWithImage)
+ (instancetype)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action title:(NSString *)title image:(UIImage *)image
{
UIKeyCommand *ret = [self keyCommandWithInput:input modifierFlags:modifierFlags action:action];
ret.title = title;
ret.image = image;
return ret;
}
@end
@implementation GBViewController
{
GB_gameboy_t _gb;
GBView *_gbView;
dispatch_queue_t _runQueue;
volatile bool _running;
volatile bool _stopping;
@ -35,6 +109,9 @@
bool _swappingROM;
bool _skipAutoLoad;
bool _rapidA, _rapidB;
uint8_t _rapidACount, _rapidBCount;
UIInterfaceOrientation _orientation;
GBHorizontalLayout *_horizontalLayoutLeft;
GBHorizontalLayout *_horizontalLayoutRight;
@ -190,6 +267,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self addDefaultObserver:^(id newValue) {
GB_set_rewind_length(gb, [newValue unsignedIntValue]);
} forKey:@"GBRewindLength"];
[self addDefaultObserver:^(id newValue) {
GB_set_turbo_cap(gb, [newValue doubleValue]);
} forKey:@"GBTurboCap"];
[self addDefaultObserver:^(id newValue) {
[[AVAudioSession sharedInstance] setCategory:[newValue isEqual:@"on"]? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient
mode:AVAudioSessionModeDefault
@ -236,6 +316,8 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
_window.rootViewController = self;
[_window makeKeyAndVisible];
_runQueue = dispatch_queue_create("SameBoy Emulation Queue", NULL);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
[self addDefaultObserver:^(id newValue) {
@ -431,9 +513,93 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self updateMirrorWindow];
if (@available(iOS 26.0, *)) {
UIMainMenuSystemConfiguration *conf = [[objc_getClass("UIMainMenuSystemConfiguration") alloc] init];
conf.newScenePreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.documentPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.printingPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.findingPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.toolbarPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.sidebarPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.inspectorPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.textFormattingPreference = UIMenuSystemElementGroupPreferenceRemoved;
UIMainMenuSystem *system = (id)[objc_getClass("UIMainMenuSystem") sharedSystem];
[system setBuildConfiguration:conf
buildHandler:^(id<UIMenuBuilder> builder) {
[builder removeMenuForIdentifier:UIMenuView]; // This menu's always empty
[builder removeMenuForIdentifier:UIMenuOpenRecent]; // This will list files to re-import, bad
[(id)builder insertElements:@[[UICommand commandWithTitle:@"About SameBoy"
image:nil
action:@selector(showAbout)
propertyList:nil]]
atStartOfMenuForIdentifier:UIMenuApplication];
UIMenu *emulationMenu = [UIMenu menuWithTitle:@"Emulation" children:@[
[UIKeyCommand keyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand action:@selector(reset) title:@"Reset" image:[UIImage systemImageNamed:@"arrow.2.circlepath"]],
[UICommand commandWithTitle:@"Change Model…" image:CreateMenuImage(@"ModelTemplate") action:@selector(changeModel) propertyList:nil],
[UICommand commandWithTitle:@"Save States…" image:[UIImage systemImageNamed:@"square.stack"] action:@selector(openStates) propertyList:nil],
[UIMenu menuWithTitle:@"Cheats" image:CreateMenuImage(@"CheatsTemplate") identifier:nil options:0 children:@[
[UIKeyCommand keyCommandWithInput:@"c" modifierFlags:UIKeyModifierCommand | UIKeyModifierShift action:@selector(toggleCheats) title:@"Enable Cheats" image:nil],
[UICommand commandWithTitle:@"Show Cheats…" image:nil action:@selector(openCheats) propertyList:nil],
]],
[UIMenu menuWithTitle:@"Connect" image:CreateMenuImage(@"LinkCableTemplate") identifier:nil options:0 children:@[
[UICommand commandWithTitle:@"None" image:nil action:@selector(disconnectCable) propertyList:nil],
[UICommand commandWithTitle:@"Printer" image:[UIImage systemImageNamed:@"printer"] action:@selector(connectPrinter) propertyList:nil],
]],
]];
[builder insertSiblingMenu:emulationMenu beforeMenuForIdentifier:UIMenuWindow];
}];
}
return true;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == @selector(reset)) {
if (self.presentedViewController || ![GBROMManager sharedManager].currentROM) return false;
}
if (action == @selector(openStates) || action == @selector(changeModel) || action == @selector(openCheats) ||
action == @selector(toggleCheats) || action == @selector(disconnectCable) || action == @selector(connectPrinter)) {
if (![GBROMManager sharedManager].currentROM) return false;
}
return [super canPerformAction:action withSender:sender];
}
- (void)validateCommand:(UICommand *)command
{
if (command.action == @selector(toggleCheats)) {
command.state = GB_is_inited(&_gb) && GB_cheats_enabled(&_gb);
}
if (command.action == @selector(connectPrinter)) {
command.state = _printerConnected;
}
if (command.action == @selector(disconnectCable)) {
command.state = !_printerConnected;
}
[super validateCommand:command];
}
- (void)toggleCheats
{
GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb));
}
- (void)orderFrontPreferencesPanel:(id)sender
{
[self openSettings];
}
- (void)open:(id)sender
{
[self openLibrary];
}
- (void)updateMirrorWindow
{
if ([UIScreen screens].count == 1) {
@ -526,6 +692,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
case GBStart:
GB_set_key_state(&_gb, (GB_key_t)gbButton, button.value > 0.25);
break;
case GBRapidA:
_rapidA = button.value > 0.25;
_rapidACount = 0;
break;
case GBRapidB:
_rapidB = button.value > 0.25;
_rapidBCount = 0;
break;
case GBTurbo:
if (button.value > analogThreshold) {
[self setRunMode:GBRunModeTurbo ignoreDynamicSpeed:!button.isAnalog];
@ -552,7 +726,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[_backgroundView fadeOverlayOut];
}
else {
if (self.runMode == GBRunModeRewind && _runModeFromController) {
if ((self.runMode == GBRunModeRewind || self.runMode == GBRunModePaused) && _runModeFromController) {
[self setRunMode:GBRunModeNormal];
_runModeFromController = false;
}
@ -673,15 +847,31 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (void)saveStateToFile:(NSString *)file
{
NSString *tempPath = [file stringByAppendingPathExtension:@"tmp"];
if (!GB_save_state(&_gb, tempPath.UTF8String)) {
int error = GB_save_state(&_gb, tempPath.UTF8String);
if (!error) {
rename(tempPath.UTF8String, file.UTF8String);
NSData *data = [NSData dataWithBytes:_gbView.previousBuffer
length:GB_get_screen_width(&_gb) *
GB_get_screen_height(&_gb) *
sizeof(*_gbView.previousBuffer)];
UIImage *screenshot = [self imageFromData:data width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb)];
[UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false];
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Could not Save State"
message:[NSString stringWithFormat:@"An error occured while attempting to save: %s", strerror(error)]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
style:UIAlertActionStyleCancel
handler:nil]];
UIViewController *top = self;
while (top.presentedViewController) {
top = top.presentedViewController;
}
[top presentViewController:alert animated:true completion:nil];
});
}
NSData *data = [NSData dataWithBytes:_gbView.previousBuffer
length:GB_get_screen_width(&_gb) *
GB_get_screen_height(&_gb) *
sizeof(*_gbView.previousBuffer)];
UIImage *screenshot = [self imageFromData:data width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb)];
[UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false];
}
- (bool)loadStateFromFile:(NSString *)file
@ -704,49 +894,68 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
if (romManager.romFile) {
if (!_skipAutoLoad) {
// Todo: display errors and warnings
if ([romManager.romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) {
_romLoaded = GB_load_isx(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
bool needsStateLoad = false;
if (![_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) {
if ([romManager.romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) {
_romLoaded = GB_load_isx(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
}
else {
_romLoaded = GB_load_rom(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
}
needsStateLoad = true;
if (@available(iOS 16.0, *)) {
dispatch_async(dispatch_get_main_queue(), ^{
[super setNeedsUpdateOfSupportedInterfaceOrientations];
});
}
}
else {
_romLoaded = GB_load_rom(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
else if (access(romManager.romFile.fileSystemRepresentation, R_OK)) {
_romLoaded = false;
}
if (_romLoaded) {
if (!_romLoaded) {
dispatch_async(dispatch_get_main_queue(), ^{
romManager.currentROM = nil;
});
}
if (!needsStateLoad) {
NSDate *date = nil;
[[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
forKey:NSURLContentModificationDateKey
error:nil];
if (![_saveDate isEqual:date]) {
needsStateLoad = true;
}
}
if (_romLoaded && needsStateLoad) {
GB_reset(&_gb);
GB_load_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
GB_remove_all_cheats(&_gb);
GB_load_cheats(&_gb, [GBROMManager sharedManager].cheatsFile.UTF8String, false);
if (![self loadStateFromFile:[GBROMManager sharedManager].autosaveStateFile]) {
// Newly played ROM, pick the best model
uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL);
if ((rom[0x143] & 0x80)) {
if (!GB_is_cgb(&_gb)) {
GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
if ([_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) {
/* Something weird just happened: we didn't change a ROM, but we failed to load the
latest save state. Save over the existing file, it's probably corrupt in some
way. */
[self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
}
else {
// Newly played ROM, pick the best model
uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL);
if ((rom[0x143] & 0x80)) {
if (!GB_is_cgb(&_gb)) {
GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
}
}
else if ((rom[0x146] == 3) && !GB_is_sgb(&_gb)) {
GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]);
}
}
else if ((rom[0x146] == 3) && !GB_is_sgb(&_gb)) {
GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]);
}
GB_rewind_reset(&_gb);
}
}
NSDate *date = nil;
@try {
[[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
forKey:NSURLContentModificationDateKey
error:nil];
}
@catch (NSException *exception) {
/* fileURLWithPath: throws an exception on some crash logs. I don't know why or how to reproduce it,
but let's at least not crash. */
GB_rewind_reset(&_gb);
}
// Reset the rewind buffer only if we switched ROMs or had the save state change externally
if (![_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM] ||
![_saveDate isEqual:date]) {
GB_rewind_reset(&_gb);
}
}
}
else {
@ -774,23 +983,48 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (void)reset
{
[self stop];
_skipAutoLoad = true;
GB_reset(&_gb);
[self start];
UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
UIAlertController *menu = [UIAlertController alertControllerWithTitle:@"Reset Emulation?"
message:@"Unsaved progress will be lost."
preferredStyle:style];
[menu addAction:[UIAlertAction actionWithTitle:@"Reset"
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
[self stop];
_skipAutoLoad = true;
GB_reset(&_gb);
[self start];
}]];
[menu addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:menu animated:true completion:nil];
}
- (void)openLibrary
{
[self presentViewController:[[GBLibraryViewController alloc] init]
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UIViewController *controller = [[GBLibraryViewController alloc] init];
presentedController = controller;
[self presentViewController:controller
animated:true
completion:nil];
}
- (void)changeModel
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
GBOptionViewController *controller = [[GBOptionViewController alloc] initWithHeader:@"Select a model to emulate"];
controller.footer = @"Changing the emulated model will reset the emulator";
presentedController = controller;
GB_model_t currentModel = GB_get_model(&_gb);
struct {
@ -845,7 +1079,12 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (void)openStates
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBStatesViewController alloc] init]];
presentedController = controller;
UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleProminent];
UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
effectView.frame = controller.view.bounds;
@ -863,23 +1102,41 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (void)openSettings
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
style:UIBarButtonItemStylePlain
target:self
action:@selector(dismissViewController)];
[self presentViewController:[GBSettingsViewController settingsViewControllerWithLeftButton:close]
UIViewController *controller = [GBSettingsViewController settingsViewControllerWithLeftButton:close];
presentedController = controller;
[self presentViewController:controller
animated:true
completion:nil];
}
- (void)showAbout
{
[self presentViewController:[[GBAboutController alloc] init] animated:true completion:nil];
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UIViewController *controller = [[GBAboutController alloc] init];
presentedController = controller;
[self presentViewController:controller animated:true completion:nil];
}
- (void)openCheats
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBCheatsController alloc] initWithGameBoy:&_gb]];
presentedController = controller;
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
style:UIBarButtonItemStylePlain
target:self
@ -905,9 +1162,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (void)setNeedsUpdateOfSupportedInterfaceOrientations
{
/* Hack. Some view controllers dismiss without calling the method above. */
[super setNeedsUpdateOfSupportedInterfaceOrientations];
dispatch_async(dispatch_get_main_queue(), ^{
[self start];
[super setNeedsUpdateOfSupportedInterfaceOrientations];
});
}
@ -916,6 +1173,22 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self dismissViewControllerAnimated:true completion:nil];
}
- (bool)dismissViewControllerIfSafe
{
if (!self.presentedViewController) return true;
if (![self.presentedViewController isKindOfClass:[UIAlertController class]]) {
[self dismissViewController];
return true;
}
if ([self.presentedViewController isKindOfClass:[GBMenuViewController class]]) {
[self dismissViewController];
return true;
}
return false;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
[self stop];
@ -923,6 +1196,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
{
if (_orientation != UIInterfaceOrientationUnknown && !((1 << orientation) & self.supportedInterfaceOrientations)) return;
GBLayout *layout = nil;
_orientation = orientation;
switch (orientation) {
@ -977,10 +1251,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
32,
32);
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
if (!self.shouldAutorotate && _orientation != UIInterfaceOrientationUnknown) {
return 1 << _orientation;
}
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
}
@ -1132,14 +1410,22 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
GB_rewind_pop(&_gb);
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
if (!GB_rewind_pop(&_gb)) {
self.runMode = GBRunModePaused;
dispatch_sync(dispatch_get_main_queue(), ^{
if (_runMode == GBRunModeRewind) {
self.runMode = GBRunModePaused;
}
});
_rewindOver = true;
}
}
else {
for (unsigned i = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindSpeed"]; i--;) {
if (!GB_rewind_pop(&_gb)) {
self.runMode = GBRunModePaused;
dispatch_sync(dispatch_get_main_queue(), ^{
if (_runMode == GBRunModeRewind) {
self.runMode = GBRunModePaused;
}
});
_rewindOver = true;
}
}
@ -1168,7 +1454,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
- (UIImage *)imageFromData:(NSData *)data width:(unsigned)width height:(unsigned)height
{
/* Convert the screenshot to a CGImageRef */
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, data.bytes, data.length, NULL);
CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)data);
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
@ -1257,7 +1543,9 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
if (_running) return;
if (self.presentedViewController) return;
_running = true;
[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
dispatch_async(_runQueue, ^{
[self run];
});
}
- (void)stop
@ -1273,6 +1561,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
[_audioLock signal];
[_audioLock unlock];
}
dispatch_sync(_runQueue, ^{});
dispatch_async(dispatch_get_main_queue(), ^{
self.runMode = GBRunModeNormal;
[_backgroundView fadeOverlayOut];
@ -1331,6 +1620,14 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
[_gbView flip];
GB_set_pixels_output(&_gb, _gbView.pixels);
}
if (_rapidA) {
_rapidACount++;
GB_set_key_state(&_gb, GB_KEY_A, !(_rapidACount & 2));
}
if (_rapidB) {
_rapidBCount++;
GB_set_key_state(&_gb, GB_KEY_B, !(_rapidBCount & 2));
}
_rewind = _runMode == GBRunModeRewind;
}
@ -1452,7 +1749,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
[url startAccessingSecurityScopedResource];
[GBROMManager sharedManager].currentROM =
[[GBROMManager sharedManager] importROM:url.path
keepOriginal:![url.path hasPrefix:tempDir] && !inPlace];
keepOriginal:![url.path hasPrefix:tempDir] && inPlace];
[url stopAccessingSecurityScopedResource];
}
return true;
@ -1465,7 +1762,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
}
[url startAccessingSecurityScopedResource];
[[GBROMManager sharedManager] importROM:url.path
keepOriginal:![url.path hasPrefix:tempDir] && !inPlace];
keepOriginal:![url.path hasPrefix:tempDir] && inPlace];
[url stopAccessingSecurityScopedResource];
}
[self openLibrary];
@ -1576,9 +1873,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
if (_runMode == GBRunModeNormal || _runMode == GBRunModeUnderclock || !([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"] && !ignoreDynamicSpeed)) {
if (_runMode == GBRunModeTurbo) {
double multiplier = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboSpeed"];
GB_set_turbo_mode(&_gb, multiplier == 1, false);
GB_set_clock_multiplier(&_gb, multiplier);
GB_set_turbo_mode(&_gb, true, false);
}
else if (_runMode == GBRunModeUnderclock) {
GB_set_clock_multiplier(&_gb, 0.5);
@ -1791,29 +2086,41 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
});
}
- (void)disconnectCable
{
if (!_printerConnected) return;
_printerConnected = false;
_currentPrinterImageData = nil;
[UIView animateWithDuration:0.25 animations:^{
_printerButton.alpha = 0;
}];
[_printerSpinner stopAnimating];
GB_disconnect_serial(&_gb);
}
- (void)connectPrinter
{
if (_printerConnected) return;
_printerConnected = true;
GB_connect_printer(&_gb, printImage, printDone);
}
- (void)openConnectMenu
{
UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
GBCheckableAlertController *menu = [GBCheckableAlertController alertControllerWithTitle:@"Connect which accessory?"
message:nil
GBCheckableAlertController *menu = [GBCheckableAlertController alertControllerWithTitle:@"Connect Accessory"
message:@"Choose an accessory to connect."
preferredStyle:style];
[menu addAction:[UIAlertAction actionWithTitle:@"None"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
_printerConnected = false;
_currentPrinterImageData = nil;
[UIView animateWithDuration:0.25 animations:^{
_printerButton.alpha = 0;
}];
[_printerSpinner stopAnimating];
GB_disconnect_serial(&_gb);
[self disconnectCable];
}]];
[menu addAction:[UIAlertAction actionWithTitle:@"Game Boy Printer"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
_printerConnected = true;
GB_connect_printer(&_gb, printImage, printDone);
[self connectPrinter];
}]];
menu.selectedAction = menu.actions[_printerConnected];
[menu addAction:[UIAlertAction actionWithTitle:@"Cancel"
@ -1874,3 +2181,31 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
}
@end
/* +[UIColor labelColor] is broken in some contexts in iOS 26 and despite being such a critical method
Apple isn't going to fix this in time. */
API_AVAILABLE(ios(19.0))
@implementation UIColor(SolariumBugs)
+ (UIColor *)_labelColor
{
return [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) {
switch (traitCollection.userInterfaceStyle) {
case UIUserInterfaceStyleUnspecified:
case UIUserInterfaceStyleLight:
return [UIColor blackColor];
case UIUserInterfaceStyleDark:
return [UIColor whiteColor];
}
}];
}
+ (void)load
{
if (@available(iOS 19.0, *)) {
method_setImplementation(class_getClassMethod(self, @selector(labelColor)),
[self methodForSelector:@selector(_labelColor)]);
}
}
@end

Some files were not shown because too many files have changed in this diff Show More