mirror of
https://github.com/mattrberry/crab.git
synced 2024-12-28 09:58:49 +01:00
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:
parent
c7bc2c31f9
commit
fca039bd12
14 changed files with 336 additions and 45 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
13
src/crab/common/frontend.cr
Normal file
13
src/crab/common/frontend.cr
Normal 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
|
7
src/crab/common/frontend/accessor.cr
Normal file
7
src/crab/common/frontend/accessor.cr
Normal 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
|
12
src/crab/common/frontend/console_accessors/gb_accessor.cr
Normal file
12
src/crab/common/frontend/console_accessors/gb_accessor.cr
Normal 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
|
12
src/crab/common/frontend/console_accessors/gba_accessor.cr
Normal file
12
src/crab/common/frontend/console_accessors/gba_accessor.cr
Normal 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
|
10
src/crab/common/frontend/headless_frontend.cr
Normal file
10
src/crab/common/frontend/headless_frontend.cr
Normal 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
|
263
src/crab/common/frontend/sdl_opengl_imgui_frontend.cr
Normal file
263
src/crab/common/frontend/sdl_opengl_imgui_frontend.cr
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue