Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
dokidoki/kernel.lua
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
351 lines (298 sloc)
10.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- dokidoki.kernel | |
| --- =============== | |
| --- | |
| --- `dokidoki.kernel` handles initialization and the core loop. It takes an | |
| --- abstract scene object which provides implementations of updating, drawing, | |
| --- and event-handling functions. | |
| --- | |
| --- A scene must implement: | |
| --- | |
| --- - `scene.update()` | |
| --- - `scene.draw()` | |
| --- - `scene.handle_event(event)` | |
| --- | |
| --- where none of the callbacks has a return value. | |
| --- | |
| --- For the `handle_event(event)` callback, `event` has one of the following | |
| --- forms: | |
| --- | |
| --- - `{type = 'close'}` | |
| --- - `{type = 'resize', width = ?, height = ?}` | |
| --- - `{type = 'key', key = ?, is_down = ?}` (`key` is a GLFW key constant) | |
| --- Implementation | |
| --- -------------- | |
| require "dokidoki.module" | |
| [[ set_fps, set_max_frameskip, get_width, get_height, get_ratio, | |
| set_fullscreen, set_video_mode, set_ratio, start_main_loop, abort_main_loop, | |
| switch_scene, get_framerate ]] | |
| require "glfw" | |
| local mixer = require "mixer" | |
| local log = require "log" | |
| import(require "gl") | |
| import(require "glu") | |
| import(require "dokidoki.base") | |
| ---- Constants ---------------------------------------------------------------- | |
| max_frameskip = 6 | |
| fps = 60 | |
| max_sample_frames = 16 | |
| -- Never sleep for less than this amount. This has to be bigger on systems with | |
| -- really lame timers, like Windows. | |
| min_sleep_time = 0.002 | |
| -- Allow for this much inaccuracy in the OS sleep operation | |
| sleep_allowance = 0.002 | |
| ---- State Variables ---------------------------------------------------------- | |
| running = false | |
| use_fullscreen = false | |
| width = 640 | |
| height = 480 | |
| ratio = width / height | |
| frame_times = {} | |
| next_scene = nil | |
| ---- Public Interface --------------------------------------------------------- | |
| --- ### `set_max_frameskip(max_frameskip)` | |
| --- Sets the maximum number of updates to run before forcing a draw. | |
| --- | |
| --- If more than this number of updates are queued to happen before a redraw, | |
| --- the rest will be discared. | |
| function set_max_frameskip(new_max_frameskip) | |
| assert(type(new_max_frameskip == 'number') and new_max_frameskip >= 1) | |
| max_frameskip = new_max_frameskip | |
| end | |
| --- ### `set_fps(fps)` | |
| --- Sets the target frames-per-second. | |
| function set_fps(new_fps) | |
| assert(type(new_fps == 'number') and fps > 0) | |
| fps = new_fps | |
| end | |
| --- ### `get_width()` | |
| --- Returns the current total window width in pixels. This includes borders | |
| --- when the ratio doesn't match the window size. | |
| function get_width () return width end | |
| --- ### `get_height()` | |
| --- Returns the current total window height in pixels. This includes borders | |
| --- when the ratio doesn't match the window size. | |
| function get_height () return height end | |
| --- ### `set_fullscreen(fullscreen)` | |
| --- Sets whether or not a fullscreen window should be opened. This must be | |
| --- called before entering the main loop. | |
| --- | |
| --- The default is to open in a window. | |
| function set_fullscreen(fullscreen) | |
| assert(not running, 'set_fullscreen must be called before the main loop') | |
| use_fullscreen = fullscreen | |
| end | |
| --- ### `set_video_mode(width, height)` | |
| --- Sets the desired width/height of the window in pixels. | |
| --- | |
| --- If the main loop hasn't yet been started, it will immediately change | |
| --- the window size (or change video modes in fullscreen). If called before | |
| --- entering the main loop, this sets the initial video mode, but has no | |
| --- immediate effect. | |
| --- | |
| --- This does not set the desired ratio, so unless you set that as well you may | |
| --- end up with borders. | |
| function set_video_mode (w, h) | |
| assert(w > 0) | |
| assert(h > 0) | |
| width = w | |
| height = h | |
| if running then | |
| glfw.SetWindowSize(w, h) | |
| update_viewport() | |
| end | |
| end | |
| --- ### `get_ratio()` | |
| --- Returns the screen ratio used within the window for the active area. See | |
| --- also `set_ratio()`. | |
| function get_ratio () return ratio end | |
| --- ### `set_ratio(ratio)` | |
| --- Sets the desired screen ratio. | |
| --- | |
| --- If the window size doesn't match this ratio, then the ratio is used to set | |
| --- the opengl viewport shape within the window. | |
| --- | |
| --- This must be set manually if a ratio other than 4/3 is wanted, since calls | |
| --- to `set_video_mode()` have no effect on it. | |
| function set_ratio (r) | |
| assert(r > 0) | |
| ratio = r | |
| if running then update_viewport() end | |
| end | |
| --- ### `start_main_loop(scene)` | |
| --- Initializes stuff and begins the main loop. `scene` is used as described in | |
| --- the introduction above. | |
| function start_main_loop (scene) | |
| -- glfw.Terminate() needs to be called even if there is an error, since | |
| -- otherwise it may not return the screen to its original resolution. | |
| local success, message = xpcall(function () | |
| log.log_message "initializing mixer. . ." | |
| assert(mixer.init()) | |
| log.log_message "initializing glfw. . ." | |
| if glfw.Init() == GL_FALSE then | |
| error("glfw initialization failed") | |
| end | |
| running = true | |
| log.log_message "detected video modes:" | |
| for _, mode in ipairs(glfw.GetVideoModes()) do | |
| log.log_message(' ' .. mode.Width .. 'x' .. mode.Height .. ' R' .. | |
| mode.RedBits .. ' G' .. mode.GreenBits .. ' B' .. | |
| mode.BlueBits) | |
| end | |
| log.log_message "setting video mode. . ." | |
| glfw.OpenWindow(width, height, 8, 8, 8, 8, 24, 0, | |
| use_fullscreen and glfw.FULLSCREEN or glfw.WINDOW) | |
| set_video_mode(width, height) | |
| log.log_message "starting main loop" | |
| next_scene = scene | |
| while next_scene do | |
| scene = next_scene | |
| next_scene = nil | |
| main_loop(scene) | |
| if next_scene then | |
| running = true | |
| end | |
| end | |
| end, debug.traceback) | |
| log.log_message "shutting down" | |
| glfw.Terminate() | |
| if not success then error(message, 0) end | |
| end | |
| --- ### `get_framerate()` | |
| --- Returns a running average of the number of `draw()` calls per second. | |
| --- Since multiple updates can happen per draw, this isn't the same as the | |
| --- number of updates per second, which should usually stay constant. | |
| function get_framerate () | |
| if #frame_times >= 2 then | |
| return (#frame_times - 1) / (frame_times[#frame_times] - frame_times[1]) | |
| else | |
| return 1 | |
| end | |
| end | |
| --- ### `abort_main_loop()` | |
| --- Sets a flag to terminate the main loop at the next opportunity. | |
| --- | |
| --- This function should be called from one of the scene callbacks; the loop | |
| --- will be aborted when the callback returns. | |
| function abort_main_loop () | |
| assert(running) | |
| running = false | |
| end | |
| --- ### `switch_scene(scene)` | |
| --- Queues up a scene to switch to. | |
| --- | |
| --- This function should be called from one of the scene callbacks; the scenes | |
| --- will be switched when the callback returns. | |
| function switch_scene(scene) | |
| assert(running) | |
| next_scene = scene | |
| running = false | |
| end | |
| ---- Utility Functions -------------------------------------------------------- | |
| function set_callback(name, callback) | |
| local set = glfw['Set' .. name .. 'Callback'] | |
| assert(set, 'invalid callback name given') | |
| local varname = '__kernel_callback_' .. name | |
| rawset(_G, varname, callback) | |
| if(callback) then | |
| set(varname) | |
| else | |
| set() | |
| end | |
| end | |
| function main_loop (scene) | |
| local last_update_time = get_current_time() | |
| -- filled in callbacks whenever glfw.SwapBuffers is called | |
| local events = {} | |
| set_callback('WindowClose', function () | |
| table.insert(events, {type = 'quit'}) | |
| return glfw.FALSE | |
| end) | |
| set_callback('WindowSize', function (width, height) | |
| table.insert(events, | |
| {type = 'resize', width = width, height = height}) | |
| end) | |
| set_callback('Key', function (key, action) | |
| table.insert(events, | |
| {type = 'key', key = key, is_down = action == glfw.PRESS}) | |
| end) | |
| while true do | |
| ---- handle events | |
| local new_width, new_height | |
| for i, event in ipairs(events) do | |
| if event.type == 'resize' then | |
| if event.width > 0 and event.height > 0 then | |
| new_width = event.width | |
| new_height = event.height | |
| end | |
| end | |
| scene.handle_event(event) | |
| if not running then return end | |
| end | |
| if new_width and (new_width ~= width or new_height ~= height) then | |
| set_video_mode(new_width, new_height) | |
| end | |
| while #events ~= 0 do events[#events] = nil end | |
| ---- update | |
| do | |
| -- wait until it's time to update at least once | |
| local update_time = 1/fps | |
| sleep_until(last_update_time + update_time) | |
| local current_time = get_current_time() | |
| log_frame_time(current_time) | |
| -- if we would have to update for more than max_update_time, skip forward | |
| local max_update_time = max_frameskip / fps + 0.001 | |
| local total_update_time = current_time - last_update_time | |
| if max_update_time < total_update_time then | |
| log.log_message( | |
| "warning: underrun of " .. | |
| math.ceil(1000 * (total_update_time - max_update_time)) .. | |
| "ms") | |
| last_update_time = current_time - max_update_time | |
| end | |
| -- update the right number of times | |
| while last_update_time + update_time <= current_time do | |
| last_update_time = last_update_time + update_time | |
| scene.update(update_time) | |
| if not running then return end | |
| end | |
| end | |
| ---- draw | |
| scene.draw() | |
| local err = glGetError() | |
| if err ~= 0 then error('OpenGL error: ' .. gluErrorString(err)) end | |
| if not running then return end | |
| glfw.SwapBuffers() | |
| end | |
| end | |
| function get_current_time () | |
| return glfw.GetTime() | |
| end | |
| function sleep_until (time) | |
| local time_to_sleep = time - get_current_time() - sleep_allowance | |
| if time_to_sleep > min_sleep_time then | |
| glfw.Sleep(time_to_sleep) | |
| end | |
| -- Floating point inaccuracy can cause this to pass here but not in the main | |
| -- loop, making us do iterations of 0 updates, which is stupid because it | |
| -- will render the same thing as last time. Making this condition slightly | |
| -- stricter fixes it. | |
| while time + 0.0001 > get_current_time() do end | |
| end | |
| function update_viewport () | |
| if width / height > ratio then | |
| -- pillarbox | |
| local inner_width = math.floor(height * ratio + 0.5) | |
| glViewport(math.floor((width - inner_width) / 2), 0, inner_width, height) | |
| else | |
| -- letterbox | |
| local inner_height = math.floor(width / ratio + 0.5) | |
| glViewport(0, math.floor((height - inner_height) / 2), width, inner_height) | |
| end | |
| end | |
| function log_frame_time(time) | |
| table.insert(frame_times, time) | |
| if #frame_times > max_sample_frames then | |
| table.remove(frame_times, 1) | |
| end | |
| end | |
| return get_module_exports() |