move gb color correction to gpu, unify display logic for gb and gba

This commit is contained in:
Matthew Berry 2021-05-11 00:47:32 -07:00
parent be81650f54
commit f2151a0a25
11 changed files with 208 additions and 225 deletions

145
src/crab/common/display.cr Normal file
View file

@ -0,0 +1,145 @@
require "lib_gl"
class Display
enum Console
GB
GBA
def width : Int32
case self
in GB then 160
in GBA then 240
end
end
def height : Int32
case self
in GB then 144
in GBA then 160
end
end
def shader : String
case self
in GB then "gb_colors.frag"
in GBA then "gba_colors.frag"
end
end
end
SCALE = 4
SHADERS = "src/crab/common/shaders"
@microseconds = 0
@frames = 0
@last_time = Time.utc
@seconds : Int32 = Time.utc.second
@blend : Bool = false
def initialize(@console : Console)
@window = SDL::Window.new(window_title(59.7), console.width * SCALE, console.height * SCALE, flags: SDL::Window::Flags::OPENGL)
setup_gl
end
def draw(framebuffer : Slice(UInt16)) : Nil
LibGL.tex_image_2d(LibGL::TEXTURE_2D, 0, LibGL::RGB5, @console.width, @console.height, 0, LibGL::RGBA, LibGL::UNSIGNED_SHORT_1_5_5_5_REV, framebuffer)
LibGL.draw_arrays(LibGL::TRIANGLE_STRIP, 0, 4)
LibSDL.gl_swap_window(@window)
update_draw_count
end
def toggle_blending : Nil
if @blend
LibGL.disable(LibGL::BLEND)
else
LibGL.enable(LibGL::BLEND)
end
@blend = !@blend
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 : Nil
{% 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)
{% unless flag?(:darwin) %}
# todo: proper debug messages for mac
LibGL.enable(LibGL::DEBUG_OUTPUT)
LibGL.enable(LibGL::DEBUG_OUTPUT_SYNCHRONOUS)
LibGL.debug_message_callback(->Display.callback, nil)
{% end %}
LibSDL.gl_create_context @window
LibSDL.gl_set_swap_interval(0) # disable vsync
shader_program = LibGL.create_program
puts "OpenGL version: #{String.new(LibGL.get_string(LibGL::VERSION))}"
puts "Shader language version: #{String.new(LibGL.get_string(LibGL::SHADING_LANGUAGE_VERSION))}"
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}/#{@console.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)
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
end

View file

@ -0,0 +1,20 @@
#version 330 core
in vec2 tex_coord;
out vec4 frag_color;
uniform sampler2D input_texture;
mat3 m = mat3(
26, 0, 6,
4, 24, 4,
2, 8, 22
);
void main() {
// Credits to [unknown] and Near for this color-correction algorithm.
// https://byuu.net/video/color-emulation
vec4 color = texture(input_texture, tex_coord);
frag_color.rgb = min(vec3(30), m * color.rgb / 32);
frag_color.a = 0.5;
}

View file

@ -1,85 +0,0 @@
require "stumpy_png"
module GB
class Display
WIDTH = 160
HEIGHT = 144
PIXELFORMAT_BGR5 = 357764866
TEXTUREACCESS_STREAMING = 1
@window : SDL::Window
@renderer : SDL::Renderer
@texture : Pointer(LibSDL::Texture)
@title : String
@fps = 30
@seconds : Int32 = Time.utc.second
def initialize(gb : GB, headless : Bool)
@title = gb.cartridge.title
flags = headless ? SDL::Window::Flags::HIDDEN : SDL::Window::Flags::SHOWN
@window = SDL::Window.new(window_title, WIDTH * DISPLAY_SCALE, HEIGHT * DISPLAY_SCALE, flags: flags)
@renderer = SDL::Renderer.new @window
@renderer.logical_size = {WIDTH, HEIGHT}
@texture = LibSDL.create_texture @renderer, PIXELFORMAT_BGR5, TEXTUREACCESS_STREAMING, WIDTH, HEIGHT
end
def window_title : String
"CryBoy - #{@title} - #{@fps} fps"
end
def draw(framebuffer : Slice(UInt16)) : Nil
LibSDL.update_texture @texture, nil, framebuffer, WIDTH * sizeof(UInt16)
@renderer.clear
@renderer.copy @texture
@renderer.present
@fps += 1
if Time.utc.second != @seconds
@window.title = window_title
@fps = 0
@seconds = Time.utc.second
end
end
def write_png(framebuffer : Array(RGB)) : Nil
canvas = StumpyPNG::Canvas.new WIDTH, HEIGHT
HEIGHT.times do |row|
WIDTH.times do |col|
rgb = framebuffer[row * WIDTH + col]
color = StumpyPNG::RGBA.from_rgb8(rgb.red, rgb.green, rgb.blue)
canvas[col, row] = color
end
end
StumpyPNG.write(canvas, "out.png")
end
end
end
###############################################################################
# Method for drawing all tiles in vram
# @all_tiles_window = SDL::Window.new("ALL TILES", 128 * scale, 192 * scale)
# @all_tiles_renderer = SDL::Renderer.new @all_tiles_window
# @all_tiles_renderer.logical_size = {128, 192}
# # a method for showing all tiles in vram for debugging
# def draw_all_tiles(memory : Memory)
# (0...24).each do |y|
# (0...16).each do |x|
# tile_ptr = 0x8000 + (y * 16 * 16) + (x * 16)
# (0...8).each do |tile_row|
# byte_1 = memory[tile_ptr + 2 * tile_row]
# byte_2 = memory[tile_ptr + 2 * tile_row + 1]
# (0...8).each do |tile_col|
# lsb = (byte_1 >> (7 - tile_col)) & 0x1
# msb = (byte_2 >> (7 - tile_col)) & 0x1
# @all_tiles_renderer.draw_color = @colors[(msb << 1) | lsb]
# @all_tiles_renderer.draw_point((8 * x + tile_col), (8 * y + tile_row))
# end
# end
# end
# end
# @all_tiles_renderer.present
# end

View file

@ -203,10 +203,10 @@ module GB
arr = @pram
end
color = @cgb_ptr.value ? pixel.color : palette[pixel.color]
@framebuffer[Display::WIDTH * @ly + @lx] = arr.to_unsafe.as(UInt16*)[4 * pixel.palette + color]
@framebuffer[WIDTH * @ly + @lx] = arr.to_unsafe.as(UInt16*)[4 * pixel.palette + color]
end
@lx += 1
if @lx == Display::WIDTH
if @lx == WIDTH
self.mode_flag = 0
end
if window_enabled? && @ly >= @wy && @lx + 7 >= @wx && !@fetching_window && @window_trigger
@ -243,7 +243,7 @@ module GB
if @cycle_counter == 456
@cycle_counter = 0
@ly += 1
if @ly == Display::HEIGHT # final row of screen complete
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

View file

@ -2,7 +2,6 @@ require "sdl"
require "./apu"
require "./cartridge"
require "./cpu"
require "./display"
require "./interrupts"
require "./joypad"
require "./mbc/*"
@ -44,7 +43,7 @@ module GB
@scheduler = Scheduler.new
@interrupts = Interrupts.new
@apu = APU.new self, @headless, @sync
@display = Display.new self, @headless
@display = Display.new Display::Console::GB
@joypad = Joypad.new self
@ppu = @fifo ? FifoPPU.new self : ScanlinePPU.new self
@timer = Timer.new self

View file

@ -111,7 +111,10 @@ module GB
@ran_bios : Bool # determine if colors should be adjusted for cgb
@cgb_ptr : Pointer(Bool)
@framebuffer = Slice(UInt16).new Display::WIDTH * Display::HEIGHT
WIDTH = 160
HEIGHT = 144
@framebuffer = Slice(UInt16).new WIDTH * HEIGHT
@pram = Bytes.new 64
@palette_index : UInt8 = 0
@ -160,6 +163,30 @@ module GB
def initialize(@gb : GB)
@cgb_ptr = gb.cgb_ptr
unless @cgb_ptr.value # fill default color palettes
# {% if flag? :pink %}
# @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [
# RGB.new(0xFF, 0xF6, 0xD3), RGB.new(0xF9, 0xA8, 0x75),
# RGB.new(0xEB, 0x6B, 0x6F), RGB.new(0x7C, 0x3F, 0x58),
# ]
# {% elsif flag? :graphics_test %}
# @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [
# RGB.new(0xFF, 0xFF, 0xFF), RGB.new(0xAA, 0xAA, 0xAA),
# RGB.new(0x55, 0x55, 0x55), RGB.new(0x00, 0x00, 0x00),
# ]
# @pram[0] = @pram[1] = @obj_pram[0] = @obj_pram[1] = 0xFF
# @pram.to_unsafe.as(UInt16*)[0] = 0x7FFF;
# {% else %}
# @palettes[0] = @obj_palettes[0] = @obj_palettes[1] = [
# RGB.new(0xE0, 0xF8, 0xCF), RGB.new(0x86, 0xC0, 0x6C),
# RGB.new(0x30, 0x68, 0x50), RGB.new(0x07, 0x17, 0x20),
# ]
# {% end %}
@pram.to_unsafe.as(UInt16*)[0] = @obj_pram.to_unsafe.as(UInt16*)[0] = @obj_pram.to_unsafe.as(UInt16*)[4] = 0x6BDF
@pram.to_unsafe.as(UInt16*)[1] = @obj_pram.to_unsafe.as(UInt16*)[1] = @obj_pram.to_unsafe.as(UInt16*)[5] = 0x3ABF
@pram.to_unsafe.as(UInt16*)[2] = @obj_pram.to_unsafe.as(UInt16*)[2] = @obj_pram.to_unsafe.as(UInt16*)[6] = 0x35BD
@pram.to_unsafe.as(UInt16*)[3] = @obj_pram.to_unsafe.as(UInt16*)[3] = @obj_pram.to_unsafe.as(UInt16*)[7] = 0x2CEF
end
@ran_bios = @cgb_ptr.value
end

View file

@ -22,7 +22,7 @@ module GB
end
# color idx, BG-to-OAM priority bit
@scanline_color_vals = Array(Tuple(UInt8, Bool)).new Display::WIDTH, {0_u8, false}
@scanline_color_vals = Array(Tuple(UInt8, Bool)).new WIDTH, {0_u8, false}
def scanline
@current_window_line = 0 if @ly == 0
@ -32,7 +32,7 @@ module GB
tile_data_table = bg_window_tile_data == 0 ? 0x1000 : 0x0000 # 0x9000 : 0x8000
tile_row_window = @current_window_line & 7
tile_row = (@ly.to_u16 + @scy) & 7
Display::WIDTH.times do |x|
WIDTH.times do |x|
if window_enabled? && @ly >= @wy && x + 7 >= @wx && @window_trigger
should_increment_window_line = true
tile_num_addr = window_map + ((x + 7 - @wx) >> 3) + ((@current_window_line >> 3) * 32)
@ -57,9 +57,9 @@ module GB
color = (msb << 1) | lsb
@scanline_color_vals[x] = {color, @vram[1][tile_num_addr] & 0x80 > 0}
if @cgb_ptr.value
@framebuffer[Display::WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[4 * (@vram[1][tile_num_addr] & 0b111) + color]
@framebuffer[WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[4 * (@vram[1][tile_num_addr] & 0b111) + color]
else
@framebuffer[Display::WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[@bgp[color]]
@framebuffer[WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[@bgp[color]]
end
elsif bg_display? || @cgb_ptr.value
tile_num_addr = background_map + (((x + @scx) >> 3) & 0x1F) + ((((@ly.to_u16 + @scy) >> 3) * 32) & 0x3FF)
@ -84,9 +84,9 @@ module GB
color = (msb << 1) | lsb
@scanline_color_vals[x] = {color, @vram[1][tile_num_addr] & 0x80 > 0}
if @cgb_ptr.value
@framebuffer[Display::WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[4 * (@vram[1][tile_num_addr] & 0b111) + color]
@framebuffer[WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[4 * (@vram[1][tile_num_addr] & 0b111) + color]
else
@framebuffer[Display::WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[@bgp[color]]
@framebuffer[WIDTH * @ly + x] = @pram.to_unsafe.as(UInt16*)[@bgp[color]]
end
end
end
@ -97,7 +97,7 @@ module GB
bytes = sprite.bytes @ly, sprite_height
8.times do |col|
x = col + sprite.x - 8
next unless 0 <= x < Display::WIDTH # only render sprites on screen
next unless 0 <= x < WIDTH # only render sprites on screen
if sprite.x_flip?
lsb = (@vram[@cgb_ptr.value ? sprite.bank_num : 0][bytes[0]] >> col) & 0x1
msb = (@vram[@cgb_ptr.value ? sprite.bank_num : 0][bytes[1]] >> col) & 0x1
@ -112,12 +112,12 @@ module GB
# objects are always on top of bg/window color 0
# objects are on top of bg/window colors 1-3 if bg_priority and object priority are both unset
if !bg_display? || @scanline_color_vals[x][0] == 0 || (!@scanline_color_vals[x][1] && sprite.priority == 0)
@framebuffer[Display::WIDTH * @ly + x] = @obj_pram.to_unsafe.as(UInt16*)[4 * sprite.cgb_palette_number + color]
@framebuffer[WIDTH * @ly + x] = @obj_pram.to_unsafe.as(UInt16*)[4 * sprite.cgb_palette_number + color]
end
else
if sprite.priority == 0 || @scanline_color_vals[x][0] == 0
palette = sprite.dmg_palette_number == 0 ? @obp0 : @obp1
@framebuffer[Display::WIDTH * @ly + x] = @obj_pram.to_unsafe.as(UInt16*)[palette[color]]
@framebuffer[WIDTH * @ly + x] = @obj_pram.to_unsafe.as(UInt16*)[palette[color]]
end
end
end
@ -146,7 +146,7 @@ module GB
if @cycle_counter >= 204 # end of hblank reached
@cycle_counter -= 204 # reset cycle_counter, saving extra cycles
@ly += 1
if @ly == Display::HEIGHT # final row of screen complete
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

View file

@ -1,122 +0,0 @@
require "lib_gl"
module GBA
class Display
WIDTH = 240
HEIGHT = 160
SCALE = 4
@microseconds = 0
@frames = 0
@last_time = Time.utc
@seconds : Int32 = Time.utc.second
@blend : Bool = false
def initialize
@window = SDL::Window.new(window_title(59.7), WIDTH * SCALE, HEIGHT * SCALE, flags: SDL::Window::Flags::OPENGL)
setup_gl
end
def draw(framebuffer : Slice(UInt16)) : Nil
LibGL.tex_image_2d(LibGL::TEXTURE_2D, 0, LibGL::RGB5, 240, 160, 0, LibGL::RGBA, LibGL::UNSIGNED_SHORT_1_5_5_5_REV, framebuffer)
LibGL.draw_arrays(LibGL::TRIANGLE_STRIP, 0, 4)
LibSDL.gl_swap_window(@window)
update_draw_count
end
def toggle_blending : Nil
if @blend
LibGL.disable(LibGL::BLEND)
else
LibGL.enable(LibGL::BLEND)
end
@blend = !@blend
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 : Nil
{% 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)
{% unless flag?(:darwin) %}
# todo: proper debug messages for mac
LibGL.enable(LibGL::DEBUG_OUTPUT)
LibGL.enable(LibGL::DEBUG_OUTPUT_SYNCHRONOUS)
LibGL.debug_message_callback(->Display.callback, nil)
{% end %}
LibSDL.gl_create_context @window
LibSDL.gl_set_swap_interval(0) # disable vsync
shader_program = LibGL.create_program
puts "OpenGL version: #{String.new(LibGL.get_string(LibGL::VERSION))}"
puts "Shader language version: #{String.new(LibGL.get_string(LibGL::SHADING_LANGUAGE_VERSION))}"
LibGL.blend_func(LibGL::SRC_ALPHA, LibGL::ONE_MINUS_SRC_ALPHA)
vert_shader_id = compile_shader(File.read("src/crab/gba/shaders/gba_colors.vert"), LibGL::VERTEX_SHADER)
frag_shader_id = compile_shader(File.read("src/crab/gba/shaders/gba_colors.frag"), 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)
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
end
end

View file

@ -9,7 +9,6 @@ require "./keypad"
require "./bus"
require "./interrupts"
require "./cpu"
require "./display"
require "./ppu"
require "./apu"
require "./dma"
@ -50,7 +49,7 @@ module GBA
@bus = Bus.new self, @bios_path
@interrupts = Interrupts.new self
@cpu = CPU.new self
@display = Display.new
@display = Display.new Display::Console::GBA
@ppu = PPU.new self
@apu = APU.new self
@dma = DMA.new self