Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to use a 'cycles target' #346

Merged
merged 22 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
208f110
Change TEST_CI to TEST_VERBOSE_IMAGES and invert semantic
Baekalfen Sep 23, 2024
9c8a7ee
Try to quit debug window better
Baekalfen Sep 20, 2024
9407c38
Remove redundant definitions in sound.pxd
Baekalfen Sep 16, 2024
6e6e0d8
Restructure CPU interrupt handling
Baekalfen Sep 17, 2024
2d06208
Simplify CGB bank read in MB getitem
Baekalfen Sep 16, 2024
de50027
Fix blargg tests for CPU without is_stuck
Baekalfen Jul 29, 2024
f1faa97
Add places to bail in MB and opcodes but not CPU
Baekalfen Sep 29, 2024
b32e627
Implement cycles target for CPU with support to bail
Baekalfen Sep 29, 2024
4b6a053
Refactor LCD and Timer cycles_to_interrupts
Baekalfen Sep 29, 2024
cecef34
Implement LCD cycles to frame
Baekalfen Sep 29, 2024
4310f98
Only pre-check interrupts and halt in CPU tick
Baekalfen Sep 17, 2024
5f7fcaf
Refactor sound ticking to have a tick method
Baekalfen Sep 16, 2024
0c15c68
Fix CPU clock cycles for CB 46, CB 4E, CB 56, CB 5E, CB 66, CB 6E, CB…
Baekalfen Sep 25, 2024
118b715
Fix memory timings, supporting sub-opcode timing
Baekalfen Sep 25, 2024
b95c2cb
Fix import order in opcodes_gen.py
Baekalfen Sep 25, 2024
3292f7a
Defer post-tick and reduce time keeping on tick
Baekalfen Sep 29, 2024
0724cb0
Update whichboot pytest
Baekalfen Sep 14, 2024
5e8cf7e
Saving SameSuite and Blargg results for sound, although they are stil…
Baekalfen Sep 26, 2024
3451731
Saving Blargg interrupt time results as they depend on sound, and are…
Baekalfen Sep 26, 2024
6459839
Saving Pokemon Pinball test even though it broke because of timing ch…
Baekalfen Sep 26, 2024
a6a895a
Saving Tetris example as timings (and randomness) have changed
Baekalfen Sep 26, 2024
44630cb
Update docs
Baekalfen Sep 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- name: Run PyTest
env:
PYTEST_SECRETS_KEY: ${{ secrets.PYTEST_SECRETS_KEY }}
TEST_CI: 1
TEST_VERBOSE_IMAGES: 0
TEST_NO_UI: 1
run: |
python -m pytest tests/ -n auto -v
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:
- name: Run PyTest
env:
PYTEST_SECRETS_KEY: ${{ secrets.PYTEST_SECRETS_KEY }}
TEST_CI: 1
TEST_VERBOSE_IMAGES: 0
TEST_NO_UI: 1
run: |
pypy3 -m pytest tests/ -n auto -v
Expand Down
46 changes: 27 additions & 19 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,8 @@ <h2 id="kwargs">Kwargs</h2>
raise KeyError(f&#34;Unknown keyword argument: {k}&#34;)

# Performance measures
self.avg_pre = 0
self.avg_tick = 0
self.avg_post = 0
self.avg_emu = 0

# Absolute frame count of the emulation
self.frame_count = 0
Expand Down Expand Up @@ -448,9 +447,7 @@ <h2 id="kwargs">Kwargs</h2>
if self.stopped:
return False

t_start = time.perf_counter_ns()
self._handle_events(self.events)
t_pre = time.perf_counter_ns()
if not self.paused:
self.gameshark.tick()
self.__rendering(render)
Expand All @@ -477,18 +474,7 @@ <h2 id="kwargs">Kwargs</h2>
self.mb.breakpoint_singlestep = self.mb.breakpoint_singlestep_latch

self.frame_count += 1
t_tick = time.perf_counter_ns()
self._post_tick()
t_post = time.perf_counter_ns()

nsecs = t_pre - t_start
self.avg_pre = 0.9 * self.avg_pre + (0.1*nsecs/1_000_000_000)

nsecs = t_tick - t_pre
self.avg_tick = 0.9 * self.avg_tick + (0.1*nsecs/1_000_000_000)

nsecs = t_post - t_tick
self.avg_post = 0.9 * self.avg_post + (0.1*nsecs/1_000_000_000)
self._post_handle_events()

return not self.quitting

Expand Down Expand Up @@ -537,11 +523,22 @@ <h2 id="kwargs">Kwargs</h2>
False if emulation has ended otherwise True
&#34;&#34;&#34;

_count = count
running = False
t_start = time.perf_counter_ns()
while count != 0:
_render = render and count == 1 # Only render on last tick to improve performance
running = self._tick(_render)
count -= 1
t_tick = time.perf_counter_ns()
self._post_tick()
t_post = time.perf_counter_ns()

if _count &gt; 0:
nsecs = t_tick - t_start
self.avg_tick = 0.9 * (self.avg_tick / _count) + (0.1*nsecs/1_000_000_000)
nsecs = t_post - t_start
self.avg_emu = 0.9 * (self.avg_emu / _count) + (0.1*nsecs/1_000_000_000)
return running

def _handle_events(self, events):
Expand Down Expand Up @@ -608,16 +605,16 @@ <h2 id="kwargs">Kwargs</h2>
self._plugin_manager.post_tick()
self._plugin_manager.frame_limiter(self.target_emulationspeed)

def _post_handle_events(self):
# Prepare an empty list, as the API might be used to send in events between ticks
self.events = []
while self.queued_input and self.frame_count == self.queued_input[0][0]:
_, _event = heapq.heappop(self.queued_input)
self.events.append(WindowEvent(_event))

def _update_window_title(self):
avg_emu = self.avg_pre + self.avg_tick + self.avg_post
self.window_title = f&#34;CPU/frame: {(self.avg_pre + self.avg_tick) / SPF * 100:0.2f}%&#34;
self.window_title += f&#39; Emulation: x{(round(SPF / avg_emu) if avg_emu &gt; 0 else &#34;INF&#34;)}&#39;
self.window_title = f&#34;CPU/frame: {(self.avg_tick) / SPF * 100:0.2f}%&#34;
self.window_title += f&#39; Emulation: x{(round(SPF / self.avg_emu) if self.avg_emu &gt; 0 else &#34;INF&#34;)}&#39;
if self.paused:
self.window_title += &#34;[PAUSED]&#34;
self.window_title += self._plugin_manager.window_title()
Expand Down Expand Up @@ -1608,11 +1605,22 @@ <h2 id="returns">Returns</h2>
False if emulation has ended otherwise True
&#34;&#34;&#34;

_count = count
running = False
t_start = time.perf_counter_ns()
while count != 0:
_render = render and count == 1 # Only render on last tick to improve performance
running = self._tick(_render)
count -= 1
t_tick = time.perf_counter_ns()
self._post_tick()
t_post = time.perf_counter_ns()

if _count &gt; 0:
nsecs = t_tick - t_start
self.avg_tick = 0.9 * (self.avg_tick / _count) + (0.1*nsecs/1_000_000_000)
nsecs = t_post - t_start
self.avg_emu = 0.9 * (self.avg_emu / _count) + (0.1*nsecs/1_000_000_000)
return running</code></pre>
</details>
</dd>
Expand Down
2 changes: 1 addition & 1 deletion docs/utils.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ <h1 class="title">Module <code>pyboy.utils</code></h1>

__all__ = [&#34;WindowEvent&#34;, &#34;dec_to_bcd&#34;, &#34;bcd_to_dec&#34;]

STATE_VERSION = 11
STATE_VERSION = 12

##############################################################
# Buffer classes
Expand Down
2 changes: 1 addition & 1 deletion extras/examples/gamewrapper_tetris.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
tetris.start_game(timer_div=0x00) # The timer_div works like a random seed in Tetris

tetromino_at_0x00 = tetris.next_tetromino()
assert tetromino_at_0x00 == "Z", tetris.next_tetromino()
assert tetromino_at_0x00 == "O", tetris.next_tetromino()
assert tetris.score == 0
assert tetris.level == 0
assert tetris.lines == 0
Expand Down
2 changes: 1 addition & 1 deletion pyboy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def main():
pyboy.load_state(f)

render = not argv.no_renderer
while pyboy._tick(render):
while pyboy.tick():
pass

pyboy.stop()
Expand Down
9 changes: 6 additions & 3 deletions pyboy/core/cpu.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#


from libc.stdint cimport int16_t, uint8_t, uint16_t, uint32_t, uint64_t
from libc.stdint cimport int16_t, int64_t, uint8_t, uint16_t, int64_t

cimport pyboy.core.mb
from pyboy.utils cimport IntIOInterface
Expand All @@ -26,17 +26,20 @@ cdef uint8_t INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW

cdef class CPU:
cdef bint is_stuck
cdef bint interrupt_master_enable, interrupt_queued, halted, stopped
cdef bint interrupt_master_enable, interrupt_queued, halted, stopped, bail

cdef uint8_t interrupts_flag, interrupts_enabled, interrupts_flag_register, interrupts_enabled_register

cdef int64_t cycles

cdef inline int check_interrupts(self) noexcept nogil
cdef void set_interruptflag(self, int) noexcept nogil
cdef bint handle_interrupt(self, uint8_t, uint16_t) noexcept nogil

@cython.locals(opcode=uint16_t)
cdef inline uint8_t fetch_and_execute(self) noexcept nogil
cdef int tick(self) noexcept nogil
@cython.locals(_cycles0=int64_t)
cdef int tick(self, int64_t) noexcept nogil
cdef void save_state(self, IntIOInterface) noexcept
cdef void load_state(self, IntIOInterface, int) noexcept

Expand Down
89 changes: 44 additions & 45 deletions pyboy/core/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(self, mb):
self.halted = False
self.stopped = False
self.is_stuck = False
self.cycles = 0

def save_state(self, f):
for n in [self.A, self.F, self.B, self.C, self.D, self.E]:
Expand All @@ -53,6 +54,7 @@ def save_state(self, f):
f.write(self.interrupts_enabled_register)
f.write(self.interrupt_queued)
f.write(self.interrupts_flag_register)
f.write_64bit(self.cycles)

def load_state(self, f, state_version):
self.A, self.F, self.B, self.C, self.D, self.E = [f.read() for _ in range(6)]
Expand All @@ -69,6 +71,8 @@ def load_state(self, f, state_version):
if state_version >= 8:
self.interrupt_queued = f.read()
self.interrupts_flag_register = f.read()
if state_version >= 12:
self.cycles = f.read_64bit()
logger.debug("State loaded: %s", self.dump_state(""))

def dump_state(self, sym_label):
Expand All @@ -93,8 +97,8 @@ def dump_state(self, sym_label):
f"Interrupts - IME: {self.mb.cpu.interrupt_master_enable}, "
f"IE: {self.mb.cpu.interrupts_enabled_register:08b}, "
f"IF: {self.mb.cpu.interrupts_flag_register:08b}\n"
f"LCD Intr.: {self.mb.lcd.cycles_to_interrupt()}, LY:{self.mb.lcd.LY}, LYC:{self.mb.lcd.LYC}\n"
f"Timer Intr.: {self.mb.timer.cycles_to_interrupt()}\n"
f"LCD Intr.: {self.mb.lcd._cycles_to_interrupt}, LY:{self.mb.lcd.LY}, LYC:{self.mb.lcd.LYC}\n"
f"Timer Intr.: {self.mb.timer._cycles_to_interrupt}\n"
f"halted:{self.halted}, "
f"interrupt_queued:{self.interrupt_queued}, "
f"stopped:{self.stopped}\n"
Expand All @@ -103,11 +107,13 @@ def dump_state(self, sym_label):
def set_interruptflag(self, flag):
self.interrupts_flag_register |= flag

def tick(self):
def tick(self, cycles_target):
_cycles0 = self.cycles
_target = _cycles0 + cycles_target

if self.check_interrupts():
self.halted = False
# TODO: We return with the cycles it took to handle the interrupt
return 0
# TODO: Cycles it took to handle the interrupt

if self.halted and self.interrupt_queued:
# GBCPUman.pdf page 20
Expand All @@ -117,62 +123,55 @@ def tick(self):
self.PC += 1
self.PC &= 0xFFFF
elif self.halted:
return 4 # TODO: Number of cycles for a HALT in effect?

old_pc = self.PC # If the PC doesn't change, we're likely stuck
old_sp = self.SP # Sometimes a RET can go to the same PC, so we check the SP too.
cycles = self.fetch_and_execute()
if not self.halted and old_pc == self.PC and old_sp == self.SP and not self.is_stuck and not self.mb.breakpoint_singlestep:
logger.debug("CPU is stuck: %s", self.dump_state(""))
self.is_stuck = True
self.cycles += cycles_target # TODO: Number of cycles for a HALT in effect?
self.interrupt_queued = False
return cycles

self.bail = False
while self.cycles < _target:
# TODO: cpu-stuck check for blargg tests?
self.fetch_and_execute()
if self.bail: # Possible cycles-target changes
break

def check_interrupts(self):
if self.interrupt_queued:
# Interrupt already queued. This happens only when using a debugger.
return False

if (self.interrupts_flag_register & 0b11111) & (self.interrupts_enabled_register & 0b11111):
if self.handle_interrupt(INTR_VBLANK, 0x0040):
self.interrupt_queued = True
elif self.handle_interrupt(INTR_LCDC, 0x0048):
self.interrupt_queued = True
elif self.handle_interrupt(INTR_TIMER, 0x0050):
self.interrupt_queued = True
elif self.handle_interrupt(INTR_SERIAL, 0x0058):
self.interrupt_queued = True
elif self.handle_interrupt(INTR_HIGHTOLOW, 0x0060):
self.interrupt_queued = True
else:
logger.error("No interrupt triggered, but it should!")
self.interrupt_queued = False
return True
else:
self.interrupt_queued = False
return False

def handle_interrupt(self, flag, addr):
if (self.interrupts_enabled_register & flag) and (self.interrupts_flag_register & flag):
raised_and_enabled = (self.interrupts_flag_register & 0b11111) & (self.interrupts_enabled_register & 0b11111)
if raised_and_enabled:
# Clear interrupt flag
if self.halted:
self.PC += 1 # Escape HALT on return
self.PC &= 0xFFFF

# Handle interrupt vectors
if self.interrupt_master_enable:
self.interrupts_flag_register ^= flag # Remove flag
self.mb.setitem((self.SP - 1) & 0xFFFF, self.PC >> 8) # High
self.mb.setitem((self.SP - 2) & 0xFFFF, self.PC & 0xFF) # Low
self.SP -= 2
self.SP &= 0xFFFF

self.PC = addr
self.interrupt_master_enable = False

if raised_and_enabled & INTR_VBLANK:
self.handle_interrupt(INTR_VBLANK, 0x0040)
elif raised_and_enabled & INTR_LCDC:
self.handle_interrupt(INTR_LCDC, 0x0048)
elif raised_and_enabled & INTR_TIMER:
self.handle_interrupt(INTR_TIMER, 0x0050)
elif raised_and_enabled & INTR_SERIAL:
self.handle_interrupt(INTR_SERIAL, 0x0058)
elif raised_and_enabled & INTR_HIGHTOLOW:
self.handle_interrupt(INTR_HIGHTOLOW, 0x0060)
self.interrupt_queued = True
return True
else:
self.interrupt_queued = False
return False

def handle_interrupt(self, flag, addr):
self.interrupts_flag_register ^= flag # Remove flag
self.mb.setitem((self.SP - 1) & 0xFFFF, self.PC >> 8) # High
self.mb.setitem((self.SP - 2) & 0xFFFF, self.PC & 0xFF) # Low
self.SP -= 2
self.SP &= 0xFFFF

self.PC = addr
self.interrupt_master_enable = False

def fetch_and_execute(self):
opcode = self.mb.getitem(self.PC)
if opcode == 0xCB: # Extension code
Expand Down
3 changes: 2 additions & 1 deletion pyboy/core/lcd.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ cdef class LCD:
cdef PaletteRegister OBP1
cdef Renderer renderer
cdef uint8_t[144][5] _scanlineparameters
cdef uint64_t last_cycles
cdef int64_t _cycles_to_interrupt, _cycles_to_frame

@cython.locals(interrupt_flag=uint8_t,bx=int,by=int,wx=int,wy=int)
cdef uint8_t tick(self, int) noexcept nogil
cdef int64_t cycles_to_interrupt(self) noexcept nogil

cdef void set_lcdc(self, uint8_t) noexcept nogil
cdef uint8_t get_lcdc(self) noexcept nogil
Expand Down
Loading
Loading