refactor out frontend to drive the emu frame-by-frame, add pause/sync

Now the Frontend drives the Emu, which is wrapped with an Accessor which
will eventually define which actions are available per Emu, and wrap
around calls into that Emu.

Added the ability to pause and toggle audio syncing from the menu bar.
This commit is contained in:
Matthew Berry 2021-06-16 19:39:37 -07:00
parent c7bc2c31f9
commit fca039bd12
14 changed files with 336 additions and 45 deletions

View file

@ -8,6 +8,8 @@ require "imgui-backends"
require "imgui-backends/lib"
require "./crab/common/*"
require "./crab/common/frontend/*"
require "./crab/gb"
require "./crab/gba"
@ -51,7 +53,9 @@ module Crab
emu = GB::GB.new bios, rom.not_nil!, fifo, headless
end
emu.post_init
emu.run
frontend = Frontend.new(emu)
frontend.run
end
end

View file

@ -3,28 +3,7 @@ abstract class Emu
end
abstract def scheduler : Scheduler
abstract def run : Nil
abstract def run_until_frame : Nil
abstract def handle_event(event : SDL::Event) : Nil
abstract def toggle_sync : Nil
abstract def toggle_blending : Nil
def handle_events(interval : Int) : Nil
scheduler.schedule interval, Proc(Nil).new { handle_events interval }
while event = SDL::Event.poll
ImGui::SDL2.process_event(event)
case event
when SDL::Event::Quit then exit 0
when SDL::Event::JoyHat,
SDL::Event::JoyButton then handle_event(event)
when SDL::Event::Keyboard
case event.sym
when .tab? then toggle_sync if event.pressed?
when .m? then toggle_blending if event.pressed?
when .q? then exit 0
else handle_event(event)
end
else nil
end
end
end
end

View file

@ -0,0 +1,13 @@
require "lib_gl"
abstract class Frontend
def self.new(emu : Emu, headless = false)
if headless
HeadlessFrontend.new(emu)
else
SDLOpenGLImGuiFrontend.new(emu)
end
end
abstract def run : NoReturn
end

View file

@ -0,0 +1,7 @@
abstract class Accessor
abstract def width : Int32
abstract def height : Int32
abstract def shader : String
abstract def get_framebuffer : Slice(UInt16)
end

View file

@ -0,0 +1,12 @@
class GBAccessor < Accessor
getter width : Int32 = 160
getter height : Int32 = 144
getter shader : String = "gb_colors.frag"
def initialize(@gb : GB::GB)
end
def get_framebuffer : Slice(UInt16)
@gb.ppu.framebuffer
end
end

View file

@ -0,0 +1,12 @@
class GBAAccessor < Accessor
getter width : Int32 = 240
getter height : Int32 = 160
getter shader : String = "gba_colors.frag"
def initialize(@gba : GBA::GBA)
end
def get_framebuffer : Slice(UInt16)
@gba.ppu.framebuffer
end
end

View file

@ -0,0 +1,10 @@
class HeadlessFrontend < Frontend
def initialize(@emu : Emu)
end
def run : NoReturn
loop do
@emu.run_until_frame
end
end
end

View file

@ -0,0 +1,263 @@
require "./console_accessors/*"
class SDLOpenGLImGuiFrontend < Frontend
SCALE = 4
SHADERS = "src/crab/common/shaders"
@window : SDL::Window
@gl_context : LibSDL::GLContext
@io : ImGui::ImGuiIO
@microseconds = 0
@frames = 0
@last_time = Time.utc
@seconds : Int32 = Time.utc.second
@enable_blend = false
@blending = false
@enable_overlay = false
@pause = false
@sync = true
@opengl_info : OpenGLInfo
def initialize(@emu : Emu)
@accessor = case emu
in GB::GB then GBAccessor.new(emu)
in GBA::GBA then GBAAccessor.new(emu)
in Emu then abort "Cannot init for the abstract emu class (this isn't even possible)"
end
@window = SDL::Window.new(window_title(59.7), @accessor.width * SCALE, @accessor.height * SCALE, flags: SDL::Window::Flags::OPENGL)
@gl_context = setup_gl
@opengl_info = OpenGLInfo.new
@io = setup_imgui
end
def run : NoReturn
loop do
@emu.run_until_frame unless @pause
handle_input
render_game
render_imgui
LibSDL.gl_swap_window(@window)
update_draw_count
end
end
private def handle_input : Nil
while event = SDL::Event.poll
ImGui::SDL2.process_event(event)
case event
when SDL::Event::Quit then exit 0
when SDL::Event::JoyHat,
SDL::Event::JoyButton then @emu.handle_event(event)
when SDL::Event::Keyboard
case event.sym
when .tab? then @emu.toggle_sync if event.pressed?
when .m? then toggle_blending if event.pressed?
when .q? then exit 0
else @emu.handle_event(event)
end
else nil
end
end
end
def toggle_blending : Nil
if @blending
LibGL.disable(LibGL::BLEND)
else
LibGL.enable(LibGL::BLEND)
end
@blending = @enable_blend = !@blending
end
private def render_game : Nil
LibGL.tex_image_2d(
LibGL::TEXTURE_2D,
0,
LibGL::RGB5,
@accessor.width,
@accessor.height,
0,
LibGL::RGBA,
LibGL::UNSIGNED_SHORT_1_5_5_5_REV,
@accessor.get_framebuffer
)
LibGL.draw_arrays(LibGL::TRIANGLE_STRIP, 0, 4)
end
private def render_imgui : Nil
ImGui::OpenGL3.new_frame
ImGui::SDL2.new_frame(@window)
ImGui.new_frame
overlay_height = 10.0
if LibSDL.get_mouse_focus
if ImGui.begin_main_menu_bar
if ImGui.begin_menu "File"
previously_paused = @pause
previously_synced = @sync
ImGui.menu_item "Overlay", "", pointerof(@enable_overlay)
ImGui.menu_item "Blend", "", pointerof(@enable_blend)
ImGui.menu_item "Pause", "", pointerof(@pause)
ImGui.menu_item "Sync", "", pointerof(@sync)
ImGui.end_menu
toggle_blending if @enable_blend ^ @blending
LibSDL.gl_set_swap_interval(@pause.to_unsafe) if previously_paused ^ @pause
@emu.toggle_sync if previously_synced ^ @sync
end
overlay_height += ImGui.get_window_size.y
ImGui.end_main_menu_bar
end
end
if @enable_overlay
ImGui.set_next_window_pos(ImGui::ImVec2.new 10, overlay_height)
ImGui.set_next_window_bg_alpha(0.5)
ImGui.begin("Overlay", pointerof(@enable_overlay),
ImGui::ImGuiWindowFlags::NoDecoration | ImGui::ImGuiWindowFlags::NoMove |
ImGui::ImGuiWindowFlags::NoSavedSettings)
io_framerate = @io.framerate
ImGui.text("FPS: #{io_framerate.format(decimal_places: 1)}")
ImGui.text("Frame time: #{(1000 / io_framerate).format(decimal_places: 3)}ms")
ImGui.separator
ImGui.text("OpenGL")
ImGui.text(" Version: #{@opengl_info.version}")
ImGui.text(" Shading: #{@opengl_info.shading}")
ImGui.end
end
if @pause
ImGui.set_next_window_pos(ImGui::ImVec2.new(@accessor.width * SCALE * 0.5, @accessor.height * SCALE * 0.5), pivot: ImGui::ImVec2.new(0.5, 0.5))
ImGui.begin("Pause", pointerof(@enable_overlay),
ImGui::ImGuiWindowFlags::NoDecoration | ImGui::ImGuiWindowFlags::NoMove |
ImGui::ImGuiWindowFlags::NoSavedSettings)
ImGui.text("PAUSED")
ImGui.end
end
ImGui.render
ImGui::OpenGL3.render_draw_data(ImGui.get_draw_data)
end
private def window_title(fps : Float) : String
"crab - #{fps.round(1)} fps"
end
private def update_draw_count : Nil
current_time = Time.utc
@microseconds += (current_time - @last_time).microseconds
@last_time = current_time
@frames += 1
if current_time.second != @seconds
fps = @frames * (1_000_000 / @microseconds)
@window.title = window_title(fps)
@microseconds = 0
@frames = 0
@seconds = current_time.second
end
end
private def compile_shader(source : String, type : UInt32) : UInt32
source_ptr = source.to_unsafe
shader = LibGL.create_shader(type)
LibGL.shader_source(shader, 1, pointerof(source_ptr), nil)
LibGL.compile_shader(shader)
shader_compiled = 0
LibGL.get_shader_iv(shader, LibGL::COMPILE_STATUS, pointerof(shader_compiled))
if shader_compiled != LibGL::TRUE
log_length = 0
LibGL.get_shader_iv(shader, LibGL::INFO_LOG_LENGTH, pointerof(log_length))
s = " " * log_length
LibGL.get_shader_info_log(shader, log_length, pointerof(log_length), s) if log_length > 0
abort "Error compiling shader: #{s}"
end
shader
end
private def setup_gl : LibSDL::GLContext
{% if flag?(:darwin) %}
LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_FLAGS, LibSDL::GLcontextFlag::FORWARD_COMPATIBLE_FLAG)
{% end %}
LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_PROFILE_MASK, LibSDL::GLprofile::PROFILE_CORE)
LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_MAJOR_VERSION, 3)
LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_CONTEXT_MINOR_VERSION, 3)
# Maybe for Dear ImGui
LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_DOUBLEBUFFER, 1)
LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_DEPTH_SIZE, 24)
LibSDL.gl_set_attribute(LibSDL::GLattr::SDL_GL_STENCIL_SIZE, 8)
{% unless flag?(:darwin) %}
# todo: proper debug messages for mac
LibGL.enable(LibGL::DEBUG_OUTPUT)
LibGL.enable(LibGL::DEBUG_OUTPUT_SYNCHRONOUS)
LibGL.debug_message_callback(->SDLOpenGLImGuiFrontend.callback, nil)
{% end %}
gl_context = LibSDL.gl_create_context @window
LibSDL.gl_set_swap_interval(0) # disable vsync
shader_program = LibGL.create_program
LibGL.blend_func(LibGL::SRC_ALPHA, LibGL::ONE_MINUS_SRC_ALPHA)
vert_shader_id = compile_shader(File.read("#{SHADERS}/identity.vert"), LibGL::VERTEX_SHADER)
frag_shader_id = compile_shader(File.read("#{SHADERS}/#{@accessor.shader}"), LibGL::FRAGMENT_SHADER)
frame_buffer = 0_u32
LibGL.gen_textures(1, pointerof(frame_buffer))
LibGL.active_texture(LibGL::TEXTURE0)
LibGL.bind_texture(LibGL::TEXTURE_2D, frame_buffer)
LibGL.attach_shader(shader_program, vert_shader_id)
LibGL.attach_shader(shader_program, frag_shader_id)
LibGL.link_program(shader_program)
LibGL.validate_program(shader_program)
a = [LibGL::BLUE, LibGL::GREEN, LibGL::RED, LibGL::ONE] # flip the rgba to bgra where a is always 1
a_ptr = pointerof(a).as(Int32*)
LibGL.tex_parameter_iv(LibGL::TEXTURE_2D, LibGL::TEXTURE_SWIZZLE_RGBA, a_ptr)
LibGL.tex_parameter_i(LibGL::TEXTURE_2D, LibGL::TEXTURE_MIN_FILTER, LibGL::NEAREST)
LibGL.tex_parameter_i(LibGL::TEXTURE_2D, LibGL::TEXTURE_MAG_FILTER, LibGL::NEAREST)
LibGL.use_program(shader_program)
vao = 0_u32 # required even if not used in modern opengl
LibGL.gen_vertex_arrays(1, pointerof(vao))
LibGL.bind_vertex_array(vao)
gl_context
end
private def setup_imgui : ImGui::ImGuiIO
LibImGuiBackends.gl3wInit
ImGui.debug_check_version_and_data_layout(
ImGui.get_version, *{
sizeof(LibImGui::ImGuiIO), sizeof(LibImGui::ImGuiStyle), sizeof(ImGui::ImVec2),
sizeof(ImGui::ImVec4), sizeof(ImGui::ImDrawVert), sizeof(ImGui::ImDrawIdx),
}.map &->LibC::SizeT.new(Int32))
ImGui.create_context
io = ImGui.get_io
ImGui.style_colors_dark
glsl_version = "#version 330"
ImGui::SDL2.init_for_opengl(@window, @gl_context)
ImGui::OpenGL3.init(glsl_version)
io
end
protected def self.callback(source : UInt32, type : UInt32, id : UInt32, severity : UInt32, length : Int32, message : Pointer(UInt8), userParam : Pointer(Void)) : Nil
puts "OpenGL debug message: #{String.new message}"
end
record OpenGLInfo, version : String, shading : String do
def initialize
@version = String.new(LibGL.get_string(LibGL::VERSION))
@shading = String.new(LibGL.get_string(LibGL::SHADING_LANGUAGE_VERSION))
end
end
end

View file

@ -246,7 +246,7 @@ module GB
if @ly == HEIGHT # final row of screen complete
self.mode_flag = 1 # switch to vblank
@gb.interrupts.vblank_interrupt = true
@gb.display.draw @framebuffer # render at vblank
@frame = true
@current_window_line = -1
else
self.mode_flag = 2 # switch to oam search

View file

@ -57,11 +57,11 @@ module GB
timer.skip_boot
end
def run : Nil
handle_events(70224)
loop do
def run_until_frame : Nil
until ppu.frame
cpu.tick
end
ppu.frame = false
end
def handle_event(event : SDL::Event) : Nil
@ -71,9 +71,5 @@ module GB
def toggle_sync : Nil
apu.toggle_sync
end
def toggle_blending : Nil
puts "Blending not implemented for gb/gbc"
end
end
end

View file

@ -118,7 +118,8 @@ module GB
DMG_COLORS = [0x6BDF_u16, 0x3ABF_u16, 0x35BD_u16, 0x2CEF_u16]
@framebuffer = Slice(UInt16).new WIDTH * HEIGHT
getter framebuffer = Slice(UInt16).new WIDTH * HEIGHT
property frame = false
@pram = Bytes.new 64
@palette_index : UInt8 = 0

View file

@ -149,7 +149,7 @@ module GB
if @ly == HEIGHT # final row of screen complete
self.mode_flag = 1 # switch to vblank
@gb.interrupts.vblank_interrupt = true
@gb.display.draw @framebuffer # render at vblank
@frame = true
else
self.mode_flag = 2 # switch to oam search
end

View file

@ -25,7 +25,6 @@ module GBA
getter! bus : Bus
getter! interrupts : Interrupts
getter! cpu : CPU
getter! display : Display
getter! ppu : PPU
getter! apu : APU
getter! dma : DMA
@ -49,19 +48,17 @@ module GBA
@bus = Bus.new self, @bios_path
@interrupts = Interrupts.new self
@cpu = CPU.new self
@display = Display.new Display::Console::GBA
@ppu = PPU.new self
@apu = APU.new self
@dma = DMA.new self
@debugger = Debugger.new self
end
def run : Nil
handle_events(280896)
loop do
{% if flag? :debugger %} debugger.check_debug {% end %}
def run_until_frame : Nil
until ppu.frame
cpu.tick
end
ppu.frame = false
end
def handle_event(event : SDL::Event) : Nil
@ -72,10 +69,6 @@ module GBA
apu.toggle_sync
end
def toggle_blending : Nil
display.toggle_blending
end
def handle_saves : Nil
scheduler.schedule 280896, ->handle_saves
storage.write_save

View file

@ -2,7 +2,8 @@ module GBA
class PPU
SPRITE_PIXEL = SpritePixel.new 4, 0, false, false # base sprite pixel to fill buffer with on each scanline
@framebuffer : Slice(UInt16) = Slice(UInt16).new 0x9600 # framebuffer as 16-bit xBBBBBGGGGGRRRRR
getter framebuffer : Slice(UInt16) = Slice(UInt16).new 0x9600 # framebuffer as 16-bit xBBBBBGGGGGRRRRR
property frame = false
@layer_palettes : Array(Bytes) = Array.new 4 { Bytes.new 240 }
@sprite_pixels : Slice(SpritePixel) = Slice(SpritePixel).new 240, SPRITE_PIXEL
@ -74,7 +75,7 @@ module GBA
end
def draw : Nil
@gba.display.draw @framebuffer
@frame = true
end
# Get the screen entry offset from the tile x, tile y, and background screen-size param using tonc algo