mirror of
https://github.com/D4C1-Labs/Flipper-ARF.git
synced 2026-07-16 19:12:08 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc59edff17 | ||
|
|
63d951dc38 | ||
|
|
f1422cb701 |
@@ -15,6 +15,7 @@ typedef enum {
|
||||
SubGhzCustomEventSceneReceiverInfoTxStart,
|
||||
SubGhzCustomEventSceneReceiverInfoTxStop,
|
||||
SubGhzCustomEventSceneReceiverInfoSave,
|
||||
SubGhzCustomEventSceneReceiverInfoTxFullDpad,
|
||||
SubGhzCustomEventSceneSaveName,
|
||||
SubGhzCustomEventSceneSignalSettings,
|
||||
SubGhzCustomEventSceneSaveSuccess,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "../subghz_i.h"
|
||||
|
||||
#include <lib/subghz/blocks/custom_btn.h>
|
||||
#include <flipper_format/flipper_format_i.h>
|
||||
|
||||
#include "applications/main/subghz/helpers/subghz_txrx_i.h"
|
||||
#include <lib/subghz/blocks/generic.h>
|
||||
@@ -20,6 +21,9 @@ void subghz_scene_receiver_info_callback(GuiButtonType result, InputType type, v
|
||||
} else if((result == GuiButtonTypeRight) && (type == InputTypeShort)) {
|
||||
view_dispatcher_send_custom_event(
|
||||
subghz->view_dispatcher, SubGhzCustomEventSceneReceiverInfoSave);
|
||||
} else if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
|
||||
view_dispatcher_send_custom_event(
|
||||
subghz->view_dispatcher, SubGhzCustomEventSceneReceiverInfoTxFullDpad);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +33,6 @@ static bool subghz_scene_receiver_info_update_parser(void* context) {
|
||||
if(subghz_txrx_load_decoder_by_name_protocol(
|
||||
subghz->txrx,
|
||||
subghz_history_get_protocol_name(subghz->history, subghz->idx_menu_chosen))) {
|
||||
// we are trying to deserialize without checking for errors, since it is assumed that we just received this chignal
|
||||
subghz_protocol_decoder_base_deserialize(
|
||||
subghz_txrx_get_decoder(subghz->txrx),
|
||||
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen));
|
||||
@@ -37,7 +40,6 @@ static bool subghz_scene_receiver_info_update_parser(void* context) {
|
||||
SubGhzRadioPreset* preset =
|
||||
subghz_history_get_radio_preset(subghz->history, subghz->idx_menu_chosen);
|
||||
|
||||
//Edit TX power, if necessary.
|
||||
subghz_txrx_set_tx_power(preset->data, preset->data_size, subghz->tx_power);
|
||||
|
||||
subghz_txrx_set_preset(
|
||||
@@ -93,7 +95,7 @@ void subghz_scene_receiver_info_draw_widget(SubGhz* subghz) {
|
||||
subghz_scene_receiver_info_callback,
|
||||
subghz);
|
||||
}
|
||||
// Removed static check
|
||||
|
||||
if(subghz_txrx_protocol_is_transmittable(subghz->txrx, false)) {
|
||||
widget_add_button_element(
|
||||
subghz->widget,
|
||||
@@ -101,9 +103,14 @@ void subghz_scene_receiver_info_draw_widget(SubGhz* subghz) {
|
||||
"Send",
|
||||
subghz_scene_receiver_info_callback,
|
||||
subghz);
|
||||
widget_add_button_element(
|
||||
subghz->widget,
|
||||
GuiButtonTypeLeft,
|
||||
"Full",
|
||||
subghz_scene_receiver_info_callback,
|
||||
subghz);
|
||||
}
|
||||
} else {
|
||||
// [NO_DOLPHIN] widget_add_icon_element(subghz->widget, 83, 22, &I_WarningDolphinFlip_45x42);
|
||||
widget_add_string_element(
|
||||
subghz->widget, 13, 8, AlignLeft, AlignBottom, FontSecondary, "Error history parse.");
|
||||
}
|
||||
@@ -131,12 +138,7 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
|
||||
if(!subghz_scene_receiver_info_update_parser(subghz)) {
|
||||
return false;
|
||||
}
|
||||
//CC1101 Stop RX -> Start TX
|
||||
subghz_txrx_hopper_pause(subghz->txrx);
|
||||
// key concept: we start endless TX until user release OK button, and after this we send last
|
||||
// protocols repeats - this guarantee that one press OK will
|
||||
// be guarantee send the required minimum protocol data packets
|
||||
// for all of this we use subghz_block_generic_global.endless_tx in protocols _yield function.
|
||||
subghz->state_notifications = SubGhzNotificationStateTx;
|
||||
subghz_block_generic_global.endless_tx = true;
|
||||
if(!subghz_tx_start(
|
||||
@@ -146,37 +148,51 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
|
||||
subghz_txrx_hopper_unpause(subghz->txrx);
|
||||
subghz->state_notifications = SubGhzNotificationStateRx;
|
||||
subghz_block_generic_global.endless_tx = false;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if(event.event == SubGhzCustomEventSceneReceiverInfoTxStop) {
|
||||
//CC1101 Stop Tx -> next tick event Start RX
|
||||
// user release OK
|
||||
// we switch off endless_tx - that mean protocols yield finish endless transmission,
|
||||
// send upload "repeat=xx" times, and after will be stoped by the tick event down in this code
|
||||
subghz->state_notifications = SubGhzNotificationStateTxWait;
|
||||
subghz_block_generic_global.endless_tx = false;
|
||||
|
||||
return true;
|
||||
|
||||
} else if(event.event == SubGhzCustomEventSceneReceiverInfoSave) {
|
||||
//CC1101 Stop RX -> Save
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
subghz_txrx_hopper_set_state(subghz->txrx, SubGhzHopperStateOFF);
|
||||
|
||||
subghz_txrx_stop(subghz->txrx);
|
||||
if(!subghz_scene_receiver_info_update_parser(subghz)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(subghz_txrx_protocol_is_serializable(subghz->txrx)) {
|
||||
subghz_file_name_clear(subghz);
|
||||
|
||||
subghz->save_datetime =
|
||||
subghz_history_get_datetime(subghz->history, subghz->idx_menu_chosen);
|
||||
subghz->save_datetime_set = true;
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if(event.event == SubGhzCustomEventSceneReceiverInfoTxFullDpad) {
|
||||
if(!subghz_scene_receiver_info_update_parser(subghz)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FlipperFormat* fff_history =
|
||||
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen);
|
||||
FlipperFormat* fff_data = subghz_txrx_get_fff_data(subghz->txrx);
|
||||
|
||||
Stream* src = flipper_format_get_raw_stream(fff_history);
|
||||
Stream* dst = flipper_format_get_raw_stream(fff_data);
|
||||
|
||||
stream_seek(src, 0, StreamOffsetFromStart);
|
||||
stream_clean(dst);
|
||||
stream_copy_full(src, dst);
|
||||
stream_seek(dst, 0, StreamOffsetFromStart);
|
||||
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
|
||||
return true;
|
||||
}
|
||||
|
||||
} else if(event.type == SceneManagerEventTypeTick) {
|
||||
if(subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF) {
|
||||
subghz_txrx_hopper_update(subghz->txrx, subghz->last_settings->hopping_threshold);
|
||||
@@ -193,18 +209,15 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
|
||||
subghz->state_notifications = SubGhzNotificationStateRx;
|
||||
break;
|
||||
case SubGhzNotificationStateTxWait:
|
||||
// we wait until hardware TX finished and after stop TX and start RX, else just blink led
|
||||
if(!subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
|
||||
notification_message(subghz->notifications, &sequence_blink_magenta_10);
|
||||
} else {
|
||||
subghz_txrx_stop(subghz->txrx);
|
||||
// update screen
|
||||
widget_reset(subghz->widget);
|
||||
subghz_scene_receiver_info_draw_widget(subghz);
|
||||
|
||||
subghz->state_notifications = SubGhzNotificationStateIDLE;
|
||||
|
||||
if(!scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneDecodeRAW)) {
|
||||
if(!scene_manager_has_previous_scene(
|
||||
subghz->scene_manager, SubGhzSceneDecodeRAW)) {
|
||||
subghz_txrx_rx_start(subghz->txrx);
|
||||
subghz_txrx_hopper_unpause(subghz->txrx);
|
||||
if(!subghz_history_get_text_space_left(subghz->history, NULL)) {
|
||||
@@ -222,7 +235,6 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
|
||||
|
||||
void subghz_scene_receiver_info_on_exit(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
|
||||
widget_reset(subghz->widget);
|
||||
subghz_txrx_reset_dynamic_and_custom_btns(subghz->txrx);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ Navigate with **Up/Down**, activate with **OK**, close with **Back**.
|
||||
| **Press START** | Sends a Start press to the game (pause menus, "PRESS START" screens) and resumes |
|
||||
| **Press SELECT** | Sends a Select press to the game and resumes |
|
||||
| **Frameskip** | Change with **Left/Right**: `auto` (recommended), or fixed `0–4`. `auto` shows the skip level currently in use |
|
||||
| **Sound** | Toggle piezo sound on/off (`n/a` if the speaker is in use by another app) |
|
||||
| **Sound** | Volume: cycle off/25/50/75/100% with **Left/Right**; **OK** toggles mute/full (`n/a` if the speaker is in use by another app) |
|
||||
| **Save SRAM** | Writes the cartridge battery save (`.sav`) to the SD card immediately |
|
||||
| **Exit** | Saves SRAM (if the cartridge has a battery) and quits the app |
|
||||
|
||||
@@ -95,8 +95,9 @@ piezo:
|
||||
3. otherwise the noise channel, mapped to a short low buzz (percussion).
|
||||
|
||||
The result is a monophonic ringtone-style rendition of the game's music
|
||||
and sound effects (sweeps like Mario's jump work). It can be toggled in
|
||||
the emulator menu. Since no waveforms are synthesized, the CPU cost is
|
||||
and sound effects (sweeps like Mario's jump work). Volume is adjustable in
|
||||
the emulator menu (off/25/50/75/100%, perceptually spaced for the piezo's
|
||||
nonlinear loudness). Since no waveforms are synthesized, the CPU cost is
|
||||
negligible (one counter per emulated instruction) and RAM cost is ~120
|
||||
bytes.
|
||||
|
||||
@@ -108,8 +109,31 @@ bytes.
|
||||
(70224 M-cycles instead of 17556). Unnoticeable on a desktop, a slideshow
|
||||
on a 64 MHz Cortex-M4. Fixed — this alone made everything ~4.5x faster.
|
||||
- While halted (games spend most of each frame in HALT waiting for vblank),
|
||||
the CPU steps 4 M-cycles at a time instead of 1, making the idle part of
|
||||
the CPU steps 8 M-cycles at a time instead of 1, making the idle part of
|
||||
the frame cheap.
|
||||
- PPU/timer/APU use catch-up batching: instead of stepping their state
|
||||
machines after every CPU instruction, cycles accumulate and are flushed
|
||||
every 32 M-cycles - or immediately before any IO register access, so
|
||||
LY/STAT/DIV/TIMA/IF always read exact. This removes the single biggest
|
||||
per-instruction cost of the interpreter loop (~2.8x faster emulation).
|
||||
- Undefined opcodes cost 1 cycle instead of 0: with 0-cycle entries,
|
||||
executing garbage could advance the PC without advancing the PPU clock
|
||||
and spin run_to_vblank() forever, freezing the whole device.
|
||||
- EI enables interrupts only after the following instruction (hardware
|
||||
behaviour). Without the delay, the classic `EI / HALT` pause idiom
|
||||
consumed its wake interrupt before HALT and games froze waiting for a
|
||||
button press that had already been eaten (input dead, music playing).
|
||||
- STOP freezes the CPU until joypad activity, independent of IE/IF/IME
|
||||
(hardware behaviour), and skips its padding byte.
|
||||
- ROM streaming: bank switches to the already-mapped bank skip the cache;
|
||||
the upper 8 KB half of a bank streams in lazily on first read; the LRU
|
||||
never evicts the most-recently-used page and protects the single hottest
|
||||
page (the music-driver bank) from map-streaming evictions; the Gameboy
|
||||
core block is pre-allocated during ROM load so the page cache can size
|
||||
itself against real free heap (Pokemon: ~2x more cache slots).
|
||||
- The auto-frameskip EMA uses signed arithmetic (an unsigned underflow
|
||||
could pin it at maximum), and the menu shows the measured cost of one
|
||||
emulated frame in ms next to the free heap (16.7ms = full speed).
|
||||
- **Frameskip `auto`** (default) measures the real cost of each emulated frame
|
||||
and skips *rendering* (never emulation) to keep the game running at correct
|
||||
speed. Games stay full-speed logically; visible FPS drops instead.
|
||||
@@ -117,8 +141,26 @@ bytes.
|
||||
- The PPU only renders the 64 scanlines (out of 144) that survive the
|
||||
downscale to the Flipper LCD — ~55% of the per-frame rendering work is
|
||||
skipped with zero visual difference.
|
||||
- Bank-switch heavy games may micro-stutter when a 16 KB bank has to be
|
||||
streamed from the SD card (only happens when the ROM doesn't fit in RAM).
|
||||
- ROM streaming works on 8 KB pages (half a MBC bank): twice the cache
|
||||
slots per KB of heap and half the SD stall per miss compared to whole-bank
|
||||
caching. Bank-switch heavy games may still micro-stutter on a cache miss
|
||||
(only happens when the ROM doesn't fit in RAM).
|
||||
- The emulator core is compiled `-O2` (the rest of the app stays `-Os`) and
|
||||
the per-instruction hot path (opcode fetch, PPU/timer/APU ticks) is
|
||||
manually inlined across the core, cutting per-instruction call overhead.
|
||||
- The CPU interpreter is a unity build: the 500 opcode bodies compile in
|
||||
the same translation unit as the dispatch switch, so the compiler inlines
|
||||
them directly into the jump table (two cross-TU calls per instruction
|
||||
eliminated) - and the deduplication actually made the binary smaller.
|
||||
- The PPU renders tiles byte-wise, not pixel-wise: tile bitplanes are
|
||||
decoded 8 pixels at once through a 256-entry spread LUT and written as
|
||||
whole palette-mapped bytes (4 px per store) - ~7x fewer operations per
|
||||
scanline. Sprites skip fully-transparent rows and use the same LUTs.
|
||||
- The 160x144 -> 128x64 downscale/dither also works one packed byte
|
||||
(4 pixels) per lookup instead of per-pixel shade extraction.
|
||||
- In `auto` mode the frameskip may go up to 8 (fixed settings stay 0-4):
|
||||
for heavy streamed games, correct game speed at a lower visible fps beats
|
||||
slow motion.
|
||||
- The emulator menu shows the free heap (`NNk free`) so you can see the
|
||||
memory headroom of the current game at a glance.
|
||||
|
||||
@@ -149,8 +191,8 @@ ufbt launch # builds, installs and runs on a connected Flipper
|
||||
The exact core that ships in the FAP can be compiled and tested on a desktop:
|
||||
|
||||
```sh
|
||||
g++ -std=c++17 -O2 -fno-exceptions -fno-rtti -I gb \
|
||||
-o hosttest/hosttest hosttest/main.cpp gb/*.cc
|
||||
g++ -std=c++17 -O2 -fno-exceptions -fno-rtti -I lib/gbcore \
|
||||
-o hosttest/hosttest hosttest/main.cpp lib/gbcore/*.cc
|
||||
|
||||
# Blargg CPU tests (print Passed/Failed via the serial port):
|
||||
./hosttest/hosttest path/to/01-special.gb 4000
|
||||
@@ -170,8 +212,8 @@ Current status: **Blargg `cpu_instrs` 11/11 PASS**.
|
||||
|
||||
| Upstream (PC) | This port (Flipper) |
|
||||
|---|---|
|
||||
| Whole ROM in RAM (with several transient copies) | 16 KB bank streaming from SD with an adaptive LRU cache; bank 0 resident; when every bank fits, the whole ROM is preloaded into individual 16 KB slots (O(1) switching, SD file closed) |
|
||||
| — | All ROM-dependent allocations are 16 KB or smaller and are checked against the largest free heap block first: heap fragmentation can never crash the firmware, the app degrades to streaming or shows "Not enough RAM" instead |
|
||||
| Whole ROM in RAM (with several transient copies) | 8 KB page streaming from SD with an adaptive LRU cache; bank 0 resident; when every page fits, the whole ROM is preloaded into individual 8 KB slots (O(1) switching, SD file closed) |
|
||||
| — | All ROM-dependent allocations are 8 KB or smaller and are checked against the largest free heap block first: heap fragmentation can never crash the firmware, the app degrades to streaming or shows "Not enough RAM" instead |
|
||||
| Renders all 144 scanlines | Renders only the 64 scanlines that are actually displayed after the 144→64 downscale (row mask, ~2x faster rendering) |
|
||||
| PPU counts M-cycles against T-cycle constants (4x too much CPU emulation per frame) | Hardware-correct M-cycle constants (114 per scanline): ~4.5x faster overall |
|
||||
| DIV register incremented every M-cycle (64x too fast) | Correct 16384 Hz rate (games use DIV for delays and randomness) |
|
||||
@@ -187,14 +229,14 @@ Current status: **Blargg `cpu_instrs` 11/11 PASS**.
|
||||
| No APU at all | Register-level APU (sweep/envelope/length/NR52, proper read-back masks) driving the piezo with the dominant voice |
|
||||
|
||||
RAM budget on device (256 KB total, ~140 KB heap; the app binary itself
|
||||
loads into ~32 KB of that heap): ~19.5 KB emulation state, 16 KB bank 0,
|
||||
adaptive bank cache (10 KB heap kept in reserve for the system), 0–32 KB
|
||||
cartridge RAM per game, 4 KB stack. The bank cache is allocated greedily in
|
||||
independent 16 KB blocks until the reserve would be touched, so any `.gb`
|
||||
loads into ~37 KB of that heap): ~19.5 KB emulation state, 16 KB bank 0,
|
||||
adaptive page cache (10 KB heap kept in reserve for the system), 0–32 KB
|
||||
cartridge RAM per game, 4 KB stack. The page cache is allocated greedily in
|
||||
independent 8 KB blocks until the reserve would be touched, so any `.gb`
|
||||
ROM size works: small ROMs end up fully resident, large ones stream through
|
||||
however many slots fit. Worst case (1 MB ROM + 32 KB battery RAM, e.g.
|
||||
Pokémon Red/Blue) needs ~78 KB before the first cache slot, which fits the
|
||||
post-launch heap with room for 1–2 streaming slots.
|
||||
post-launch heap with room for a few streaming pages.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -3,10 +3,21 @@ App(
|
||||
name="FlipGB",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="flipgb_app",
|
||||
sources=["*.c*", "!hosttest"],
|
||||
sources=["*.c*", "!hosttest", "!lib"],
|
||||
requires=["gui", "dialogs", "storage"],
|
||||
stack_size=8 * 1024,
|
||||
cdefines=[("GB_FB_ROWS", "64")],
|
||||
fap_private_libs=[
|
||||
Lib(
|
||||
# The emulator core is built as a private lib so it can use -O2:
|
||||
# ufbt builds FAP sources with -Os, which costs ~30% emulation
|
||||
# speed in the per-instruction hot path. Code size difference
|
||||
# is ~6 KB - well worth it.
|
||||
name="gbcore",
|
||||
cflags=["-O2"],
|
||||
cdefines=[("GB_FB_ROWS", "64")],
|
||||
),
|
||||
],
|
||||
fap_category="Games",
|
||||
fap_icon="flipgb_icon.png",
|
||||
fap_author="user",
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
@@ -1,73 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "input.h"
|
||||
#include "cpu.h"
|
||||
#include "video.h"
|
||||
#include "timer.h"
|
||||
#include "mmu.h"
|
||||
#include "cartridge.h"
|
||||
#include "apu.h"
|
||||
|
||||
class Gameboy {
|
||||
public:
|
||||
/* bank0: pointer to the first 16 KB of ROM (stays resident).
|
||||
* provider: returns pointers to 16 KB switchable banks. */
|
||||
Gameboy(
|
||||
const u8* bank0,
|
||||
uint rom_bank_count,
|
||||
MBCType mbc,
|
||||
u8* cart_ram,
|
||||
u32 cart_ram_size,
|
||||
RomBankProvider provider,
|
||||
void* provider_ctx);
|
||||
|
||||
/* Runs the emulator until the next vblank (one full frame). */
|
||||
void run_to_vblank();
|
||||
|
||||
/* Called on every vblank BEFORE the framebuffer is cleared for the next
|
||||
* frame: this is where the frontend must convert/copy the image. */
|
||||
void set_frame_callback(void (*cb)(void*), void* ctx) {
|
||||
user_frame_cb = cb;
|
||||
user_frame_ctx = ctx;
|
||||
}
|
||||
|
||||
void button_pressed(GbButton button);
|
||||
void button_released(GbButton button);
|
||||
|
||||
void set_skip_render(bool skip) { video.skip_render = skip; }
|
||||
|
||||
/* Optional display-line mask (see Video::row_mask). The array must stay
|
||||
* valid for the lifetime of the emulator. */
|
||||
void set_row_mask(const u8* mask) { video.set_row_mask(mask); }
|
||||
auto get_framebuffer() const -> const FrameBuffer& { return video.get_framebuffer(); }
|
||||
|
||||
auto get_cartridge_ram() -> u8* { return cartridge.get_ram(); }
|
||||
auto get_cartridge_ram_size() const -> u32 { return cartridge.get_ram_size(); }
|
||||
|
||||
Cartridge cartridge;
|
||||
|
||||
CPU cpu;
|
||||
friend class CPU;
|
||||
|
||||
Video video;
|
||||
friend class Video;
|
||||
|
||||
MMU mmu;
|
||||
friend class MMU;
|
||||
|
||||
Timer timer;
|
||||
friend class Timer;
|
||||
|
||||
Apu apu;
|
||||
|
||||
Input input;
|
||||
|
||||
private:
|
||||
void tick();
|
||||
|
||||
static void vblank_trampoline(void* ctx);
|
||||
volatile bool frame_done = false;
|
||||
|
||||
void (*user_frame_cb)(void*) = nullptr;
|
||||
void* user_frame_ctx = nullptr;
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
#include "timer.h"
|
||||
|
||||
#include "definitions.h"
|
||||
#include "gameboy.h"
|
||||
#include "cpu.h"
|
||||
#include "bitwise.h"
|
||||
|
||||
const uint CLOCKS_PER_CYCLE = 4;
|
||||
|
||||
Timer::Timer(Gameboy& _gb) : gb(_gb) {}
|
||||
|
||||
void Timer::tick(uint cycles) {
|
||||
/* DIV increments at 16384 Hz = every 64 M-cycles (upstream incremented
|
||||
* it once per M-cycle: 64x too fast, breaking games that use DIV for
|
||||
* delays or randomness) */
|
||||
div_clocks += cycles;
|
||||
if(div_clocks >= 64) {
|
||||
divider.set(static_cast<u8>(divider.value() + (div_clocks >> 6)));
|
||||
div_clocks &= 63;
|
||||
}
|
||||
|
||||
clocks += cycles * CLOCKS_PER_CYCLE;
|
||||
|
||||
auto timer_is_on = timer_control.check_bit(2);
|
||||
if (timer_is_on == 0) { return; }
|
||||
|
||||
auto clock_limit = clocks_needed_to_increment();
|
||||
|
||||
if (clocks >= clock_limit) {
|
||||
clocks = clocks % clock_limit;
|
||||
|
||||
u8 old_timer_counter = timer_counter.value();
|
||||
timer_counter.increment();
|
||||
|
||||
if (timer_counter.value() < old_timer_counter) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(2, true);
|
||||
timer_counter.set(timer_modulo.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Timer::get_divider() const -> u8 { return divider.value(); }
|
||||
|
||||
auto Timer::get_timer() const -> u8 { return timer_counter.value(); }
|
||||
|
||||
auto Timer::get_timer_modulo() const -> u8 { return timer_modulo.value(); }
|
||||
|
||||
// Only the bottom three bits of this register are usable
|
||||
auto Timer::get_timer_control() const -> u8 { return timer_control.value() & 0x3; }
|
||||
|
||||
void Timer::reset_divider() {
|
||||
divider.set(0x0);
|
||||
}
|
||||
|
||||
void Timer::set_timer(u8 value) {
|
||||
timer_counter.set(value);
|
||||
}
|
||||
|
||||
void Timer::set_timer_modulo(u8 value) {
|
||||
timer_modulo.set(value);
|
||||
}
|
||||
|
||||
void Timer::set_timer_control(u8 value) {
|
||||
timer_control.set(value);
|
||||
}
|
||||
|
||||
uint Timer::clocks_needed_to_increment() {
|
||||
using bitwise::check_bit;
|
||||
|
||||
switch (get_timer_control()) {
|
||||
case 0: return CLOCK_RATE / 4096;
|
||||
case 1: return CLOCK_RATE / 262144;
|
||||
case 2: return CLOCK_RATE / 65536;
|
||||
case 3: return CLOCK_RATE / 16384;
|
||||
default: return CLOCK_RATE / 4096; /* unreachable */
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
#include "register.h"
|
||||
|
||||
class Gameboy;
|
||||
|
||||
class Timer {
|
||||
public:
|
||||
Timer(Gameboy& inGb);
|
||||
|
||||
void tick(uint cycles);
|
||||
|
||||
auto get_divider() const -> u8;
|
||||
auto get_timer() const -> u8;
|
||||
auto get_timer_modulo() const -> u8;
|
||||
auto get_timer_control() const -> u8;
|
||||
|
||||
void reset_divider();
|
||||
void set_timer(u8 value);
|
||||
void set_timer_modulo(u8 value);
|
||||
void set_timer_control(u8 value);
|
||||
|
||||
private:
|
||||
uint clocks_needed_to_increment();
|
||||
|
||||
uint clocks = 0;
|
||||
uint div_clocks = 0;
|
||||
|
||||
Gameboy& gb;
|
||||
|
||||
ByteRegister divider;
|
||||
ByteRegister timer_counter;
|
||||
|
||||
ByteRegister timer_modulo;
|
||||
ByteRegister timer_control;
|
||||
};
|
||||
@@ -1,353 +0,0 @@
|
||||
#include "video.h"
|
||||
|
||||
#include "gameboy.h"
|
||||
#include "cpu.h"
|
||||
#include "bitwise.h"
|
||||
|
||||
using bitwise::check_bit;
|
||||
|
||||
Video::Video(Gameboy& inGb)
|
||||
: gb(inGb) {
|
||||
}
|
||||
|
||||
void Video::tick(Cycles cycles) {
|
||||
cycle_counter += cycles.cycles;
|
||||
|
||||
switch(current_mode) {
|
||||
case VideoMode::ACCESS_OAM:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE_OAM) {
|
||||
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE_OAM;
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, true);
|
||||
current_mode = VideoMode::ACCESS_VRAM;
|
||||
}
|
||||
break;
|
||||
case VideoMode::ACCESS_VRAM:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE_VRAM) {
|
||||
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE_VRAM;
|
||||
current_mode = VideoMode::HBLANK;
|
||||
|
||||
bool hblank_interrupt = check_bit(lcd_status.value(), 3);
|
||||
|
||||
if(hblank_interrupt) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(1, true);
|
||||
}
|
||||
|
||||
bool ly_coincidence_interrupt = check_bit(lcd_status.value(), 6);
|
||||
bool ly_coincidence = ly_compare.value() == line.value();
|
||||
if(ly_coincidence_interrupt && ly_coincidence) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(1, true);
|
||||
}
|
||||
lcd_status.set_bit_to(2, ly_coincidence);
|
||||
|
||||
lcd_status.set_bit_to(1, false);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
}
|
||||
break;
|
||||
case VideoMode::HBLANK:
|
||||
if(cycle_counter >= CLOCKS_PER_HBLANK) {
|
||||
if(!skip_render) write_scanline(line.value());
|
||||
line.increment();
|
||||
|
||||
cycle_counter = cycle_counter % CLOCKS_PER_HBLANK;
|
||||
|
||||
/* Line 145 (index 144) is the first line of VBLANK */
|
||||
if(line == 144) {
|
||||
current_mode = VideoMode::VBLANK;
|
||||
lcd_status.set_bit_to(1, false);
|
||||
lcd_status.set_bit_to(0, true);
|
||||
gb.cpu.interrupt_flag.set_bit_to(0, true);
|
||||
} else {
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
current_mode = VideoMode::ACCESS_OAM;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case VideoMode::VBLANK:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE) {
|
||||
line.increment();
|
||||
|
||||
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE;
|
||||
|
||||
/* Line 155 (index 154) is the last line */
|
||||
if(line == 154) {
|
||||
if(!skip_render) {
|
||||
write_sprites();
|
||||
draw();
|
||||
buffer.reset();
|
||||
} else {
|
||||
draw(); /* still notify the frontend for pacing */
|
||||
}
|
||||
line.reset();
|
||||
current_mode = VideoMode::ACCESS_OAM;
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto Video::display_enabled() const -> bool {
|
||||
return check_bit(control_byte, 7);
|
||||
}
|
||||
auto Video::window_tile_map() const -> bool {
|
||||
return check_bit(control_byte, 6);
|
||||
}
|
||||
auto Video::window_enabled() const -> bool {
|
||||
return check_bit(control_byte, 5);
|
||||
}
|
||||
auto Video::bg_window_tile_data() const -> bool {
|
||||
return check_bit(control_byte, 4);
|
||||
}
|
||||
auto Video::bg_tile_map_display() const -> bool {
|
||||
return check_bit(control_byte, 3);
|
||||
}
|
||||
auto Video::sprite_size() const -> bool {
|
||||
return check_bit(control_byte, 2);
|
||||
}
|
||||
auto Video::sprites_enabled() const -> bool {
|
||||
return check_bit(control_byte, 1);
|
||||
}
|
||||
auto Video::bg_enabled() const -> bool {
|
||||
return check_bit(control_byte, 0);
|
||||
}
|
||||
|
||||
void Video::write_scanline(u8 current_line) {
|
||||
if(!display_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Lines the frontend never displays are not worth rendering */
|
||||
if(row_mask && current_line < GAMEBOY_HEIGHT && !row_mask[current_line]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(bg_enabled()) {
|
||||
draw_bg_line(current_line);
|
||||
}
|
||||
|
||||
if(window_enabled()) {
|
||||
draw_window_line(current_line);
|
||||
}
|
||||
}
|
||||
|
||||
void Video::write_sprites() {
|
||||
if(!sprites_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(uint sprite_n = 0; sprite_n < 40; sprite_n++) {
|
||||
draw_sprite(sprite_n);
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw_bg_line(uint current_line) {
|
||||
/* Note: tileset two uses signed numbering to share half the tiles with
|
||||
* tileset one */
|
||||
bool use_tile_set_zero = bg_window_tile_data();
|
||||
bool use_tile_map_zero = !bg_tile_map_display();
|
||||
|
||||
Palette palette = load_palette(bg_palette);
|
||||
|
||||
u16 tile_set_address = use_tile_set_zero ? TILE_SET_ZERO_ADDRESS : TILE_SET_ONE_ADDRESS;
|
||||
u16 tile_map_address = use_tile_map_zero ? TILE_MAP_ZERO_ADDRESS : TILE_MAP_ONE_ADDRESS;
|
||||
|
||||
uint screen_y = current_line;
|
||||
uint scrolled_y = (screen_y + scroll_y.value()) % BG_MAP_SIZE;
|
||||
uint tile_y = scrolled_y / TILE_HEIGHT_PX;
|
||||
uint tile_pixel_y = scrolled_y % TILE_HEIGHT_PX;
|
||||
uint tile_data_line_offset = tile_pixel_y * 2;
|
||||
|
||||
/* Render tile-by-tile instead of refetching the tile data for every
|
||||
* pixel like upstream did */
|
||||
uint scroll_x_val = scroll_x.value();
|
||||
|
||||
uint screen_x = 0;
|
||||
while(screen_x < GAMEBOY_WIDTH) {
|
||||
uint scrolled_x = (screen_x + scroll_x_val) % BG_MAP_SIZE;
|
||||
uint tile_x = scrolled_x / TILE_WIDTH_PX;
|
||||
uint tile_pixel_x = scrolled_x % TILE_WIDTH_PX;
|
||||
|
||||
uint tile_index = tile_y * TILES_PER_LINE + tile_x;
|
||||
u8 tile_id = video_ram[tile_map_address - 0x8000 + tile_index];
|
||||
|
||||
uint tile_data_mem_offset = use_tile_set_zero ?
|
||||
tile_id * TILE_BYTES :
|
||||
static_cast<uint>(
|
||||
(static_cast<s8>(tile_id) + 128)) *
|
||||
TILE_BYTES;
|
||||
|
||||
uint line_addr = (tile_set_address - 0x8000) + tile_data_mem_offset +
|
||||
tile_data_line_offset;
|
||||
|
||||
u8 pixels_1 = video_ram[line_addr];
|
||||
u8 pixels_2 = video_ram[line_addr + 1];
|
||||
|
||||
/* Draw the remainder of this tile's row */
|
||||
for(uint px = tile_pixel_x; px < TILE_WIDTH_PX && screen_x < GAMEBOY_WIDTH;
|
||||
px++, screen_x++) {
|
||||
u8 pixel_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(px));
|
||||
buffer.set_pixel(screen_x, screen_y, get_shade_from_palette(pixel_color, palette));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw_window_line(uint current_line) {
|
||||
bool use_tile_set_zero = bg_window_tile_data();
|
||||
bool use_tile_map_zero = !window_tile_map();
|
||||
|
||||
Palette palette = load_palette(bg_palette);
|
||||
|
||||
u16 tile_set_address = use_tile_set_zero ? TILE_SET_ZERO_ADDRESS : TILE_SET_ONE_ADDRESS;
|
||||
u16 tile_map_address = use_tile_map_zero ? TILE_MAP_ZERO_ADDRESS : TILE_MAP_ONE_ADDRESS;
|
||||
|
||||
uint screen_y = current_line;
|
||||
uint scrolled_y = screen_y - window_y.value();
|
||||
|
||||
if(scrolled_y >= GAMEBOY_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint tile_y = scrolled_y / TILE_HEIGHT_PX;
|
||||
uint tile_pixel_y = scrolled_y % TILE_HEIGHT_PX;
|
||||
uint tile_data_line_offset = tile_pixel_y * 2;
|
||||
|
||||
for(uint screen_x = 0; screen_x < GAMEBOY_WIDTH; screen_x++) {
|
||||
uint scrolled_x = screen_x + window_x.value() - 7;
|
||||
|
||||
uint tile_x = scrolled_x / TILE_WIDTH_PX;
|
||||
uint tile_pixel_x = scrolled_x % TILE_WIDTH_PX;
|
||||
|
||||
uint tile_index = tile_y * TILES_PER_LINE + tile_x;
|
||||
if(tile_index >= 32 * 32) continue;
|
||||
|
||||
u8 tile_id = video_ram[tile_map_address - 0x8000 + tile_index];
|
||||
|
||||
uint tile_data_mem_offset = use_tile_set_zero ?
|
||||
tile_id * TILE_BYTES :
|
||||
static_cast<uint>(
|
||||
(static_cast<s8>(tile_id) + 128)) *
|
||||
TILE_BYTES;
|
||||
|
||||
uint line_addr = (tile_set_address - 0x8000) + tile_data_mem_offset +
|
||||
tile_data_line_offset;
|
||||
|
||||
u8 pixels_1 = video_ram[line_addr];
|
||||
u8 pixels_2 = video_ram[line_addr + 1];
|
||||
|
||||
u8 pixel_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(tile_pixel_x));
|
||||
buffer.set_pixel(screen_x, screen_y, get_shade_from_palette(pixel_color, palette));
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw_sprite(const uint sprite_n) {
|
||||
/* Each sprite is represented by 4 bytes */
|
||||
u16 oam_start = static_cast<u16>(sprite_n * SPRITE_BYTES);
|
||||
|
||||
u8 sprite_y = gb.mmu.oam_ram[oam_start];
|
||||
u8 sprite_x = gb.mmu.oam_ram[oam_start + 1];
|
||||
|
||||
/* Offscreen sprites are not drawn */
|
||||
if(sprite_y == 0 || sprite_y >= 160) {
|
||||
return;
|
||||
}
|
||||
if(sprite_x == 0 || sprite_x >= 168) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint sprite_height = sprite_size() ? 16 : 8;
|
||||
|
||||
u8 pattern_n = gb.mmu.oam_ram[oam_start + 2];
|
||||
u8 sprite_attrs = gb.mmu.oam_ram[oam_start + 3];
|
||||
|
||||
/* Bits 0-3 are used only for CGB */
|
||||
bool use_palette_1 = check_bit(sprite_attrs, 4);
|
||||
bool flip_x = check_bit(sprite_attrs, 5);
|
||||
bool flip_y = check_bit(sprite_attrs, 6);
|
||||
bool obj_behind_bg = check_bit(sprite_attrs, 7);
|
||||
|
||||
Palette palette = use_palette_1 ? load_palette(sprite_palette_1) :
|
||||
load_palette(sprite_palette_0);
|
||||
|
||||
uint tile_offset = pattern_n * TILE_BYTES;
|
||||
|
||||
int start_y = sprite_y - 16;
|
||||
int start_x = sprite_x - 8;
|
||||
|
||||
for(uint y = 0; y < sprite_height; y++) {
|
||||
int screen_y = start_y + static_cast<int>(y);
|
||||
if(screen_y < 0 || screen_y >= static_cast<int>(GAMEBOY_HEIGHT)) continue;
|
||||
if(row_mask && !row_mask[screen_y]) continue;
|
||||
|
||||
uint src_y = !flip_y ? y : sprite_height - y - 1;
|
||||
|
||||
uint line_addr = tile_offset + src_y * 2; /* relative to tile set zero */
|
||||
u8 pixels_1 = video_ram[line_addr];
|
||||
u8 pixels_2 = video_ram[line_addr + 1];
|
||||
|
||||
for(uint x = 0; x < TILE_WIDTH_PX; x++) {
|
||||
int screen_x = start_x + static_cast<int>(x);
|
||||
if(screen_x < 0 || screen_x >= static_cast<int>(GAMEBOY_WIDTH)) continue;
|
||||
|
||||
uint src_x = !flip_x ? x : TILE_WIDTH_PX - x - 1;
|
||||
|
||||
u8 gb_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(src_x));
|
||||
|
||||
/* Color 0 is transparent */
|
||||
if(gb_color == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Shade existing_pixel = buffer.get_pixel(
|
||||
static_cast<uint>(screen_x), static_cast<uint>(screen_y));
|
||||
|
||||
/* Note: same behaviour as upstream - compares the final shade
|
||||
* rather than the logical color 0 */
|
||||
if(obj_behind_bg && existing_pixel != SHADE_WHITE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.set_pixel(
|
||||
static_cast<uint>(screen_x),
|
||||
static_cast<uint>(screen_y),
|
||||
get_shade_from_palette(gb_color, palette));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Video::get_pixel_from_line(u8 byte1, u8 byte2, u8 pixel_index) -> u8 {
|
||||
using bitwise::bit_value;
|
||||
|
||||
return static_cast<u8>(
|
||||
(bit_value(byte2, 7 - pixel_index) << 1) | bit_value(byte1, 7 - pixel_index));
|
||||
}
|
||||
|
||||
auto Video::load_palette(const ByteRegister& palette_register) -> Palette {
|
||||
u8 v = palette_register.value();
|
||||
|
||||
Palette palette;
|
||||
palette.color0 = static_cast<Shade>(v & 0x3);
|
||||
palette.color1 = static_cast<Shade>((v >> 2) & 0x3);
|
||||
palette.color2 = static_cast<Shade>((v >> 4) & 0x3);
|
||||
palette.color3 = static_cast<Shade>((v >> 6) & 0x3);
|
||||
return palette;
|
||||
}
|
||||
|
||||
auto Video::get_shade_from_palette(u8 color, const Palette& palette) -> Shade {
|
||||
switch(color) {
|
||||
case 0:
|
||||
return palette.color0;
|
||||
case 1:
|
||||
return palette.color1;
|
||||
case 2:
|
||||
return palette.color2;
|
||||
default:
|
||||
return palette.color3;
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw() {
|
||||
if(vblank_callback) vblank_callback(vblank_ctx);
|
||||
}
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
* Usage: hosttest <rom.gb> [max_frames] [--dump-frame N]
|
||||
*/
|
||||
|
||||
#include "../gb/gameboy.h"
|
||||
#include "../lib/gbcore/gameboy.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -29,9 +29,9 @@ static void serial_hook(u8 byte) {
|
||||
}
|
||||
}
|
||||
|
||||
static const u8* bank_provider(void* /*ctx*/, uint bank) {
|
||||
long offset = static_cast<long>(bank) * 0x4000;
|
||||
if(offset + 0x4000 > g_rom_size) offset = 0;
|
||||
static const u8* bank_provider(void* /*ctx*/, uint page) {
|
||||
long offset = static_cast<long>(page) * 0x2000; /* 8 KB ROM pages */
|
||||
if(offset + 0x2000 > g_rom_size) offset = 0;
|
||||
return g_rom + offset;
|
||||
}
|
||||
|
||||
@@ -98,10 +98,12 @@ int main(int argc, char** argv) {
|
||||
bool dump = false;
|
||||
bool rowmask = false;
|
||||
bool dump_audio = false;
|
||||
bool skiprender = false;
|
||||
for(int i = 1; i < argc; i++) {
|
||||
if(!strcmp(argv[i], "--dump-frame")) dump = true;
|
||||
if(!strcmp(argv[i], "--rowmask")) rowmask = true;
|
||||
if(!strcmp(argv[i], "--dump-audio")) dump_audio = true;
|
||||
if(!strcmp(argv[i], "--skiprender")) skiprender = true;
|
||||
}
|
||||
|
||||
/* Same 144 -> 64 line subsampling the Flipper frontend uses */
|
||||
@@ -146,6 +148,7 @@ int main(int argc, char** argv) {
|
||||
auto* gb = new Gameboy(g_rom, banks, mbc, cart_ram, ram_size, bank_provider, nullptr);
|
||||
gb->set_frame_callback(frame_hook, gb);
|
||||
if(rowmask) gb->set_row_mask(mask);
|
||||
if(skiprender) gb->set_skip_render(true);
|
||||
|
||||
u32 last_freq = 0;
|
||||
int last_ch = -1;
|
||||
|
||||
+4
-10
@@ -1,15 +1,9 @@
|
||||
#include "apu.h"
|
||||
|
||||
/* One frame-sequencer step every 2048 M-cycles (8192 T-cycles = 512 Hz),
|
||||
* in the same M-cycle domain the CPU/PPU/timer now share. */
|
||||
static const uint FRAME_SEQ_PERIOD = 2048;
|
||||
|
||||
void Apu::tick(uint cycles) {
|
||||
if(!power) return;
|
||||
|
||||
seq_counter += cycles;
|
||||
while(seq_counter >= FRAME_SEQ_PERIOD) {
|
||||
seq_counter -= FRAME_SEQ_PERIOD;
|
||||
/* Slow path of Apu::tick (see apu.h): runs the due frame-sequencer steps */
|
||||
void Apu::seq_run() {
|
||||
while(seq_counter >= 2048) {
|
||||
seq_counter -= 2048;
|
||||
seq_step = (u8)((seq_step + 1) & 7);
|
||||
|
||||
if((seq_step & 1) == 0) clock_lengths(); /* 256 Hz */
|
||||
+9
-1
@@ -28,7 +28,14 @@ struct ApuVoice {
|
||||
|
||||
class Apu {
|
||||
public:
|
||||
void tick(uint cycles);
|
||||
/* Called once per emulated instruction; the fast path (powered off, or
|
||||
* no sequencer step due) must stay inline and branch-cheap. One
|
||||
* frame-sequencer step every 2048 M-cycles = 8192 T-cycles = 512 Hz. */
|
||||
void tick(uint cycles) {
|
||||
if(!power) return;
|
||||
seq_counter += cycles;
|
||||
if(seq_counter >= 2048) seq_run();
|
||||
}
|
||||
|
||||
/* 0xFF10 - 0xFF3F (sound registers + wave RAM) */
|
||||
auto read(u16 addr) const -> u8;
|
||||
@@ -81,6 +88,7 @@ private:
|
||||
}
|
||||
|
||||
void trigger(uint n);
|
||||
void seq_run();
|
||||
void clock_lengths();
|
||||
void clock_envelopes();
|
||||
void clock_sweep();
|
||||
+19
-1
@@ -21,6 +21,7 @@ void Cartridge::init(
|
||||
bank_low = 1;
|
||||
bank_high = 0;
|
||||
ram_bank = 0;
|
||||
cur_bank = 0xFFFFFFFFu;
|
||||
|
||||
update_rom_bank();
|
||||
}
|
||||
@@ -51,7 +52,24 @@ void Cartridge::update_rom_bank() {
|
||||
|
||||
if(bank_count) bank %= bank_count;
|
||||
|
||||
bankN = provider(provider_ctx, bank);
|
||||
/* Games (Pokemon's Bankswitch routine included) frequently rewrite the
|
||||
* bank register with the bank that is already mapped: skip the cache
|
||||
* lookups entirely in that case. */
|
||||
if(bank == cur_bank) return;
|
||||
cur_bank = bank;
|
||||
|
||||
/* map the lo 8 KB page now; the hi page streams in lazily on first
|
||||
* read from 0x6000-0x7FFF (Cartridge::read) */
|
||||
bankN_lo = provider(provider_ctx, bank * 2);
|
||||
bankN_hi = nullptr;
|
||||
}
|
||||
|
||||
void Cartridge::fetch_hi_page() const {
|
||||
/* Refresh the lo page's LRU stamp first so the hi fetch can never
|
||||
* evict it (the provider guarantees the two most recently returned
|
||||
* pages are safe when the cache has >= 2 slots). */
|
||||
bankN_lo = provider(provider_ctx, cur_bank * 2);
|
||||
bankN_hi = provider(provider_ctx, cur_bank * 2 + 1);
|
||||
}
|
||||
|
||||
void Cartridge::write(u16 addr, u8 value) {
|
||||
+23
-8
@@ -2,19 +2,23 @@
|
||||
|
||||
#include "definitions.h"
|
||||
|
||||
/* Cartridge with pluggable ROM bank provider.
|
||||
/* Cartridge with pluggable ROM page provider.
|
||||
*
|
||||
* Instead of holding the whole ROM in RAM (impossible on Flipper Zero for
|
||||
* anything above 32 KB), the cartridge asks the platform for a pointer to a
|
||||
* 16 KB bank whenever the game switches banks. On the desktop test build the
|
||||
* provider just returns `rom + bank * 0x4000`; on the Flipper it is backed
|
||||
* by an LRU cache streaming from the SD card.
|
||||
* anything above 32 KB), the cartridge asks the platform for pointers to
|
||||
* 8 KB ROM pages (page n = ROM offset n * 0x2000) whenever the game
|
||||
* switches banks. 8 KB granularity -- half a MBC bank -- doubles how many
|
||||
* cache slots fit in the same RAM and halves the SD stall of a cache miss,
|
||||
* which matters a lot for bank-switch heavy games (Pokemon switches banks
|
||||
* for music/code/data every frame). On the desktop test build the provider
|
||||
* just returns `rom + page * 0x2000`; on the Flipper it is backed by an
|
||||
* LRU cache streaming from the SD card.
|
||||
*
|
||||
* Supported mappers: ROM only, MBC1 (incl. upper bits / mode select),
|
||||
* MBC2 (built-in 512x4 RAM), MBC3 (no RTC), MBC5.
|
||||
*/
|
||||
|
||||
using RomBankProvider = const u8* (*)(void* ctx, uint bank);
|
||||
using RomBankProvider = const u8* (*)(void* ctx, uint page);
|
||||
|
||||
enum class MBCType : u8 {
|
||||
None,
|
||||
@@ -39,7 +43,15 @@ public:
|
||||
|
||||
auto read(u16 addr) const -> u8 {
|
||||
if(addr < 0x4000) return bank0[addr];
|
||||
if(addr < 0x8000) return bankN[addr - 0x4000];
|
||||
if(addr < 0x6000) return bankN_lo[addr - 0x4000];
|
||||
if(addr < 0x8000) {
|
||||
/* lazy: the hi half of a bank is only streamed in when the
|
||||
* game actually reads 0x6000-0x7FFF from it. Many switches
|
||||
* exist just to read a table at 0x4xxx; fetching both 8 KB
|
||||
* pages eagerly doubled the SD misses of streamed games. */
|
||||
if(!bankN_hi) fetch_hi_page();
|
||||
return bankN_hi[addr - 0x6000];
|
||||
}
|
||||
/* 0xA000 - 0xBFFF: cartridge RAM */
|
||||
return read_ram(addr);
|
||||
}
|
||||
@@ -59,9 +71,12 @@ private:
|
||||
auto read_ram(u16 addr) const -> u8;
|
||||
void write_ram(u16 addr, u8 value);
|
||||
void update_rom_bank();
|
||||
void fetch_hi_page() const;
|
||||
|
||||
const u8* bank0 = nullptr;
|
||||
const u8* bankN = nullptr;
|
||||
mutable const u8* bankN_lo = nullptr; /* 0x4000 - 0x5FFF */
|
||||
mutable const u8* bankN_hi = nullptr; /* 0x6000 - 0x7FFF, lazy (see read) */
|
||||
uint cur_bank = 0xFFFFFFFFu; /* currently mapped bank (memo) */
|
||||
|
||||
u8* ram = nullptr;
|
||||
u32 ram_size = 0;
|
||||
+41
-27
@@ -29,18 +29,42 @@ void CPU::init_post_boot() {
|
||||
}
|
||||
|
||||
auto CPU::tick() -> Cycles {
|
||||
handle_interrupts();
|
||||
/* STOP: the CPU core is frozen until joypad activity (notify_joypad).
|
||||
* Interrupt flags raised meanwhile stay pending. */
|
||||
if (stopped) { return 8; }
|
||||
|
||||
/* Halted: batch 4 M-cycles per iteration. Games spend most of every
|
||||
/* Interrupt fast path: nothing pending for the enabled sources (the
|
||||
* overwhelmingly common case) costs two loads and a branch. Bits 5-7
|
||||
* of IF/IE are unwired on hardware and must be masked: without the
|
||||
* mask, IF's always-set upper bits (post-boot 0xE1) against a game
|
||||
* writing IE=0xFF would push PC without dispatching any vector. */
|
||||
u8 fired = (u8)(interrupt_flag.value() & interrupt_enabled.value() & 0x1F);
|
||||
if(fired) handle_interrupts(fired);
|
||||
|
||||
/* Halted: batch 8 M-cycles per iteration. Games spend most of every
|
||||
* frame in HALT waiting for vblank; stepping 1 cycle at a time made
|
||||
* the idle part of the frame as expensive to emulate as the busy part.
|
||||
* Interrupt recognition is delayed by at most 3 M-cycles (12 T-cycles),
|
||||
* Interrupt recognition is delayed by at most 7 M-cycles (28 T-cycles),
|
||||
* well within what real hardware tolerates. */
|
||||
if (halted) { return 4; }
|
||||
if (halted) { return 8; }
|
||||
|
||||
/* EI enables IME only AFTER the instruction that follows it. Without
|
||||
* this delay, the classic pause idiom `EI / HALT` with an interrupt
|
||||
* already pending dispatched BETWEEN the two: the ISR consumed the
|
||||
* wake event, RETI returned onto the HALT, and the game slept with
|
||||
* its wake condition already spent -- input appeared permanently dead
|
||||
* while the vblank ISR (music) kept running. */
|
||||
bool ei_was_pending = ime_pending;
|
||||
|
||||
u16 opcode_pc = pc.value();
|
||||
auto opcode = get_byte_from_pc();
|
||||
auto cycles = execute_opcode(opcode, opcode_pc);
|
||||
|
||||
if(ei_was_pending && ime_pending) {
|
||||
/* not cancelled by a DI in the delay slot */
|
||||
interrupts_enabled = true;
|
||||
ime_pending = false;
|
||||
}
|
||||
return cycles;
|
||||
}
|
||||
|
||||
@@ -55,11 +79,8 @@ auto CPU::execute_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
|
||||
return execute_normal_opcode(opcode, opcode_pc);
|
||||
}
|
||||
|
||||
void CPU::handle_interrupts() {
|
||||
u8 fired_interrupts = interrupt_flag.value() & interrupt_enabled.value();
|
||||
if (!fired_interrupts) { return; }
|
||||
|
||||
if (halted && fired_interrupts != 0x0) {
|
||||
void CPU::handle_interrupts(u8 fired_interrupts) {
|
||||
if (halted) {
|
||||
// TODO: Handle halt bug
|
||||
halted = false;
|
||||
}
|
||||
@@ -99,24 +120,9 @@ auto CPU::handle_interrupt(u8 interrupt_bit, u16 interrupt_vector, u8 fired_inte
|
||||
return true;
|
||||
}
|
||||
|
||||
auto CPU::get_byte_from_pc() -> u8 {
|
||||
u8 byte = gb.mmu.read(Address(pc));
|
||||
pc.increment();
|
||||
|
||||
return byte;
|
||||
}
|
||||
|
||||
auto CPU::get_signed_byte_from_pc() -> s8 {
|
||||
u8 byte = get_byte_from_pc();
|
||||
return static_cast<s8>(byte);
|
||||
}
|
||||
|
||||
auto CPU::get_word_from_pc() -> u16 {
|
||||
u8 low_byte = get_byte_from_pc();
|
||||
u8 high_byte = get_byte_from_pc();
|
||||
|
||||
return compose_bytes(high_byte, low_byte);
|
||||
}
|
||||
/* get_byte_from_pc / get_signed_byte_from_pc / get_word_from_pc are the
|
||||
* hottest functions in the emulator (every instruction fetches through
|
||||
* them): they are defined inline at the bottom of gameboy.h. */
|
||||
|
||||
void CPU::set_flag_zero(bool set) { f.set_flag_zero(set); }
|
||||
void CPU::set_flag_subtract(bool set) { f.set_flag_subtract(set); }
|
||||
@@ -223,3 +229,11 @@ auto CPU::execute_cb_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
|
||||
|
||||
return opcode_cycles_cb[opcode];
|
||||
}
|
||||
|
||||
/* Unity build: the opcode implementations are compiled inside this same
|
||||
* translation unit so the dispatch switches above can call (and inline)
|
||||
* them directly. As separate TUs, every emulated instruction paid two
|
||||
* cross-TU calls (dispatch -> opcode_XX -> opcode helper), which the
|
||||
* compiler could not eliminate without LTO. */
|
||||
#include "opcode_mapping.inc"
|
||||
#include "opcodes.inc"
|
||||
+15
-1
@@ -52,13 +52,27 @@ public:
|
||||
ByteRegister interrupt_enabled;
|
||||
|
||||
private:
|
||||
void handle_interrupts();
|
||||
void handle_interrupts(u8 fired_interrupts);
|
||||
auto handle_interrupt(u8 interrupt_bit, u16 interrupt_vector, u8 fired_interrupts) -> bool;
|
||||
|
||||
Gameboy& gb;
|
||||
|
||||
bool interrupts_enabled = false;
|
||||
bool ime_pending = false; /* EI takes effect AFTER the next instruction */
|
||||
bool halted = false;
|
||||
bool stopped = false; /* STOP: woken only by joypad activity */
|
||||
|
||||
public:
|
||||
/* Joypad line activity: wakes STOP unconditionally (real DMG behaviour:
|
||||
* STOP exits on a joypad line change regardless of IE/IF/IME). */
|
||||
void notify_joypad() {
|
||||
if(stopped) {
|
||||
stopped = false;
|
||||
halted = false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
bool branch_taken = false;
|
||||
|
||||
+9
@@ -61,6 +61,15 @@ public:
|
||||
* storage slot s starts at bit offset s * GAMEBOY_WIDTH * 2. */
|
||||
auto raw() const -> const u8* { return buf; }
|
||||
|
||||
/* Direct pointer to the packed bytes of screen row y (40 bytes,
|
||||
* 4 pixels each, LSB-first) or nullptr if the row has no storage.
|
||||
* Used by the byte-oriented renderer in video.cc. */
|
||||
auto row_ptr(uint y) -> u8* {
|
||||
uint slot = row_slot[y];
|
||||
if(slot == NO_ROW) return nullptr;
|
||||
return buf + slot * (GAMEBOY_WIDTH / 4);
|
||||
}
|
||||
|
||||
private:
|
||||
static const u8 NO_ROW = 0xFF;
|
||||
|
||||
+6
-8
@@ -44,8 +44,10 @@ void Gameboy::vblank_trampoline(void* ctx) {
|
||||
void Gameboy::button_pressed(GbButton button) {
|
||||
input.button_pressed(button);
|
||||
/* Request the joypad interrupt (missing upstream); mainly wakes
|
||||
* games waiting in HALT/STOP for input. */
|
||||
* games waiting in HALT for input. */
|
||||
cpu.interrupt_flag.set_bit_to(4, true);
|
||||
/* STOP wakes on joypad activity regardless of IE/IF/IME */
|
||||
cpu.notify_joypad();
|
||||
}
|
||||
|
||||
void Gameboy::button_released(GbButton button) {
|
||||
@@ -53,15 +55,11 @@ void Gameboy::button_released(GbButton button) {
|
||||
}
|
||||
|
||||
void Gameboy::run_to_vblank() {
|
||||
/* Gameboy::tick and the peripheral ticks it calls are defined inline
|
||||
* in gameboy.h, so this loop compiles into one flattened body with a
|
||||
* single out-of-line call per instruction (cpu.tick). */
|
||||
frame_done = false;
|
||||
while(!frame_done) {
|
||||
tick();
|
||||
}
|
||||
}
|
||||
|
||||
void Gameboy::tick() {
|
||||
auto cycles = cpu.tick();
|
||||
video.tick(cycles);
|
||||
timer.tick(cycles.cycles);
|
||||
apu.tick(cycles.cycles);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
#pragma once
|
||||
|
||||
#include "input.h"
|
||||
#include "cpu.h"
|
||||
#include "video.h"
|
||||
#include "timer.h"
|
||||
#include "mmu.h"
|
||||
#include "cartridge.h"
|
||||
#include "apu.h"
|
||||
|
||||
class Gameboy {
|
||||
public:
|
||||
/* bank0: pointer to the first 16 KB of ROM (stays resident).
|
||||
* provider: returns pointers to 16 KB switchable banks. */
|
||||
Gameboy(
|
||||
const u8* bank0,
|
||||
uint rom_bank_count,
|
||||
MBCType mbc,
|
||||
u8* cart_ram,
|
||||
u32 cart_ram_size,
|
||||
RomBankProvider provider,
|
||||
void* provider_ctx);
|
||||
|
||||
/* Runs the emulator until the next vblank (one full frame). */
|
||||
void run_to_vblank();
|
||||
|
||||
/* Called on every vblank BEFORE the framebuffer is cleared for the next
|
||||
* frame: this is where the frontend must convert/copy the image. */
|
||||
void set_frame_callback(void (*cb)(void*), void* ctx) {
|
||||
user_frame_cb = cb;
|
||||
user_frame_ctx = ctx;
|
||||
}
|
||||
|
||||
void button_pressed(GbButton button);
|
||||
void button_released(GbButton button);
|
||||
|
||||
void set_skip_render(bool skip) { video.skip_render = skip; }
|
||||
|
||||
/* Flush pending peripheral cycles. Called automatically every
|
||||
* PERIPH_BATCH M-cycles and before any IO register read/write so
|
||||
* LY/STAT/DIV/TIMA/IF are always fresh where the game can see them. */
|
||||
void sync_peripherals() {
|
||||
uint n = pending_cycles;
|
||||
if(n) {
|
||||
pending_cycles = 0;
|
||||
video.tick(Cycles(n));
|
||||
timer.tick(n);
|
||||
apu.tick(n);
|
||||
}
|
||||
}
|
||||
|
||||
/* Optional display-line mask (see Video::row_mask). The array must stay
|
||||
* valid for the lifetime of the emulator. */
|
||||
void set_row_mask(const u8* mask) { video.set_row_mask(mask); }
|
||||
auto get_framebuffer() const -> const FrameBuffer& { return video.get_framebuffer(); }
|
||||
|
||||
auto get_cartridge_ram() -> u8* { return cartridge.get_ram(); }
|
||||
auto get_cartridge_ram_size() const -> u32 { return cartridge.get_ram_size(); }
|
||||
|
||||
Cartridge cartridge;
|
||||
|
||||
CPU cpu;
|
||||
friend class CPU;
|
||||
|
||||
Video video;
|
||||
friend class Video;
|
||||
|
||||
MMU mmu;
|
||||
friend class MMU;
|
||||
|
||||
Timer timer;
|
||||
friend class Timer;
|
||||
|
||||
Apu apu;
|
||||
|
||||
Input input;
|
||||
|
||||
private:
|
||||
void tick();
|
||||
|
||||
/* Peripheral catch-up batching (the single biggest hot-loop win on the
|
||||
* Cortex-M4): instead of stepping the PPU/timer/APU state machines
|
||||
* after every CPU instruction, cycles accumulate and are flushed every
|
||||
* PERIPH_BATCH M-cycles -- or immediately whenever the CPU touches an
|
||||
* IO register. Interrupt delivery is delayed by at most PERIPH_BATCH
|
||||
* M-cycles (128 T-cycles), far below what games can observe, while IO
|
||||
* reads always see exact values thanks to the sync points. */
|
||||
static const uint PERIPH_BATCH = 32;
|
||||
uint pending_cycles = 0;
|
||||
|
||||
static void vblank_trampoline(void* ctx);
|
||||
volatile bool frame_done = false;
|
||||
|
||||
void (*user_frame_cb)(void*) = nullptr;
|
||||
void* user_frame_ctx = nullptr;
|
||||
};
|
||||
|
||||
/* ==================== hot-path inline implementations ====================
|
||||
*
|
||||
* Everything below runs once (or more) per emulated instruction. These
|
||||
* bodies need the complete Gameboy definition, and they are defined here
|
||||
* -- instead of in their .cc files -- so the core can inline them without
|
||||
* LTO. Before this, each emulated instruction paid 6-8 cross-TU function
|
||||
* calls (opcode fetch through the MMU, PPU/timer/APU ticks), which
|
||||
* dominated the frame time on the Cortex-M4.
|
||||
*
|
||||
* Deliberately NOT inlined: the full MMU read/write dispatch. Inlining it
|
||||
* into the ~300 opcode call sites costs ~15 KB of code (= RAM on the
|
||||
* Flipper, where the binary loads into the same heap as the ROM cache).
|
||||
* Only the instruction-fetch fast path (PC in ROM, true for virtually
|
||||
* every instruction) is inlined below. */
|
||||
|
||||
inline void Video::tick(Cycles cycles) {
|
||||
cycle_counter += cycles.cycles;
|
||||
|
||||
/* Fast path: no mode boundary crossed. Boundary work (interrupts,
|
||||
* scanline rendering) is out-of-line in video.cc. The counter wrap
|
||||
* uses subtraction instead of the old modulo: per-instruction cycle
|
||||
* increments (<= 6) can never overshoot a whole extra period. */
|
||||
switch(current_mode) {
|
||||
case VideoMode::ACCESS_OAM:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE_OAM) {
|
||||
cycle_counter -= CLOCKS_PER_SCANLINE_OAM;
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, true);
|
||||
current_mode = VideoMode::ACCESS_VRAM;
|
||||
}
|
||||
break;
|
||||
case VideoMode::ACCESS_VRAM:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE_VRAM) {
|
||||
cycle_counter -= CLOCKS_PER_SCANLINE_VRAM;
|
||||
mode_transition_vram_end();
|
||||
}
|
||||
break;
|
||||
case VideoMode::HBLANK:
|
||||
if(cycle_counter >= CLOCKS_PER_HBLANK) {
|
||||
cycle_counter -= CLOCKS_PER_HBLANK;
|
||||
mode_transition_hblank_end();
|
||||
}
|
||||
break;
|
||||
case VideoMode::VBLANK:
|
||||
if(cycle_counter >= CLOCKS_PER_SCANLINE) {
|
||||
cycle_counter -= CLOCKS_PER_SCANLINE;
|
||||
mode_transition_vblank_line();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inline void Timer::tick(uint cycles) {
|
||||
/* DIV increments at 16384 Hz = every 64 M-cycles */
|
||||
div_clocks += cycles;
|
||||
if(div_clocks >= 64) {
|
||||
divider.set(static_cast<u8>(divider.value() + (div_clocks >> 6)));
|
||||
div_clocks &= 63;
|
||||
}
|
||||
|
||||
/* Accumulate T-cycles only while the timer is enabled: accumulating
|
||||
* with TAC off built up an unbounded backlog that, when a game later
|
||||
* enabled the timer, drained as a burst of TIMA overflows + spurious
|
||||
* timer interrupts (plus a long stall in the loop below). */
|
||||
if(!timer_control.check_bit(2)) {
|
||||
clocks = 0;
|
||||
return;
|
||||
}
|
||||
clocks += cycles * 4; /* M-cycles -> T-cycles */
|
||||
|
||||
uint clock_limit = clocks_needed_to_increment();
|
||||
/* a long instruction can cross more than one period of the fastest
|
||||
* (16 T-cycle) timer rate; the old modulo silently dropped those */
|
||||
while(clocks >= clock_limit) {
|
||||
clocks -= clock_limit;
|
||||
|
||||
u8 old_timer_counter = timer_counter.value();
|
||||
timer_counter.increment();
|
||||
|
||||
if(timer_counter.value() < old_timer_counter) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(2, true);
|
||||
timer_counter.set(timer_modulo.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline auto CPU::get_byte_from_pc() -> u8 {
|
||||
u16 a = pc.value();
|
||||
pc.increment();
|
||||
/* PC sits in cartridge ROM for virtually every instruction: read the
|
||||
* mapped page directly and skip the full MMU dispatch */
|
||||
if(a < 0x8000) return gb.cartridge.read(a);
|
||||
return gb.mmu.read(Address(a));
|
||||
}
|
||||
|
||||
inline auto CPU::get_signed_byte_from_pc() -> s8 {
|
||||
return static_cast<s8>(get_byte_from_pc());
|
||||
}
|
||||
|
||||
inline auto CPU::get_word_from_pc() -> u16 {
|
||||
u8 low_byte = get_byte_from_pc();
|
||||
u8 high_byte = get_byte_from_pc();
|
||||
return static_cast<u16>((high_byte << 8) | low_byte);
|
||||
}
|
||||
|
||||
inline void Gameboy::tick() {
|
||||
pending_cycles += cpu.tick().cycles;
|
||||
if(pending_cycles >= PERIPH_BATCH) sync_peripherals();
|
||||
}
|
||||
+2
-1
@@ -50,5 +50,6 @@ auto Input::get_input() const -> u8 {
|
||||
buttons = set_bit_to(buttons, 4, !direction_switch);
|
||||
buttons = set_bit_to(buttons, 5, !button_switch);
|
||||
|
||||
return buttons;
|
||||
/* bits 6-7 are unwired and read as 1 on hardware */
|
||||
return static_cast<u8>(buttons | 0xC0);
|
||||
}
|
||||
+6
@@ -94,6 +94,9 @@ void MMU::write(const Address& address, u8 byte) {
|
||||
}
|
||||
|
||||
auto MMU::read_io(const Address& address) const -> u8 {
|
||||
/* catch-up: make LY/STAT/DIV/TIMA/IF/NRxx exact before the game reads */
|
||||
gb.sync_peripherals();
|
||||
|
||||
u16 a = address.value();
|
||||
|
||||
/* Sound registers + wave RAM */
|
||||
@@ -167,6 +170,9 @@ auto MMU::read_io(const Address& address) const -> u8 {
|
||||
}
|
||||
|
||||
void MMU::write_io(const Address& address, u8 byte) {
|
||||
/* catch-up: apply pending time before the write changes IO state */
|
||||
gb.sync_peripherals();
|
||||
|
||||
u16 a = address.value();
|
||||
|
||||
/* Sound registers + wave RAM */
|
||||
+15
-8
@@ -3,6 +3,13 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
/* NOTE: undefined opcodes (0xD3, 0xDB, 0xDD, 0xE3, 0xE4, 0xEB, 0xEC, 0xED,
|
||||
* 0xF4, 0xFC, 0xFD) and the 0xCB prefix placeholder cost 1 M-cycle here,
|
||||
* NOT 0. With 0-cycle entries, executing garbage (bad jump, corrupted
|
||||
* read) advanced the PC without advancing the PPU/timer clocks, so
|
||||
* run_to_vblank() could spin without ever finishing a frame: on the
|
||||
* Flipper that froze the whole device until a hard reboot. */
|
||||
|
||||
const std::array<u8, 256> opcode_cycles = {
|
||||
1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1,
|
||||
1, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
|
||||
@@ -16,10 +23,10 @@ const std::array<u8, 256> opcode_cycles = {
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 0, 3, 6, 2, 4,
|
||||
2, 3, 3, 0, 3, 4, 2, 4, 2, 4, 3, 0, 3, 0, 2, 4,
|
||||
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
|
||||
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4
|
||||
2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 1, 3, 6, 2, 4,
|
||||
2, 3, 3, 1, 3, 4, 2, 4, 2, 4, 3, 1, 3, 1, 2, 4,
|
||||
3, 3, 2, 1, 1, 4, 2, 4, 4, 1, 4, 1, 1, 1, 2, 4,
|
||||
3, 3, 2, 1, 1, 4, 2, 4, 3, 2, 4, 1, 1, 1, 2, 4
|
||||
};
|
||||
|
||||
const std::array<u8, 256> opcode_cycles_branched = {
|
||||
@@ -35,10 +42,10 @@ const std::array<u8, 256> opcode_cycles_branched = {
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
|
||||
5, 3, 4, 4, 6, 4, 2, 4, 5, 4, 4, 0, 6, 6, 2, 4,
|
||||
5, 3, 4, 0, 6, 4, 2, 4, 5, 4, 4, 0, 6, 0, 2, 4,
|
||||
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
|
||||
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4
|
||||
5, 3, 4, 4, 6, 4, 2, 4, 5, 4, 4, 1, 6, 6, 2, 4,
|
||||
5, 3, 4, 1, 6, 4, 2, 4, 5, 4, 4, 1, 6, 1, 2, 4,
|
||||
3, 3, 2, 1, 1, 4, 2, 4, 4, 1, 4, 1, 1, 1, 2, 4,
|
||||
3, 3, 2, 1, 1, 4, 2, 4, 3, 2, 4, 1, 1, 1, 2, 4
|
||||
};
|
||||
|
||||
const std::array<u8, 256> opcode_cycles_cb = {
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
#include "cpu.h"
|
||||
#include "gameboy.h" /* inline hot-path definitions (operand fetch) */
|
||||
/* clang-format off */
|
||||
|
||||
/**
|
||||
+15
-6
@@ -262,12 +262,13 @@ void CPU::opcode_dec(Address&& addr) {
|
||||
/* DI */
|
||||
void CPU::opcode_di() {
|
||||
interrupts_enabled = false;
|
||||
ime_pending = false; /* DI in the EI delay slot cancels the enable */
|
||||
}
|
||||
|
||||
|
||||
/* EI */
|
||||
/* EI: IME is enabled after the NEXT instruction (see CPU::tick) */
|
||||
void CPU::opcode_ei() {
|
||||
interrupts_enabled = true;
|
||||
ime_pending = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -543,10 +544,10 @@ void CPU::opcode_ret(Condition condition) {
|
||||
}
|
||||
|
||||
|
||||
/* RETI */
|
||||
/* RETI: unlike EI, re-enables IME immediately (no delay slot) */
|
||||
void CPU::opcode_reti() {
|
||||
opcode_ret();
|
||||
opcode_ei();
|
||||
interrupts_enabled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -810,9 +811,17 @@ void CPU::opcode_srl(Address&& addr) {
|
||||
}
|
||||
|
||||
|
||||
/* STOP */
|
||||
/* STOP: on real DMG the CPU freezes until a joypad LINE CHANGE -- this is
|
||||
* independent of IE/IF/IME (it does not go through interrupt dispatch).
|
||||
* Modelling it as plain HALT was wrong twice over: with IE bit4 clear a
|
||||
* press never satisfied IF&IE so the game froze forever, and with IE
|
||||
* bit0 set our free-running PPU vblank woke it instantly. The `stopped`
|
||||
* flag is cleared only by CPU::notify_joypad() (wired to button presses).
|
||||
* STOP is also a 2-byte instruction: consume the padding byte. */
|
||||
void CPU::opcode_stop() {
|
||||
/* halted = true; */
|
||||
get_byte_from_pc(); /* skip the 0x00 pad */
|
||||
halted = true;
|
||||
stopped = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "definitions.h"
|
||||
#include "register.h"
|
||||
|
||||
class Gameboy;
|
||||
|
||||
class Timer {
|
||||
public:
|
||||
Timer(Gameboy& inGb)
|
||||
: gb(inGb) {}
|
||||
|
||||
/* Called once per emulated instruction: defined inline at the bottom
|
||||
* of gameboy.h (needs the complete Gameboy type). */
|
||||
void tick(uint cycles);
|
||||
|
||||
auto get_divider() const -> u8 { return divider.value(); }
|
||||
auto get_timer() const -> u8 { return timer_counter.value(); }
|
||||
auto get_timer_modulo() const -> u8 { return timer_modulo.value(); }
|
||||
/* Only the bottom three bits of this register are usable */
|
||||
auto get_timer_control() const -> u8 { return timer_control.value() & 0x7; }
|
||||
|
||||
void reset_divider() { divider.set(0x0); }
|
||||
void set_timer(u8 value) { timer_counter.set(value); }
|
||||
void set_timer_modulo(u8 value) { timer_modulo.set(value); }
|
||||
void set_timer_control(u8 value) { timer_control.set(value); }
|
||||
|
||||
private:
|
||||
auto clocks_needed_to_increment() const -> uint {
|
||||
switch(get_timer_control() & 0x3) {
|
||||
case 0: return CLOCK_RATE / 4096;
|
||||
case 1: return CLOCK_RATE / 262144;
|
||||
case 2: return CLOCK_RATE / 65536;
|
||||
default: return CLOCK_RATE / 16384;
|
||||
}
|
||||
}
|
||||
|
||||
uint clocks = 0;
|
||||
uint div_clocks = 0;
|
||||
|
||||
Gameboy& gb;
|
||||
|
||||
ByteRegister divider;
|
||||
ByteRegister timer_counter;
|
||||
|
||||
ByteRegister timer_modulo;
|
||||
ByteRegister timer_control;
|
||||
};
|
||||
@@ -0,0 +1,398 @@
|
||||
#include "video.h"
|
||||
|
||||
#include "gameboy.h"
|
||||
#include "cpu.h"
|
||||
#include "bitwise.h"
|
||||
|
||||
using bitwise::check_bit;
|
||||
|
||||
/* ---------------------------------------------------------- tile decode */
|
||||
|
||||
/* s_tile_lut[b] spreads the 8 bits of a tile-data byte into 8 2-bit pixel
|
||||
* fields, leftmost pixel (bit 7) in the LOWEST field -- the same LSB-first
|
||||
* packing the framebuffer uses, so whole bytes can be emitted directly.
|
||||
* s_tile_lut_rev is the X-flipped variant for sprites. 1 KB total. */
|
||||
static u16 s_tile_lut[256];
|
||||
static u16 s_tile_lut_rev[256];
|
||||
static bool s_luts_ready = false;
|
||||
|
||||
Video::Video(Gameboy& inGb)
|
||||
: gb(inGb) {
|
||||
if(!s_luts_ready) {
|
||||
for(uint b = 0; b < 256; b++) {
|
||||
u16 fwd = 0, rev = 0;
|
||||
for(uint k = 0; k < 8; k++) {
|
||||
u16 bit = (u16)((b >> (7 - k)) & 1);
|
||||
fwd |= (u16)(bit << (2 * k));
|
||||
rev |= (u16)(bit << (2 * (7 - k)));
|
||||
}
|
||||
s_tile_lut[b] = fwd;
|
||||
s_tile_lut_rev[b] = rev;
|
||||
}
|
||||
s_luts_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Palette LUT: maps a packed 2bpp byte (4 pixels) through BGP in one
|
||||
* lookup. Rebuilt only when the game writes a new BGP value. */
|
||||
auto Video::bg_pal_lut() -> const u8* {
|
||||
u8 bgp = bg_palette.value();
|
||||
if(!bgp_lut_valid || bgp != bgp_lut_cached_for) {
|
||||
u8 shade[4] = {
|
||||
(u8)(bgp & 0x3),
|
||||
(u8)((bgp >> 2) & 0x3),
|
||||
(u8)((bgp >> 4) & 0x3),
|
||||
(u8)((bgp >> 6) & 0x3),
|
||||
};
|
||||
for(uint v = 0; v < 256; v++) {
|
||||
bgp_lut[v] = (u8)(
|
||||
shade[v & 3] | (shade[(v >> 2) & 3] << 2) | (shade[(v >> 4) & 3] << 4) |
|
||||
(shade[(v >> 6) & 3] << 6));
|
||||
}
|
||||
bgp_lut_cached_for = bgp;
|
||||
bgp_lut_valid = true;
|
||||
}
|
||||
return bgp_lut;
|
||||
}
|
||||
|
||||
/* Video::tick is called once per emulated instruction; it lives as an
|
||||
* inline definition at the bottom of gameboy.h. Mode-transition work
|
||||
* (rendering, interrupts) stays out-of-line in this file. */
|
||||
|
||||
void Video::mode_transition_vram_end() {
|
||||
current_mode = VideoMode::HBLANK;
|
||||
|
||||
bool hblank_interrupt = check_bit(lcd_status.value(), 3);
|
||||
|
||||
if(hblank_interrupt) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(1, true);
|
||||
}
|
||||
|
||||
bool ly_coincidence_interrupt = check_bit(lcd_status.value(), 6);
|
||||
bool ly_coincidence = ly_compare.value() == line.value();
|
||||
if(ly_coincidence_interrupt && ly_coincidence) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(1, true);
|
||||
}
|
||||
lcd_status.set_bit_to(2, ly_coincidence);
|
||||
|
||||
lcd_status.set_bit_to(1, false);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
}
|
||||
|
||||
void Video::mode_transition_hblank_end() {
|
||||
if(!skip_render) write_scanline(line.value());
|
||||
line.increment();
|
||||
|
||||
/* Line 145 (index 144) is the first line of VBLANK */
|
||||
if(line == 144) {
|
||||
current_mode = VideoMode::VBLANK;
|
||||
lcd_status.set_bit_to(1, false);
|
||||
lcd_status.set_bit_to(0, true);
|
||||
gb.cpu.interrupt_flag.set_bit_to(0, true);
|
||||
} else {
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
current_mode = VideoMode::ACCESS_OAM;
|
||||
}
|
||||
}
|
||||
|
||||
void Video::mode_transition_vblank_line() {
|
||||
line.increment();
|
||||
|
||||
/* LY=LYC STAT interrupt must also fire for lines 144-153: games that
|
||||
* wait for a coincidence inside vblank hung without this (it was only
|
||||
* evaluated at the mode3->hblank transition of visible lines) */
|
||||
if(check_bit(lcd_status.value(), 6) && ly_compare.value() == line.value()) {
|
||||
gb.cpu.interrupt_flag.set_bit_to(1, true);
|
||||
}
|
||||
lcd_status.set_bit_to(2, ly_compare.value() == line.value());
|
||||
|
||||
/* Line 155 (index 154) is the last line */
|
||||
if(line == 154) {
|
||||
if(!skip_render) {
|
||||
write_sprites();
|
||||
draw();
|
||||
buffer.reset();
|
||||
} else {
|
||||
draw(); /* still notify the frontend for pacing */
|
||||
}
|
||||
line.reset();
|
||||
current_mode = VideoMode::ACCESS_OAM;
|
||||
lcd_status.set_bit_to(1, true);
|
||||
lcd_status.set_bit_to(0, false);
|
||||
}
|
||||
}
|
||||
|
||||
auto Video::display_enabled() const -> bool {
|
||||
return check_bit(control_byte, 7);
|
||||
}
|
||||
auto Video::window_tile_map() const -> bool {
|
||||
return check_bit(control_byte, 6);
|
||||
}
|
||||
auto Video::window_enabled() const -> bool {
|
||||
return check_bit(control_byte, 5);
|
||||
}
|
||||
auto Video::bg_window_tile_data() const -> bool {
|
||||
return check_bit(control_byte, 4);
|
||||
}
|
||||
auto Video::bg_tile_map_display() const -> bool {
|
||||
return check_bit(control_byte, 3);
|
||||
}
|
||||
auto Video::sprite_size() const -> bool {
|
||||
return check_bit(control_byte, 2);
|
||||
}
|
||||
auto Video::sprites_enabled() const -> bool {
|
||||
return check_bit(control_byte, 1);
|
||||
}
|
||||
auto Video::bg_enabled() const -> bool {
|
||||
return check_bit(control_byte, 0);
|
||||
}
|
||||
|
||||
void Video::write_scanline(u8 current_line) {
|
||||
if(!display_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Lines the frontend never displays are not worth rendering */
|
||||
if(row_mask && current_line < GAMEBOY_HEIGHT && !row_mask[current_line]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(bg_enabled()) {
|
||||
draw_bg_line(current_line);
|
||||
}
|
||||
|
||||
if(window_enabled()) {
|
||||
draw_window_line(current_line);
|
||||
}
|
||||
}
|
||||
|
||||
void Video::write_sprites() {
|
||||
if(!sprites_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(uint sprite_n = 0; sprite_n < 40; sprite_n++) {
|
||||
draw_sprite(sprite_n);
|
||||
}
|
||||
}
|
||||
|
||||
/* Byte-oriented tile-strip renderer: emits `count` pixels of one tile-map
|
||||
* row into the packed framebuffer row `dst_row`, starting at screen pixel
|
||||
* `dst_x`, sourcing map pixels from `src_px` (wraps at 256). Pixels flow
|
||||
* through a small bit-queue so the bulk of the line is written as whole
|
||||
* palette-mapped bytes (4 px per store) instead of per-pixel RMW packing:
|
||||
* ~7x fewer operations per line than the old per-pixel path. */
|
||||
void Video::render_strip(
|
||||
u8* dst_row,
|
||||
uint dst_x,
|
||||
uint count,
|
||||
uint map_row_base, /* VRAM offset of the tile map row (32 entries) */
|
||||
uint src_px,
|
||||
uint tile_line_off, /* 2 * (row within the tile) */
|
||||
bool use_tile_set_zero,
|
||||
const u8* pal_lut) {
|
||||
uint tile_x = (src_px >> 3) & 31;
|
||||
uint fine = src_px & 7;
|
||||
uint tset_base = use_tile_set_zero ? 0x0000u : 0x0800u;
|
||||
|
||||
/* bit-queue of pending 2bpp pixels, LSB = next pixel to emit */
|
||||
u32 acc = 0;
|
||||
uint nbits = 0;
|
||||
|
||||
#define FETCH_TILE_ROW() \
|
||||
do { \
|
||||
u8 tid = video_ram[map_row_base + tile_x]; \
|
||||
tile_x = (tile_x + 1) & 31; \
|
||||
uint toff = use_tile_set_zero ? \
|
||||
(uint)tid * TILE_BYTES : \
|
||||
(uint)((s8)tid + 128) * TILE_BYTES; \
|
||||
uint la = tset_base + toff + tile_line_off; \
|
||||
u16 v = (u16)(s_tile_lut[video_ram[la]] | (s_tile_lut[video_ram[la + 1]] << 1)); \
|
||||
acc |= (u32)v << nbits; \
|
||||
nbits += 16; \
|
||||
} while(0)
|
||||
|
||||
/* prime the queue, discarding the fine-scroll pixels */
|
||||
FETCH_TILE_ROW();
|
||||
acc >>= fine * 2;
|
||||
nbits -= fine * 2;
|
||||
|
||||
/* head: single pixels until the destination is byte-aligned */
|
||||
while(count && (dst_x & 3)) {
|
||||
if(nbits < 2) FETCH_TILE_ROW();
|
||||
uint sh = (dst_x & 3) * 2;
|
||||
u8* p = dst_row + (dst_x >> 2);
|
||||
*p = (u8)((*p & ~(3u << sh)) | ((pal_lut[acc & 3] & 3u) << sh));
|
||||
acc >>= 2;
|
||||
nbits -= 2;
|
||||
dst_x++;
|
||||
count--;
|
||||
}
|
||||
|
||||
/* body: whole bytes, palette applied 4 pixels at a time */
|
||||
u8* out = dst_row + (dst_x >> 2);
|
||||
while(count >= 4) {
|
||||
if(nbits < 8) FETCH_TILE_ROW();
|
||||
*out++ = pal_lut[acc & 0xFF];
|
||||
acc >>= 8;
|
||||
nbits -= 8;
|
||||
count -= 4;
|
||||
dst_x += 4;
|
||||
}
|
||||
|
||||
/* tail */
|
||||
while(count) {
|
||||
if(nbits < 2) FETCH_TILE_ROW();
|
||||
uint sh = (dst_x & 3) * 2;
|
||||
u8* p = dst_row + (dst_x >> 2);
|
||||
*p = (u8)((*p & ~(3u << sh)) | ((pal_lut[acc & 3] & 3u) << sh));
|
||||
acc >>= 2;
|
||||
nbits -= 2;
|
||||
dst_x++;
|
||||
count--;
|
||||
}
|
||||
#undef FETCH_TILE_ROW
|
||||
}
|
||||
|
||||
void Video::draw_bg_line(uint current_line) {
|
||||
/* Note: tileset two uses signed numbering to share half the tiles with
|
||||
* tileset one */
|
||||
bool use_tile_set_zero = bg_window_tile_data();
|
||||
bool use_tile_map_zero = !bg_tile_map_display();
|
||||
|
||||
u8* dst = buffer.row_ptr(current_line);
|
||||
if(!dst) return;
|
||||
|
||||
uint scrolled_y = (current_line + scroll_y.value()) % BG_MAP_SIZE;
|
||||
uint map_row_base = (use_tile_map_zero ? (TILE_MAP_ZERO_ADDRESS - 0x8000) :
|
||||
(TILE_MAP_ONE_ADDRESS - 0x8000)) +
|
||||
(scrolled_y / TILE_HEIGHT_PX) * TILES_PER_LINE;
|
||||
|
||||
render_strip(
|
||||
dst,
|
||||
0,
|
||||
GAMEBOY_WIDTH,
|
||||
map_row_base,
|
||||
scroll_x.value(),
|
||||
(scrolled_y % TILE_HEIGHT_PX) * 2,
|
||||
use_tile_set_zero,
|
||||
bg_pal_lut());
|
||||
}
|
||||
|
||||
void Video::draw_window_line(uint current_line) {
|
||||
bool use_tile_set_zero = bg_window_tile_data();
|
||||
bool use_tile_map_zero = !window_tile_map();
|
||||
|
||||
uint scrolled_y = current_line - window_y.value();
|
||||
if(scrolled_y >= GAMEBOY_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* the window covers screen pixels from WX-7 onward, sourcing the
|
||||
* window map from column 0 */
|
||||
uint wx = window_x.value();
|
||||
uint start_x = wx >= 7 ? wx - 7 : 0;
|
||||
if(start_x >= GAMEBOY_WIDTH) return;
|
||||
|
||||
u8* dst = buffer.row_ptr(current_line);
|
||||
if(!dst) return;
|
||||
|
||||
uint map_row_base = (use_tile_map_zero ? (TILE_MAP_ZERO_ADDRESS - 0x8000) :
|
||||
(TILE_MAP_ONE_ADDRESS - 0x8000)) +
|
||||
(scrolled_y / TILE_HEIGHT_PX) * TILES_PER_LINE;
|
||||
|
||||
render_strip(
|
||||
dst,
|
||||
start_x,
|
||||
GAMEBOY_WIDTH - start_x,
|
||||
map_row_base,
|
||||
wx >= 7 ? 0 : 7 - wx,
|
||||
(scrolled_y % TILE_HEIGHT_PX) * 2,
|
||||
use_tile_set_zero,
|
||||
bg_pal_lut());
|
||||
}
|
||||
|
||||
void Video::draw_sprite(const uint sprite_n) {
|
||||
/* Each sprite is represented by 4 bytes */
|
||||
u16 oam_start = static_cast<u16>(sprite_n * SPRITE_BYTES);
|
||||
|
||||
u8 sprite_y = gb.mmu.oam_ram[oam_start];
|
||||
u8 sprite_x = gb.mmu.oam_ram[oam_start + 1];
|
||||
|
||||
/* Offscreen sprites are not drawn */
|
||||
if(sprite_y == 0 || sprite_y >= 160) {
|
||||
return;
|
||||
}
|
||||
if(sprite_x == 0 || sprite_x >= 168) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint sprite_height = sprite_size() ? 16 : 8;
|
||||
|
||||
u8 pattern_n = gb.mmu.oam_ram[oam_start + 2];
|
||||
u8 sprite_attrs = gb.mmu.oam_ram[oam_start + 3];
|
||||
|
||||
/* Bits 0-3 are used only for CGB */
|
||||
bool use_palette_1 = check_bit(sprite_attrs, 4);
|
||||
bool flip_x = check_bit(sprite_attrs, 5);
|
||||
bool flip_y = check_bit(sprite_attrs, 6);
|
||||
bool obj_behind_bg = check_bit(sprite_attrs, 7);
|
||||
|
||||
/* 4-entry shade table instead of a per-pixel palette switch */
|
||||
u8 obp = use_palette_1 ? sprite_palette_1.value() : sprite_palette_0.value();
|
||||
u8 shade[4] = {
|
||||
(u8)(obp & 0x3),
|
||||
(u8)((obp >> 2) & 0x3),
|
||||
(u8)((obp >> 4) & 0x3),
|
||||
(u8)((obp >> 6) & 0x3),
|
||||
};
|
||||
|
||||
uint tile_offset = pattern_n * TILE_BYTES;
|
||||
|
||||
int start_y = sprite_y - 16;
|
||||
int start_x = sprite_x - 8;
|
||||
|
||||
const u16* lut = flip_x ? s_tile_lut_rev : s_tile_lut;
|
||||
|
||||
for(uint y = 0; y < sprite_height; y++) {
|
||||
int screen_y = start_y + static_cast<int>(y);
|
||||
if(screen_y < 0 || screen_y >= static_cast<int>(GAMEBOY_HEIGHT)) continue;
|
||||
if(row_mask && !row_mask[screen_y]) continue;
|
||||
|
||||
u8* row = buffer.row_ptr(static_cast<uint>(screen_y));
|
||||
if(!row) continue;
|
||||
|
||||
uint src_y = !flip_y ? y : sprite_height - y - 1;
|
||||
|
||||
uint line_addr = tile_offset + src_y * 2; /* relative to tile set zero */
|
||||
|
||||
/* all 8 pixel colors of this sprite row, LSB-first left-to-right
|
||||
* (flip handled by the reversed LUT) */
|
||||
u16 v = (u16)(lut[video_ram[line_addr]] | (lut[video_ram[line_addr + 1]] << 1));
|
||||
if(v == 0) continue; /* fully transparent row: common, skip */
|
||||
|
||||
for(uint x = 0; x < TILE_WIDTH_PX; x++, v >>= 2) {
|
||||
uint color = v & 3;
|
||||
if(color == 0) continue; /* color 0 is transparent */
|
||||
|
||||
int screen_x = start_x + static_cast<int>(x);
|
||||
if(screen_x < 0 || screen_x >= static_cast<int>(GAMEBOY_WIDTH)) continue;
|
||||
|
||||
uint sh = ((uint)screen_x & 3) * 2;
|
||||
u8* p = row + ((uint)screen_x >> 2);
|
||||
|
||||
/* Note: same behaviour as upstream - the priority bit compares
|
||||
* the final shade rather than the logical color 0 */
|
||||
if(obj_behind_bg && ((*p >> sh) & 3) != SHADE_WHITE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
*p = (u8)((*p & ~(3u << sh)) | (shade[color] << sh));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Video::draw() {
|
||||
if(vblank_callback) vblank_callback(vblank_ctx);
|
||||
}
|
||||
+18
-4
@@ -72,10 +72,22 @@ private:
|
||||
void write_scanline(u8 current_line);
|
||||
void write_sprites();
|
||||
void draw();
|
||||
void mode_transition_vram_end();
|
||||
void mode_transition_hblank_end();
|
||||
void mode_transition_vblank_line();
|
||||
void draw_bg_line(uint current_line);
|
||||
void draw_window_line(uint current_line);
|
||||
void draw_sprite(uint sprite_n);
|
||||
static auto get_pixel_from_line(u8 byte1, u8 byte2, u8 pixel_index) -> u8;
|
||||
void render_strip(
|
||||
u8* dst_row,
|
||||
uint dst_x,
|
||||
uint count,
|
||||
uint map_row_base,
|
||||
uint src_px,
|
||||
uint tile_line_off,
|
||||
bool use_tile_set_zero,
|
||||
const u8* pal_lut);
|
||||
auto bg_pal_lut() -> const u8*;
|
||||
|
||||
static auto is_on_screen(int x, int y) -> bool {
|
||||
return x >= 0 && y >= 0 && x < static_cast<int>(GAMEBOY_WIDTH) &&
|
||||
@@ -91,13 +103,15 @@ private:
|
||||
auto sprites_enabled() const -> bool;
|
||||
auto bg_enabled() const -> bool;
|
||||
|
||||
static auto load_palette(const ByteRegister& palette_register) -> Palette;
|
||||
static auto get_shade_from_palette(u8 color, const Palette& palette) -> Shade;
|
||||
|
||||
Gameboy& gb;
|
||||
|
||||
FrameBuffer buffer;
|
||||
|
||||
/* packed-byte -> palette-mapped-byte LUT for the current BGP value */
|
||||
u8 bgp_lut[256];
|
||||
u8 bgp_lut_cached_for = 0;
|
||||
bool bgp_lut_valid = false;
|
||||
|
||||
u8 video_ram[0x2000] = {}; /* DMG: 8 KB (was 16 KB upstream) */
|
||||
|
||||
VideoMode current_mode = VideoMode::ACCESS_OAM;
|
||||
@@ -21,14 +21,15 @@
|
||||
#include <string.h>
|
||||
#include <new>
|
||||
|
||||
#include "gb/gameboy.h"
|
||||
#include "lib/gbcore/gameboy.h"
|
||||
|
||||
#define SCREEN_W 128
|
||||
#define SCREEN_H 64
|
||||
#define BUFFER_SIZE (SCREEN_W * SCREEN_H / 8)
|
||||
|
||||
#define GB_FRAME_US 16742 /* 59.73 Hz */
|
||||
#define BANK_SIZE 0x4000u
|
||||
#define BANK_SIZE 0x4000u /* MBC bank: 16 KB (bank 0 stays resident whole) */
|
||||
#define PAGE_SIZE 0x2000u /* cache granularity: 8 KB = half a bank */
|
||||
/* Heap kept free for the GUI/system while playing. 10 KB is enough for the
|
||||
* direct-draw takeover + input subscription + background services; the old
|
||||
* 24 KB reserve was more than one whole ROM bank of wasted headroom and
|
||||
@@ -58,28 +59,32 @@ enum {
|
||||
KBIT_B = 1 << 5,
|
||||
};
|
||||
|
||||
/* ROM bank cache.
|
||||
/* ROM page cache.
|
||||
*
|
||||
* Every switchable bank lives in its own 16 KB heap block: there is no
|
||||
* "whole ROM in one contiguous malloc" fast path any more, because a large
|
||||
* contiguous allocation is exactly what fails (and used to crash the
|
||||
* firmware) on a fragmented heap. Instead:
|
||||
* Every 8 KB ROM page lives in its own heap block: there is no "whole ROM
|
||||
* in one contiguous malloc" fast path, because a large contiguous
|
||||
* allocation is exactly what fails (and used to crash the firmware) on a
|
||||
* fragmented heap. 8 KB granularity (half a MBC bank) doubles the number
|
||||
* of cache slots per KB of heap and halves the SD stall of a miss, which
|
||||
* is what makes bank-switch heavy games (Pokemon) playable while
|
||||
* streaming.
|
||||
*
|
||||
* - as many 16 KB slots as the heap safely affords are allocated up front;
|
||||
* - if every switchable bank got a slot, the ROM is fully resident: bank
|
||||
* - as many 8 KB slots as the heap safely affords are allocated up front;
|
||||
* - if every switchable page got a slot, the ROM is fully resident: page
|
||||
* lookup is a direct O(1) index and the SD file is closed;
|
||||
* - otherwise the slots form an LRU cache streaming banks from SD.
|
||||
* - otherwise the slots form an LRU cache streaming pages from SD.
|
||||
*/
|
||||
typedef struct {
|
||||
File* file; /* NULL once the ROM is fully resident */
|
||||
u8* bank0;
|
||||
u8** slots; /* num_slots pointers to 16 KB blocks */
|
||||
u16* slot_bank; /* which bank each slot holds (0 = empty) */
|
||||
u8** slots; /* num_slots pointers to 8 KB blocks */
|
||||
u16* slot_page; /* which ROM page each slot holds (0 = empty) */
|
||||
u32* slot_use; /* LRU stamps (streaming mode only) */
|
||||
u16* slot_hits; /* hit counters: the hottest slot is eviction-protected */
|
||||
u32 use_counter;
|
||||
u16 num_slots;
|
||||
u16 banks;
|
||||
bool fully_loaded; /* slots[i] permanently holds bank i+1 */
|
||||
bool fully_loaded; /* slots[i] permanently holds page i+2 */
|
||||
} RomCache;
|
||||
|
||||
typedef struct {
|
||||
@@ -90,21 +95,24 @@ typedef struct {
|
||||
FuriMutex* fb_mutex;
|
||||
|
||||
volatile uint8_t keys; /* KBIT_* currently pressed */
|
||||
uint8_t keys_blocked; /* held keys suppressed until physical release */
|
||||
volatile bool menu_requested;
|
||||
volatile bool menu_active;
|
||||
volatile bool exit_requested;
|
||||
|
||||
Gameboy* gb;
|
||||
void* gb_mem; /* raw block for the Gameboy, reserved during rom_load */
|
||||
RomCache rom;
|
||||
u8* cart_ram;
|
||||
u32 cart_ram_size;
|
||||
bool has_battery;
|
||||
|
||||
/* sound (single-tone piezo fed with the dominant APU voice) */
|
||||
bool sound_enabled;
|
||||
uint8_t volume_setting; /* 0 = off, 1..4 = 25/50/75/100% */
|
||||
bool speaker_acquired;
|
||||
uint32_t tone_freq; /* currently playing tone, 0 = silent */
|
||||
uint8_t tone_vol; /* 0..15, scaled by master volume */
|
||||
uint8_t tone_setting; /* volume_setting the current tone was started with */
|
||||
|
||||
/* menu */
|
||||
int menu_cursor;
|
||||
@@ -138,39 +146,66 @@ extern "C" [[noreturn]] void gb_fatal(const char* msg) {
|
||||
|
||||
/* ------------------------------------------------------- rom bank provider */
|
||||
|
||||
static const u8* rom_bank_provider(void* ctx, uint bank) {
|
||||
static const u8* rom_bank_provider(void* ctx, uint page) {
|
||||
RomCache* rc = (RomCache*)ctx;
|
||||
|
||||
if(bank == 0) return rc->bank0;
|
||||
/* pages 0 and 1 are the two halves of the resident bank 0 */
|
||||
if(page < 2) return rc->bank0 + page * PAGE_SIZE;
|
||||
|
||||
if(rc->fully_loaded) {
|
||||
return rc->slots[bank - 1]; /* O(1), the whole ROM is resident */
|
||||
return rc->slots[page - 2]; /* O(1), the whole ROM is resident */
|
||||
}
|
||||
|
||||
/* cache lookup */
|
||||
u16 lru = 0;
|
||||
/* cache lookup. Eviction is LRU with two protections:
|
||||
* - never evict the MRU slot: the cartridge's lo page is always the
|
||||
* MRU when its (lazy) hi page faults in, so the lo pointer can
|
||||
* never be left dangling;
|
||||
* - keep the single hottest slot (most hits, e.g. the page holding
|
||||
* the music driver that is switched in every frame): pure LRU on
|
||||
* Pokemon's cyclic per-frame bank pattern degenerated to a ~0%
|
||||
* hit rate. Hit counters decay periodically so the protection
|
||||
* adapts when the hot page changes. */
|
||||
u16 hottest = 0;
|
||||
u16 mru = 0;
|
||||
for(u16 i = 1; i < rc->num_slots; i++) {
|
||||
if(rc->slot_hits[i] > rc->slot_hits[hottest]) hottest = i;
|
||||
if(rc->slot_use[i] > rc->slot_use[mru]) mru = i;
|
||||
}
|
||||
|
||||
u16 lru = 0xFFFF;
|
||||
u32 lru_use = 0xFFFFFFFFu;
|
||||
for(u16 i = 0; i < rc->num_slots; i++) {
|
||||
if(rc->slot_bank[i] == bank) {
|
||||
if(rc->slot_page[i] == page) {
|
||||
rc->slot_use[i] = ++rc->use_counter;
|
||||
if(rc->slot_hits[i] < 0xFFFF) rc->slot_hits[i]++;
|
||||
if((rc->use_counter & 63) == 0) {
|
||||
for(u16 j = 0; j < rc->num_slots; j++)
|
||||
rc->slot_hits[j] >>= 1;
|
||||
}
|
||||
return rc->slots[i];
|
||||
}
|
||||
if(rc->slot_use[i] < lru_use) {
|
||||
if(i != hottest && i != mru && rc->slot_use[i] < lru_use) {
|
||||
lru_use = rc->slot_use[i];
|
||||
lru = i;
|
||||
}
|
||||
}
|
||||
|
||||
/* miss: stream the bank from SD into the LRU slot */
|
||||
storage_file_seek(rc->file, (u32)bank * BANK_SIZE, true);
|
||||
size_t got = storage_file_read(rc->file, rc->slots[lru], BANK_SIZE);
|
||||
if(got < BANK_SIZE) {
|
||||
/* short read (SD hiccup / truncated ROM): open-bus instead of
|
||||
* stale data from whatever bank lived here before */
|
||||
memset(rc->slots[lru] + got, 0xFF, BANK_SIZE - got);
|
||||
if(lru == 0xFFFF) {
|
||||
/* every other slot excluded (2-slot cache): sacrifice the hottest;
|
||||
* it is never the MRU in this branch, so the lo page stays safe */
|
||||
lru = (hottest != mru) ? hottest : (u16)(mru == 0 ? 1 : 0);
|
||||
}
|
||||
rc->slot_bank[lru] = (u16)bank;
|
||||
|
||||
/* miss: stream the page from SD into the LRU slot */
|
||||
storage_file_seek(rc->file, (u32)page * PAGE_SIZE, true);
|
||||
size_t got = storage_file_read(rc->file, rc->slots[lru], PAGE_SIZE);
|
||||
if(got < PAGE_SIZE) {
|
||||
/* short read (SD hiccup / truncated ROM): open-bus instead of
|
||||
* stale data from whatever page lived here before */
|
||||
memset(rc->slots[lru] + got, 0xFF, PAGE_SIZE - got);
|
||||
}
|
||||
rc->slot_page[lru] = (u16)page;
|
||||
rc->slot_use[lru] = ++rc->use_counter;
|
||||
rc->slot_hits[lru] = 1;
|
||||
return rc->slots[lru];
|
||||
}
|
||||
|
||||
@@ -185,9 +220,11 @@ static uint8_t s_ymap[SCREEN_H];
|
||||
* rendering work per frame). */
|
||||
static u8 s_rowmask[GAMEBOY_HEIGHT];
|
||||
|
||||
/* light/dark decision per (y-parity, x-parity, shade): 2x2 ordered dither.
|
||||
* white -> lit, light gray -> 3/4 lit, dark gray -> 1/4 lit, black -> off */
|
||||
static uint8_t s_dither[2][2][4];
|
||||
/* Dither LUT: maps one packed 2bpp framebuffer byte (4 consecutive GB
|
||||
* pixels) to 4 light/dark bits (bit j = pixel j), per destination row
|
||||
* parity. 2x2 ordered dither: white -> lit, light gray -> 3/4 lit,
|
||||
* dark gray -> 1/4 lit, black -> off. */
|
||||
static uint8_t s_dither4[2][256];
|
||||
|
||||
static void init_scale_maps(void) {
|
||||
for(int x = 0; x < SCREEN_W; x++)
|
||||
@@ -200,17 +237,32 @@ static void init_scale_maps(void) {
|
||||
s_rowmask[s_ymap[y]] = 1;
|
||||
|
||||
for(int py = 0; py < 2; py++) {
|
||||
for(int px = 0; px < 2; px++) {
|
||||
s_dither[py][px][0] = 1;
|
||||
s_dither[py][px][1] = (px == 1 && py == 1) ? 0 : 1;
|
||||
s_dither[py][px][2] = (px == 0 && py == 0) ? 1 : 0;
|
||||
s_dither[py][px][3] = 0;
|
||||
for(int v = 0; v < 256; v++) {
|
||||
uint8_t bits = 0;
|
||||
for(int j = 0; j < 4; j++) {
|
||||
int s = (v >> (j * 2)) & 3;
|
||||
int px = j & 1;
|
||||
bool light;
|
||||
switch(s) {
|
||||
case 0: light = true; break;
|
||||
case 1: light = !(px == 1 && py == 1); break;
|
||||
case 2: light = (px == 0 && py == 0); break;
|
||||
default: light = false; break;
|
||||
}
|
||||
if(light) bits |= (uint8_t)(1 << j);
|
||||
}
|
||||
s_dither4[py][v] = bits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Called by the core on every vblank, before the framebuffer is cleared.
|
||||
* Converts 4 shades -> 1 bit with a 2x2 ordered dither. */
|
||||
* Converts 4 shades -> 1 bit with a 2x2 ordered dither.
|
||||
*
|
||||
* Byte-oriented: destination pixels 4g..4g+3 map to source pixels
|
||||
* 5g..5g+3 (the 160->128 downscale drops every 5th column), so each
|
||||
* destination nibble comes from 8 consecutive source bits, converted with
|
||||
* a single LUT lookup instead of per-pixel shade extraction. */
|
||||
static void frame_callback(void* ctx) {
|
||||
AppState* app = (AppState*)ctx;
|
||||
const u8* raw = app->gb->get_framebuffer().raw(); /* packed 2bpp */
|
||||
@@ -221,19 +273,24 @@ static void frame_callback(void* ctx) {
|
||||
for(int y = 0; y < SCREEN_H; y++) {
|
||||
/* storage is compacted to the 64 displayed rows in ascending order
|
||||
* (GB_FB_ROWS=64 + row mask), so displayed row y == storage slot y */
|
||||
uint base = (uint)y * GAMEBOY_WIDTH;
|
||||
const u8* src = raw + (uint)y * (GAMEBOY_WIDTH / 4);
|
||||
uint8_t bit = (uint8_t)(1u << (y & 7));
|
||||
uint8_t nbit = (uint8_t)~bit;
|
||||
uint8_t* row = dst + (y >> 3) * SCREEN_W;
|
||||
const uint8_t(*dither)[4] = s_dither[y & 1];
|
||||
const uint8_t* dlut = s_dither4[y & 1];
|
||||
|
||||
for(int x = 0; x < SCREEN_W; x++) {
|
||||
uint i = base + s_xmap[x];
|
||||
uint s = (raw[i >> 2] >> ((i & 3) << 1)) & 0x3;
|
||||
if(dither[x & 1][s])
|
||||
row[x] |= bit;
|
||||
else
|
||||
row[x] &= nbit;
|
||||
for(int g = 0; g < SCREEN_W / 4; g++) {
|
||||
uint spx = (uint)g * 5; /* first source pixel of this group */
|
||||
uint so = spx >> 2;
|
||||
uint sh = (spx & 3) * 2;
|
||||
uint packed = ((uint)src[so] | ((uint)src[so + 1] << 8)) >> sh;
|
||||
uint8_t four = dlut[packed & 0xFF];
|
||||
|
||||
uint8_t* p = row + g * 4;
|
||||
p[0] = (four & 1) ? (uint8_t)(p[0] | bit) : (uint8_t)(p[0] & nbit);
|
||||
p[1] = (four & 2) ? (uint8_t)(p[1] | bit) : (uint8_t)(p[1] & nbit);
|
||||
p[2] = (four & 4) ? (uint8_t)(p[2] | bit) : (uint8_t)(p[2] & nbit);
|
||||
p[3] = (four & 8) ? (uint8_t)(p[3] | bit) : (uint8_t)(p[3] & nbit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,8 +368,11 @@ static void input_events_callback(const void* value, void* ctx) {
|
||||
uint8_t keys =
|
||||
(uint8_t)__atomic_or_fetch((uint8_t*)&app->keys, bit, __ATOMIC_RELAXED);
|
||||
/* Up+Down together: physically impossible on a real GB d-pad,
|
||||
* so it is our reserved menu gesture */
|
||||
if((keys & (KBIT_UP | KBIT_DOWN)) == (KBIT_UP | KBIT_DOWN)) {
|
||||
* so it is our reserved menu gesture. Ignored while the menu
|
||||
* is already open: fast Up->Down rollover while navigating
|
||||
* used to re-latch the request and instantly reopen the menu
|
||||
* after closing it (merging consecutive Start injections). */
|
||||
if(!app->menu_active && (keys & (KBIT_UP | KBIT_DOWN)) == (KBIT_UP | KBIT_DOWN)) {
|
||||
app->menu_requested = true;
|
||||
}
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
@@ -325,7 +385,11 @@ static void input_events_callback(const void* value, void* ctx) {
|
||||
|
||||
/* Apply the current key snapshot to the emulated joypad (edge based) */
|
||||
static void apply_input(AppState* app, uint8_t* last_applied) {
|
||||
uint8_t now = app->menu_active ? 0 : app->keys;
|
||||
/* keys_blocked: the A/B press that operated the menu must not leak
|
||||
* into the game as a fresh press when the menu closes; blocked bits
|
||||
* clear automatically on physical release */
|
||||
app->keys_blocked &= app->keys;
|
||||
uint8_t now = app->menu_active ? 0 : (uint8_t)(app->keys & ~app->keys_blocked);
|
||||
uint8_t changed = (uint8_t)(now ^ *last_applied);
|
||||
if(!changed && !app->inject_start_frames && !app->inject_select_frames) return;
|
||||
|
||||
@@ -379,7 +443,7 @@ static void sound_update(AppState* app, bool force_silent) {
|
||||
ApuVoice best = {false, 0, 0, 0};
|
||||
bool have = false;
|
||||
|
||||
if(!force_silent && app->sound_enabled && !app->menu_active) {
|
||||
if(!force_silent && app->volume_setting && !app->menu_active) {
|
||||
ApuVoice v;
|
||||
/* pulse 1 / pulse 2 */
|
||||
for(uint n = 0; n < 2; n++) {
|
||||
@@ -422,10 +486,17 @@ static void sound_update(AppState* app, bool force_silent) {
|
||||
uint8_t vol = (uint8_t)((best.volume * (master + 1)) >> 3); /* 0..15 */
|
||||
if(vol == 0) {
|
||||
have = false;
|
||||
} else if(f != app->tone_freq || vol != app->tone_vol) {
|
||||
furi_hal_speaker_start((float)f, (float)vol / 15.0f);
|
||||
} else if(
|
||||
f != app->tone_freq || vol != app->tone_vol ||
|
||||
app->volume_setting != app->tone_setting) {
|
||||
/* piezo loudness vs PWM value is very nonlinear: spread
|
||||
* the 4 user levels perceptually, not linearly */
|
||||
static const float level[5] = {0.0f, 0.10f, 0.25f, 0.50f, 1.0f};
|
||||
furi_hal_speaker_start(
|
||||
(float)f, ((float)vol / 15.0f) * level[app->volume_setting]);
|
||||
app->tone_freq = f;
|
||||
app->tone_vol = vol;
|
||||
app->tone_setting = app->volume_setting;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,18 +582,25 @@ static void menu_draw(AppState* app) {
|
||||
canvas_draw_str(c, 72, y, buf);
|
||||
}
|
||||
if(i == 4) {
|
||||
static const char* vols[5] = {"off", "25%", "50%", "75%", "100%"};
|
||||
canvas_draw_str(
|
||||
c, 72, y, !app->speaker_acquired ? "n/a" : (app->sound_enabled ? "on" : "off"));
|
||||
c, 72, y, !app->speaker_acquired ? "n/a" : vols[app->volume_setting]);
|
||||
}
|
||||
}
|
||||
|
||||
if(app->status_msg[0]) {
|
||||
canvas_draw_str(c, 70, 10, app->status_msg);
|
||||
} else {
|
||||
/* free heap indicator: helps spotting memory pressure on device */
|
||||
char rambuf[16];
|
||||
snprintf(rambuf, sizeof(rambuf), "%uk free", (unsigned)(memmgr_get_free_heap() / 1024u));
|
||||
canvas_draw_str_aligned(c, 126, 10, AlignRight, AlignBottom, rambuf);
|
||||
/* diagnostics: free heap + real cost of one emulated frame
|
||||
* (16.7ms = full speed; above that the game runs slow) */
|
||||
char diagbuf[28];
|
||||
snprintf(
|
||||
diagbuf,
|
||||
sizeof(diagbuf),
|
||||
"%uk %lums",
|
||||
(unsigned)(memmgr_get_free_heap() / 1024u),
|
||||
(unsigned long)(app->emu_ms_ema >> 4));
|
||||
canvas_draw_str_aligned(c, 126, 10, AlignRight, AlignBottom, diagbuf);
|
||||
}
|
||||
|
||||
canvas_commit(c);
|
||||
@@ -554,7 +632,11 @@ static bool menu_tick(AppState* app, Storage* storage, FuriString* sav_path) {
|
||||
app->frameskip_setting = v;
|
||||
}
|
||||
if(app->menu_cursor == 4 && app->speaker_acquired) {
|
||||
app->sound_enabled = !app->sound_enabled;
|
||||
/* volume: off, 25, 50, 75, 100% */
|
||||
if(pressed & KBIT_RIGHT)
|
||||
app->volume_setting = (uint8_t)((app->volume_setting + 1) % 5);
|
||||
else
|
||||
app->volume_setting = (uint8_t)((app->volume_setting + 4) % 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,17 +647,22 @@ static bool menu_tick(AppState* app, Storage* storage, FuriString* sav_path) {
|
||||
case 0:
|
||||
return false;
|
||||
case 1:
|
||||
app->gb->button_pressed(GbButton::Start);
|
||||
/* extend rather than re-press if an injection is still live:
|
||||
* two overlapping presses would merge into one edge for the
|
||||
* game (Tetris pause state would get out of sync) */
|
||||
if(app->inject_start_frames == 0) app->gb->button_pressed(GbButton::Start);
|
||||
app->inject_start_frames = 8;
|
||||
return false;
|
||||
case 2:
|
||||
app->gb->button_pressed(GbButton::Select);
|
||||
if(app->inject_select_frames == 0) app->gb->button_pressed(GbButton::Select);
|
||||
app->inject_select_frames = 8;
|
||||
return false;
|
||||
case 3:
|
||||
break;
|
||||
case 4:
|
||||
if(app->speaker_acquired) app->sound_enabled = !app->sound_enabled;
|
||||
/* OK toggles mute <-> full volume */
|
||||
if(app->speaker_acquired)
|
||||
app->volume_setting = app->volume_setting ? 0 : 4;
|
||||
break;
|
||||
case 5:
|
||||
if(app->has_battery) {
|
||||
@@ -648,52 +735,59 @@ static RomLoadResult
|
||||
memset(app->cart_ram, 0, app->cart_ram_size);
|
||||
}
|
||||
|
||||
/* Allocate as many 16 KB bank slots as the heap safely affords.
|
||||
*
|
||||
* IMPORTANT: the emulator core (Gameboy: 8K VRAM + 8K WRAM + packed
|
||||
* framebuffer + CPU state, ~22 KB) plus the GUI takeover (mutex, canvas,
|
||||
* input subscription) are allocated AFTER the ROM cache, so room for
|
||||
* them is reserved up front. Each slot is its own allocation: no huge
|
||||
* contiguous block is ever requested, which makes the loader immune to
|
||||
* heap fragmentation (the old single-malloc full-ROM path could crash
|
||||
* the firmware even when the total free heap looked sufficient). */
|
||||
u32 switchable = (u32)(rc->banks - 1);
|
||||
const size_t reserve = HEAP_RESERVE + sizeof(Gameboy) + 1024 /* alloc slack */;
|
||||
/* Reserve the emulator core RIGHT NOW as a real allocation instead of
|
||||
* a pessimistic estimate: every byte of over-reservation here used to
|
||||
* cost cache slots (Pokemon got 2-3 slots for a 1 MB ROM). The block
|
||||
* is placement-constructed later. */
|
||||
app->gb_mem = safe_malloc(sizeof(Gameboy));
|
||||
if(!app->gb_mem) return RomLoadNoMem;
|
||||
|
||||
/* Allocate as many 8 KB page slots as the heap safely affords. Each
|
||||
* slot is its own allocation: no huge contiguous block is ever
|
||||
* requested, which makes the loader immune to heap fragmentation.
|
||||
* HEAP_RESERVE stays free for the GUI takeover + system services. */
|
||||
u32 switchable = (u32)(rc->banks - 1) * 2; /* 8 KB pages past bank 0 */
|
||||
const size_t reserve = HEAP_RESERVE;
|
||||
|
||||
/* slot bookkeeping arrays (a few bytes per slot) */
|
||||
size_t free_heap = memmgr_get_free_heap();
|
||||
u32 max_slots = (u32)(free_heap / BANK_SIZE) + 1;
|
||||
u32 max_slots = (u32)(free_heap / PAGE_SIZE) + 1;
|
||||
if(max_slots > switchable) max_slots = switchable;
|
||||
if(max_slots < 1) max_slots = 1;
|
||||
if(max_slots < 2) max_slots = 2;
|
||||
|
||||
rc->slots = (u8**)safe_malloc(max_slots * sizeof(u8*));
|
||||
rc->slot_bank = (u16*)safe_malloc(max_slots * sizeof(u16));
|
||||
rc->slot_page = (u16*)safe_malloc(max_slots * sizeof(u16));
|
||||
rc->slot_use = (u32*)safe_malloc(max_slots * sizeof(u32));
|
||||
if(!rc->slots || !rc->slot_bank || !rc->slot_use) return RomLoadNoMem;
|
||||
rc->slot_hits = (u16*)safe_malloc(max_slots * sizeof(u16));
|
||||
if(!rc->slots || !rc->slot_page || !rc->slot_use || !rc->slot_hits) return RomLoadNoMem;
|
||||
|
||||
rc->num_slots = 0;
|
||||
while((u32)rc->num_slots < max_slots) {
|
||||
if(memmgr_get_free_heap() < reserve + BANK_SIZE + ALLOC_MARGIN) break;
|
||||
u8* slot = (u8*)safe_malloc(BANK_SIZE);
|
||||
if(memmgr_get_free_heap() < reserve + PAGE_SIZE + ALLOC_MARGIN) break;
|
||||
u8* slot = (u8*)safe_malloc(PAGE_SIZE);
|
||||
if(!slot) break;
|
||||
rc->slots[rc->num_slots] = slot;
|
||||
rc->slot_bank[rc->num_slots] = 0; /* bank 0 never lives in a slot */
|
||||
rc->slot_page[rc->num_slots] = 0; /* page 0 never lives in a slot */
|
||||
rc->slot_use[rc->num_slots] = 0;
|
||||
rc->slot_hits[rc->num_slots] = 0;
|
||||
rc->num_slots++;
|
||||
}
|
||||
|
||||
/* not even one 16 KB slot fits: fail gracefully with the
|
||||
* "Not enough RAM" dialog instead of crashing inside malloc() */
|
||||
if(rc->num_slots == 0) return RomLoadNoMem;
|
||||
/* fewer than two 8 KB slots: fail gracefully with the "Not enough
|
||||
* RAM" dialog instead of crashing inside malloc(). Two is the hard
|
||||
* minimum: the cartridge maps a lo and a (lazy) hi page and the
|
||||
* provider guarantees the two most recently returned pages are never
|
||||
* evicted -- with a single slot the lo pointer would dangle. */
|
||||
if(rc->num_slots < 2) return RomLoadNoMem;
|
||||
|
||||
if((u32)rc->num_slots >= switchable) {
|
||||
/* every switchable bank fits: preload the whole ROM and close the
|
||||
/* every switchable page fits: preload the whole ROM and close the
|
||||
* SD file (O(1) bank switching, zero stutter, frees the handle) */
|
||||
storage_file_seek(rc->file, BANK_SIZE, true);
|
||||
for(u32 i = 0; i < switchable; i++) {
|
||||
size_t got = storage_file_read(rc->file, rc->slots[i], BANK_SIZE);
|
||||
if(got < BANK_SIZE) memset(rc->slots[i] + got, 0xFF, BANK_SIZE - got);
|
||||
rc->slot_bank[i] = (u16)(i + 1);
|
||||
size_t got = storage_file_read(rc->file, rc->slots[i], PAGE_SIZE);
|
||||
if(got < PAGE_SIZE) memset(rc->slots[i] + got, 0xFF, PAGE_SIZE - got);
|
||||
rc->slot_page[i] = (u16)(i + 2);
|
||||
}
|
||||
rc->fully_loaded = true;
|
||||
storage_file_close(rc->file);
|
||||
@@ -709,8 +803,9 @@ static void rom_free(AppState* app) {
|
||||
for(u16 i = 0; i < rc->num_slots; i++)
|
||||
if(rc->slots[i]) free(rc->slots[i]);
|
||||
if(rc->slots) free(rc->slots);
|
||||
if(rc->slot_bank) free(rc->slot_bank);
|
||||
if(rc->slot_page) free(rc->slot_page);
|
||||
if(rc->slot_use) free(rc->slot_use);
|
||||
if(rc->slot_hits) free(rc->slot_hits);
|
||||
if(rc->bank0) free(rc->bank0);
|
||||
if(rc->file) {
|
||||
storage_file_close(rc->file);
|
||||
@@ -778,18 +873,10 @@ extern "C" int32_t flipgb_app(void* p) {
|
||||
app->fb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
if(!app->fb_mutex) break;
|
||||
|
||||
/* rom_load reserved room for this, but double-check anyway: on the
|
||||
* Flipper an unchecked new/malloc crashes the firmware on OOM */
|
||||
void* gb_mem = safe_malloc(sizeof(Gameboy));
|
||||
if(!gb_mem) {
|
||||
DialogMessage* msg = dialog_message_alloc();
|
||||
dialog_message_set_text(msg, "Not enough RAM", 64, 30, AlignCenter, AlignCenter);
|
||||
dialog_message_set_buttons(msg, NULL, "OK", NULL);
|
||||
dialog_message_show(dialogs, msg);
|
||||
dialog_message_free(msg);
|
||||
break;
|
||||
}
|
||||
app->gb = new(gb_mem) Gameboy(
|
||||
/* rom_load pre-allocated this block (so the ROM cache could size
|
||||
* itself against real free heap, not an estimate) */
|
||||
furi_check(app->gb_mem != NULL);
|
||||
app->gb = new(app->gb_mem) Gameboy(
|
||||
app->rom.bank0,
|
||||
app->rom.banks,
|
||||
mbc,
|
||||
@@ -802,7 +889,7 @@ extern "C" int32_t flipgb_app(void* p) {
|
||||
|
||||
/* --- sound: piezo plays the dominant APU voice --- */
|
||||
app->speaker_acquired = furi_hal_speaker_acquire(50);
|
||||
app->sound_enabled = app->speaker_acquired;
|
||||
app->volume_setting = app->speaker_acquired ? 4 : 0;
|
||||
|
||||
/* --- take over the display --- */
|
||||
gui = (Gui*)furi_record_open(RECORD_GUI);
|
||||
@@ -846,6 +933,11 @@ extern "C" int32_t flipgb_app(void* p) {
|
||||
}
|
||||
furi_delay_ms(33);
|
||||
}
|
||||
/* the A/B press that operated the menu must not reach the
|
||||
* game; the request latch is cleared so held Up+Down (or
|
||||
* menu-navigation rollover) can't instantly reopen it */
|
||||
app->keys_blocked = (uint8_t)(app->keys & (KBIT_A | KBIT_B));
|
||||
app->menu_requested = false;
|
||||
frame_deadline_us = 0;
|
||||
last_now_ms = furi_get_tick();
|
||||
continue;
|
||||
@@ -867,11 +959,18 @@ extern "C" int32_t flipgb_app(void* p) {
|
||||
/* refresh the piezo with this frame's dominant APU voice */
|
||||
sound_update(app, false);
|
||||
|
||||
/* EMA of the cost of one emulated frame (x16 fixed point) */
|
||||
app->emu_ms_ema += ((emu_ms << 4) - app->emu_ms_ema) / 8;
|
||||
/* EMA of the cost of one emulated frame (x16 fixed point).
|
||||
* MUST be signed: with unsigned arithmetic, any frame faster
|
||||
* than the average underflowed the difference to ~4 billion,
|
||||
* blowing the EMA up and pinning auto-frameskip at maximum. */
|
||||
int32_t ema_diff = (int32_t)(emu_ms << 4) - (int32_t)app->emu_ms_ema;
|
||||
app->emu_ms_ema = (uint32_t)((int32_t)app->emu_ms_ema + ema_diff / 8);
|
||||
uint32_t ema_ms = app->emu_ms_ema >> 4;
|
||||
app->auto_skip = ema_ms <= 17 ? 0 : (int)((ema_ms - 1) / 17);
|
||||
if(app->auto_skip > 4) app->auto_skip = 4;
|
||||
/* auto mode may skip harder than the fixed 0-4 settings: for
|
||||
* heavy streamed games, correct game speed at a lower visible
|
||||
* fps beats slow motion */
|
||||
if(app->auto_skip > 8) app->auto_skip = 8;
|
||||
|
||||
if(render_this && !app->menu_active) {
|
||||
canvas_commit(canvas);
|
||||
@@ -925,6 +1024,9 @@ extern "C" int32_t flipgb_app(void* p) {
|
||||
if(app->gb) {
|
||||
app->gb->~Gameboy(); /* placement-new counterpart */
|
||||
free(app->gb);
|
||||
app->gb_mem = NULL;
|
||||
} else if(app->gb_mem) {
|
||||
free(app->gb_mem); /* rom_load reserved it but construction never ran */
|
||||
}
|
||||
rom_free(app);
|
||||
if(app->cart_ram) free(app->cart_ram);
|
||||
|
||||
@@ -13,12 +13,15 @@ def ProtoPirateDefineEnabled(name, app_manifest_path=app_manifest_path):
|
||||
|
||||
|
||||
_ENABLE_TIMING_TUNER = ProtoPirateDefineEnabled("ENABLE_TIMING_TUNER_SCENE")
|
||||
_ENABLE_EMULATE = ProtoPirateDefineEnabled("ENABLE_EMULATE_FEATURE")
|
||||
|
||||
_MAIN_APP_SOURCES = [
|
||||
"*.c*",
|
||||
"!protocols/plugins",
|
||||
"!scenes/plugins",
|
||||
"!protocols",
|
||||
"!protopirate_emulate_plugin.c",
|
||||
"!protopirate_psa_bf_plugin.c",
|
||||
"protocols/protocol_items.c",
|
||||
"protocols/protocols_common.c",
|
||||
"!raw_file_reader.c",
|
||||
@@ -30,6 +33,11 @@ if not _ENABLE_TIMING_TUNER:
|
||||
"!protocol_timings.c",
|
||||
]
|
||||
|
||||
if not _ENABLE_EMULATE:
|
||||
_MAIN_APP_SOURCES += [
|
||||
"!protopirate_scene_emulate.c",
|
||||
]
|
||||
|
||||
App(
|
||||
appid="proto_pirate",
|
||||
name="ProtoPirate",
|
||||
@@ -39,7 +47,7 @@ App(
|
||||
requires=["gui"],
|
||||
stack_size=8 * 1024,
|
||||
fap_description="Decode car key fob signals from Sub-GHz",
|
||||
fap_version="3.0",
|
||||
fap_version="3.2",
|
||||
fap_icon="images/protopirate_10px.png",
|
||||
fap_category="Sub-GHz",
|
||||
fap_icon_assets="images",
|
||||
@@ -165,7 +173,11 @@ def ProtoPirateTxProtocolPlugin(
|
||||
App=App,
|
||||
FlipperAppType=FlipperAppType,
|
||||
tx_plugin_sources=_TX_PLUGIN_SOURCES,
|
||||
enable_emulate=_ENABLE_EMULATE,
|
||||
):
|
||||
if not enable_emulate:
|
||||
return
|
||||
|
||||
App(
|
||||
appid=f"protopirate_tx_{key}_plugin",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
@@ -370,17 +382,18 @@ if _ENABLE_TIMING_TUNER:
|
||||
fal_embedded=True,
|
||||
)
|
||||
|
||||
App(
|
||||
appid="protopirate_emulate_plugin",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="protopirate_emulate_plugin_ep",
|
||||
requires=["proto_pirate"],
|
||||
sources=[
|
||||
"scenes/plugins/protopirate_emulate_plugin.c",
|
||||
"protocols/protocol_items.c",
|
||||
],
|
||||
fal_embedded=True,
|
||||
)
|
||||
if _ENABLE_EMULATE:
|
||||
App(
|
||||
appid="protopirate_emulate_plugin",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="protopirate_emulate_plugin_ep",
|
||||
requires=["proto_pirate"],
|
||||
sources=[
|
||||
"scenes/plugins/protopirate_emulate_plugin.c",
|
||||
"protocols/protocol_items.c",
|
||||
],
|
||||
fal_embedded=True,
|
||||
)
|
||||
|
||||
App(
|
||||
appid="protopirate_psa_bf_plugin",
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <string.h>
|
||||
|
||||
#define TAG "ProtoPirateProtocolPlugin"
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
#define PROTOPIRATE_TX_PLUGIN_PATH_MAX 160U
|
||||
#endif
|
||||
|
||||
static const char* protopirate_get_registry_plugin_path(ProtoPirateProtocolRegistryRoute route) {
|
||||
switch(route) {
|
||||
@@ -25,6 +27,7 @@ static const char* protopirate_get_registry_plugin_path(ProtoPirateProtocolRegis
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
static bool protopirate_build_tx_protocol_plugin_path(
|
||||
const char* tx_key,
|
||||
char* plugin_path,
|
||||
@@ -40,6 +43,7 @@ static bool protopirate_build_tx_protocol_plugin_path(
|
||||
tx_key);
|
||||
return (written > 0) && ((size_t)written < plugin_path_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
static const SubGhzProtocolRegistry protopirate_empty_protocol_registry = {
|
||||
.items = NULL,
|
||||
@@ -165,6 +169,7 @@ static bool protopirate_ensure_protocol_registry_plugin(
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
static bool protopirate_ensure_tx_protocol_plugin(
|
||||
ProtoPirateApp* app,
|
||||
const char* protocol_name,
|
||||
@@ -264,6 +269,7 @@ static bool protopirate_ensure_tx_protocol_plugin(
|
||||
*registry = plugin->registry;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool protopirate_refresh_protocol_registry(ProtoPirateApp* app, bool ensure_receiver_ready) {
|
||||
furi_check(app);
|
||||
@@ -378,6 +384,7 @@ bool protopirate_apply_protocol_registry_for_context(
|
||||
}
|
||||
|
||||
if(protocol_name && protocol_name[0] != '\0') {
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
const char* registry_name = protopirate_protocol_catalog_canonical_name(protocol_name);
|
||||
if(!registry_name || !protopirate_protocol_catalog_can_tx(protocol_name)) {
|
||||
FURI_LOG_E(TAG, "No TX protocol plugin for %s", protocol_name);
|
||||
@@ -410,6 +417,9 @@ bool protopirate_apply_protocol_registry_for_context(
|
||||
subghz_environment_set_protocol_registry(app->txrx->environment, tx_registry);
|
||||
app->txrx->protocol_registry = tx_registry;
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
ProtoPirateProtocolRegistryRoute route = protopirate_get_protocol_registry_route(
|
||||
|
||||
@@ -103,12 +103,14 @@ void protopirate_settings_load(ProtoPirateSettings* settings) {
|
||||
}
|
||||
settings->hopping_enabled = (hopping_temp == 1);
|
||||
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
uint32_t emulate_temp = 0;
|
||||
if(!flipper_format_read_uint32(ff, "EmulateFeature", &emulate_temp, 1)) {
|
||||
FURI_LOG_I(TAG, "EmulateFeature key missing, defaulting to disabled");
|
||||
emulate_temp = 0;
|
||||
}
|
||||
settings->emulate_feature_enabled = (emulate_temp == 1);
|
||||
#endif
|
||||
|
||||
uint32_t check_saved_temp = 0;
|
||||
if(!flipper_format_read_uint32(ff, "CheckSaved", &check_saved_temp, 1)) {
|
||||
@@ -185,11 +187,13 @@ void protopirate_settings_save(ProtoPirateSettings* settings) {
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
uint32_t emulate_temp = settings->emulate_feature_enabled ? 1 : 0;
|
||||
if(!flipper_format_write_uint32(ff, "EmulateFeature", &emulate_temp, 1)) {
|
||||
FURI_LOG_E(TAG, "Failed to write emulate feature flag");
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t check_saved_temp = settings->check_saved ? 1 : 0;
|
||||
if(!flipper_format_write_uint32(ff, "CheckSaved", &check_saved_temp, 1)) {
|
||||
|
||||
@@ -513,6 +513,20 @@ static bool protopirate_storage_copy_key_2(
|
||||
protopirate_storage_copy_u32_optional(save_file, flipper_format, "Key_2");
|
||||
}
|
||||
|
||||
static bool protopirate_storage_copy_key2(
|
||||
FlipperFormat* save_file,
|
||||
FlipperFormat* flipper_format) {
|
||||
bool copied = false;
|
||||
if(!protopirate_storage_copy_hex_fixed(save_file, flipper_format, "Key2", 8, &copied)) {
|
||||
return false;
|
||||
}
|
||||
if(copied) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return protopirate_storage_copy_hex_or_u32(save_file, flipper_format, "Key2", 4);
|
||||
}
|
||||
|
||||
static bool protopirate_storage_write_capture_data(
|
||||
FlipperFormat* save_file,
|
||||
FlipperFormat* flipper_format) {
|
||||
@@ -548,7 +562,7 @@ static bool protopirate_storage_write_capture_data(
|
||||
protopirate_storage_base_u32_fields,
|
||||
COUNT_OF(protopirate_storage_base_u32_fields)))
|
||||
break;
|
||||
if(!protopirate_storage_copy_hex_or_u32(save_file, flipper_format, "Key2", 4)) break;
|
||||
if(!protopirate_storage_copy_key2(save_file, flipper_format)) break;
|
||||
if(!protopirate_storage_copy_u32_optional(save_file, flipper_format, "KeyIdx")) break;
|
||||
if(!protopirate_storage_copy_u32_optional(save_file, flipper_format, "Seed")) break;
|
||||
if(!protopirate_storage_copy_hex_or_u32(save_file, flipper_format, "ValidationField", 2))
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <furi.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../defines.h"
|
||||
|
||||
#define TAG "ProtoPirateCatalog"
|
||||
|
||||
#define PROTOPIRATE_CC1101_REG_MDMCFG2 0x12U
|
||||
@@ -17,6 +19,12 @@
|
||||
|
||||
#define PROTOPIRATE_COUNT_OF(array) (sizeof(array) / sizeof((array)[0]))
|
||||
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
#define PROTOPIRATE_TX_KEY(key) key
|
||||
#else
|
||||
#define PROTOPIRATE_TX_KEY(key) NULL
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
ProtoPirateProtocolCatalogModulationAM = 0,
|
||||
ProtoPirateProtocolCatalogModulationFM,
|
||||
@@ -28,33 +36,33 @@ typedef struct {
|
||||
} ProtoPirateProtocolCatalogAlias;
|
||||
|
||||
static const ProtoPirateProtocolCatalogEntry protopirate_protocol_catalog[] = {
|
||||
{"Chrysler V0", ProtoPirateProtocolCatalogRouteAMDefault, "chrysler_v0"},
|
||||
{"Fiat V0", ProtoPirateProtocolCatalogRouteAMDefault, "fiat_v0"},
|
||||
{"Fiat V1", ProtoPirateProtocolCatalogRouteAMDefault, "fiat_v1"},
|
||||
{"Chrysler V0", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("chrysler_v0")},
|
||||
{"Fiat V0", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("fiat_v0")},
|
||||
{"Fiat V1", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("fiat_v1")},
|
||||
{"Fiat V2", ProtoPirateProtocolCatalogRouteAMDefault, NULL},
|
||||
{"Ford V0", ProtoPirateProtocolCatalogRouteAMDefault, "ford_v0"},
|
||||
{"Ford V1", ProtoPirateProtocolCatalogRouteFMF4, "ford_v1"},
|
||||
{"Ford V2", ProtoPirateProtocolCatalogRouteFMF4, "ford_v2"},
|
||||
{"Ford V0", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("ford_v0")},
|
||||
{"Ford V1", ProtoPirateProtocolCatalogRouteFMF4, PROTOPIRATE_TX_KEY("ford_v1")},
|
||||
{"Ford V2", ProtoPirateProtocolCatalogRouteFMF4, PROTOPIRATE_TX_KEY("ford_v2")},
|
||||
{"Ford V3", ProtoPirateProtocolCatalogRouteFMF4, NULL},
|
||||
{"Honda Static", ProtoPirateProtocolCatalogRouteFMHonda1, "honda_static"},
|
||||
{"Honda V1", ProtoPirateProtocolCatalogRouteAMDefault, "honda_v1"},
|
||||
{"Kia V0", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v0"},
|
||||
{"Kia V1", ProtoPirateProtocolCatalogRouteAMDefault, "kia_v1"},
|
||||
{"Kia V2", ProtoPirateProtocolCatalogRouteAMDefault, "kia_v2"},
|
||||
{"Kia V3/V4", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v3_v4"},
|
||||
{"Kia V5", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v5"},
|
||||
{"Kia V6", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v6"},
|
||||
{"Kia V7", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v7"},
|
||||
{"Honda V2", ProtoPirateProtocolCatalogRouteFMF4, "honda_v2"},
|
||||
{"Mazda V0", ProtoPirateProtocolCatalogRouteByModulation, "mazda_v0"},
|
||||
{"Honda Static", ProtoPirateProtocolCatalogRouteFMHonda1, PROTOPIRATE_TX_KEY("honda_static")},
|
||||
{"Honda V1", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("honda_v1")},
|
||||
{"Kia V0", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v0")},
|
||||
{"Kia V1", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("kia_v1")},
|
||||
{"Kia V2", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("kia_v2")},
|
||||
{"Kia V3/V4", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v3_v4")},
|
||||
{"Kia V5", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v5")},
|
||||
{"Kia V6", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v6")},
|
||||
{"Kia V7", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v7")},
|
||||
{"Honda V2", ProtoPirateProtocolCatalogRouteFMF4, PROTOPIRATE_TX_KEY("honda_v2")},
|
||||
{"Mazda V0", ProtoPirateProtocolCatalogRouteByModulation, PROTOPIRATE_TX_KEY("mazda_v0")},
|
||||
{"Mitsubishi V0", ProtoPirateProtocolCatalogRouteFMDefault, NULL},
|
||||
{"Porsche Touareg", ProtoPirateProtocolCatalogRouteAMDefault, NULL},
|
||||
{"PSA", ProtoPirateProtocolCatalogRouteByModulation, "psa"},
|
||||
{"Renault V0", ProtoPirateProtocolCatalogRouteAMDefault, "renault_v0"},
|
||||
{"PSA", ProtoPirateProtocolCatalogRouteByModulation, PROTOPIRATE_TX_KEY("psa")},
|
||||
{"Renault V0", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("renault_v0")},
|
||||
{"Scher-Khan", ProtoPirateProtocolCatalogRouteFMDefault, NULL},
|
||||
{"Star Line", ProtoPirateProtocolCatalogRouteAMDefault, "star_line"},
|
||||
{"Subaru", ProtoPirateProtocolCatalogRouteAMDefault, "subaru"},
|
||||
{"VAG", ProtoPirateProtocolCatalogRouteAMVag, "vag"},
|
||||
{"Star Line", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("star_line")},
|
||||
{"Subaru", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("subaru")},
|
||||
{"VAG", ProtoPirateProtocolCatalogRouteAMVag, PROTOPIRATE_TX_KEY("vag")},
|
||||
};
|
||||
|
||||
static const ProtoPirateProtocolCatalogAlias protopirate_protocol_catalog_aliases[] = {
|
||||
|
||||
@@ -90,7 +90,11 @@ ProtoPirateApp* protopirate_app_alloc() {
|
||||
app->auto_save = settings.auto_save;
|
||||
app->check_saved = settings.check_saved;
|
||||
app->tx_power = settings.tx_power;
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
app->emulate_feature_enabled = settings.emulate_feature_enabled;
|
||||
#else
|
||||
app->emulate_feature_enabled = false;
|
||||
#endif
|
||||
|
||||
// Init setting - KEEP THIS, it's small
|
||||
app->setting = subghz_setting_alloc();
|
||||
@@ -175,7 +179,11 @@ void protopirate_app_free(ProtoPirateApp* app) {
|
||||
settings.check_saved = app->check_saved;
|
||||
settings.tx_power = app->tx_power;
|
||||
settings.hopping_enabled = (app->txrx->hopper_state != ProtoPirateHopperStateOFF);
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
settings.emulate_feature_enabled = app->emulate_feature_enabled;
|
||||
#else
|
||||
settings.emulate_feature_enabled = false;
|
||||
#endif
|
||||
|
||||
// Find current preset index
|
||||
settings.preset_index = 0;
|
||||
@@ -273,14 +281,18 @@ int32_t protopirate_app(char* p) {
|
||||
|
||||
//We now jump straight to emulate scene from Browser. If the user wanted the key to look at, just click back.
|
||||
if(load_saved) {
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
if(protopirate_app->emulate_feature_enabled) {
|
||||
view_dispatcher_send_custom_event(
|
||||
protopirate_app->view_dispatcher, ProtoPirateCustomEventSavedInfoEmulate);
|
||||
notification_message(protopirate_app->notifications, &sequence_success);
|
||||
} else {
|
||||
#endif
|
||||
view_dispatcher_send_custom_event(
|
||||
protopirate_app->view_dispatcher, ProtoPirateCustomEventReceiverInfoSave);
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
view_dispatcher_run(protopirate_app->view_dispatcher);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#define CREDIT_LINE_HEIGHT 10
|
||||
#define SCROLL_SPEED 1
|
||||
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
static const InputKey EMULATE_TOGGLE_COMBO[] = {
|
||||
InputKeyUp,
|
||||
InputKeyUp,
|
||||
@@ -23,6 +24,7 @@ static const InputKey EMULATE_TOGGLE_COMBO[] = {
|
||||
InputKeyRight,
|
||||
};
|
||||
#define EMULATE_TOGGLE_COMBO_LEN (sizeof(EMULATE_TOGGLE_COMBO) / sizeof(EMULATE_TOGGLE_COMBO[0]))
|
||||
#endif
|
||||
|
||||
static const char* credits[] = {
|
||||
"",
|
||||
@@ -62,7 +64,9 @@ typedef struct {
|
||||
uint8_t frame;
|
||||
uint8_t seed;
|
||||
int16_t scroll_offset;
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
uint8_t combo_progress;
|
||||
#endif
|
||||
} GlitchState;
|
||||
|
||||
static GlitchState g_state = {0};
|
||||
@@ -159,6 +163,7 @@ static void about_draw_callback(Canvas* canvas, void* context) {
|
||||
|
||||
static bool about_input_callback(InputEvent* event, void* context) {
|
||||
furi_check(context);
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
ProtoPirateApp* app = context;
|
||||
|
||||
if(event->type != InputTypePress) {
|
||||
@@ -184,8 +189,13 @@ static bool about_input_callback(InputEvent* event, void* context) {
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
UNUSED(event);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
static void about_show_emulate_toggle_popup(ProtoPirateApp* app) {
|
||||
const bool now_enabled = app->emulate_feature_enabled;
|
||||
|
||||
@@ -204,6 +214,7 @@ static void about_show_emulate_toggle_popup(ProtoPirateApp* app) {
|
||||
dialog_message_show(app->dialogs, message);
|
||||
dialog_message_free(message);
|
||||
}
|
||||
#endif
|
||||
|
||||
void protopirate_scene_about_on_enter(void* context) {
|
||||
furi_check(context);
|
||||
@@ -218,7 +229,9 @@ void protopirate_scene_about_on_enter(void* context) {
|
||||
g_state.frame = 0;
|
||||
g_state.seed = furi_get_tick() & 0xFF;
|
||||
g_state.scroll_offset = 0;
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
g_state.combo_progress = 0;
|
||||
#endif
|
||||
|
||||
view_set_draw_callback(app->view_about, about_draw_callback);
|
||||
view_set_input_callback(app->view_about, about_input_callback);
|
||||
@@ -245,7 +258,9 @@ bool protopirate_scene_about_on_event(void* context, SceneManagerEvent event) {
|
||||
|
||||
view_commit_model(app->view_about, true);
|
||||
consumed = true;
|
||||
} else if(event.type == SceneManagerEventTypeCustom) {
|
||||
}
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
else if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == ProtoPirateCustomEventAboutToggleEmulate) {
|
||||
app->emulate_feature_enabled = !app->emulate_feature_enabled;
|
||||
|
||||
@@ -262,6 +277,7 @@ bool protopirate_scene_about_on_event(void* context, SceneManagerEvent event) {
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
@@ -1205,7 +1205,9 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
|
||||
app);
|
||||
|
||||
ctx->signal_info_left_is_emulate = false;
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
bool left_button_used = false;
|
||||
#endif
|
||||
app->emulate_disabled_for_loaded = true;
|
||||
|
||||
// Store reference to history item's flipper format for saving
|
||||
@@ -1239,7 +1241,9 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
|
||||
"Brute force",
|
||||
protopirate_scene_sub_decode_widget_callback,
|
||||
app);
|
||||
#ifdef ENABLE_EMULATE_FEATURE
|
||||
left_button_used = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@ import hashlib
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from collections import defaultdict
|
||||
@@ -26,6 +27,10 @@ def replace_file_with_retry(source: str, target: str, attempts: int = 50) -> Non
|
||||
if attempt == attempts - 1:
|
||||
raise
|
||||
time.sleep(0.1)
|
||||
except OSError:
|
||||
shutil.copy2(source, target)
|
||||
os.remove(source)
|
||||
return
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -147,7 +152,7 @@ class Main(App):
|
||||
RelSection(section_name, section.name, unique_relocations)
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with tempfile.TemporaryDirectory(dir=os.path.dirname(os.path.abspath(fap_path))) as temp_dir:
|
||||
current_fap_path = fap_path
|
||||
|
||||
for section_index, section in enumerate(sections):
|
||||
|
||||
Reference in New Issue
Block a user