From 7d48b226ab681fc311e927875712f1f6cb224f2b Mon Sep 17 00:00:00 2001 From: Daanish Date: Wed, 20 May 2026 17:30:38 +0100 Subject: [PATCH] Fix loop to be more accuratly timed, and added custom ruby libs inclusion setup --- .gitignore | 4 +- Makefile | 16 +- include/app/app.h | 22 +- include/binding/ruby.h | 10 + include/window/window.h | 4 +- libs/l_mgems/compile.sh | 40 ++ libs/l_mgems/pp.rb | 1010 +++++++++++++++++++++++++++++++++++++++ samples/button.rb | 29 +- src/main.cc | 1 + src/window/window.cc | 7 +- writeup/analysis.md | 1 + 11 files changed, 1117 insertions(+), 27 deletions(-) create mode 100755 libs/l_mgems/compile.sh create mode 100644 libs/l_mgems/pp.rb diff --git a/.gitignore b/.gitignore index 8d6cd86..5ab73f4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ build/* bin/* -.clangd \ No newline at end of file +.clangd + +include/binding/ruby_compiled.h \ No newline at end of file diff --git a/Makefile b/Makefile index 279ce6a..24ec806 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ CXX := $(CCACHE) clang++ CC := $(CCACHE) clang # ---------------- SDL3 ---------------- + SDL_CFLAGS := $(shell pkg-config --cflags sdl3 sdl3-ttf sdl3-image) SDL_LIBS_DEBUG := $(shell pkg-config --libs sdl3 sdl3-ttf sdl3-image) SDL_LIBS_RELEASE := $(shell pkg-config --libs sdl3 sdl3-ttf sdl3-image) @@ -42,9 +43,11 @@ PCH_CFLAGS_RELEASE := $(CFLAGS_RELEASE) -x c++-header LIBS := ./libs/mruby/build/host/lib/libmruby.a +MGEM_DIR := ./libs/l_mgems + # ---------------- SOURCES ---------------- -SRC := $(wildcard $(SRC_DIR)/*.cc) $(wildcard $(SRC_DIR)/**/*.cc) +SRC := $(shell find $(SRC_DIR) -name "*.cc") OBJ_DEBUG := $(patsubst $(SRC_DIR)/%.cc,$(OBJ_DIR)/debug/%.o,$(SRC)) OBJ_RELEASE := $(patsubst $(SRC_DIR)/%.cc,$(OBJ_DIR)/release/%.o,$(SRC)) @@ -54,13 +57,13 @@ DEP_RELEASE := $(OBJ_RELEASE:.o=.d) # ---------------- TARGETS ---------------- -.PHONY: all debug release clean setup +.PHONY: all debug release clean setup mgems all: debug -debug: $(TARGET_DEBUG) +debug: mgems $(TARGET_DEBUG) -release: $(TARGET_RELEASE) +release: mgems $(TARGET_RELEASE) # ---------------- CLANGD ---------------- @@ -68,6 +71,11 @@ setup: setup.rb @echo "Setting up clangd configuration..." ruby setup.rb +# ---------------- MRB_MGEMS ---------------- + +mgems: + $(MGEM_DIR)/compile.sh + # ---------------- PCH ---------------- $(PCH_DEBUG): $(INCLUDE_DIR)/pch.h diff --git a/include/app/app.h b/include/app/app.h index c8fe817..b59a027 100644 --- a/include/app/app.h +++ b/include/app/app.h @@ -54,8 +54,7 @@ struct EImage : Element { Animation &animation = *animation_pool[animation_id]; if (animation.frames.empty()) return; - - int frame_index = (int)((abs_time * animation.frame_rate) / 1000) % animation.frames.size(); + int frame_index = (int)((abs_time * animation.frame_rate) / 1e9) % animation.frames.size(); window.draw(animation.frames[frame_index], position, z, rotation, scale, alpha); } }; @@ -166,10 +165,10 @@ struct Scene { } void update_scene(uint64_t delta_time) { - if (!mrb_nil_p(update.proc)) { - mrb_value dt_val = mrb_int_value(Ruby::mrb, delta_time); - update.call(1, &dt_val); - } + if (mrb_nil_p(update.proc)) + return; + mrb_value dt_val = mrb_float_value(Ruby::mrb, delta_time / 1e9); + update.call(1, &dt_val); } void click(Vec2 position) { @@ -234,7 +233,7 @@ struct App { } void loop() { - const uint64_t target_ms = 1000 / 60; + const uint64_t TARGET_NS = 1e9 / 60; uint64_t time_a = window.get_time(); uint64_t time_b = window.get_time(); @@ -242,8 +241,13 @@ struct App { while (running) { time_a = window.get_time(); uint64_t frame_time = time_a - time_b; - if (frame_time <= target_ms) - usleep((target_ms - frame_time) * 1000); + + if (frame_time <= TARGET_NS) { + struct timespec req; + req.tv_sec = 0; + req.tv_nsec = TARGET_NS - frame_time; + nanosleep(&req, nullptr); + } time_b = window.get_time(); diff --git a/include/binding/ruby.h b/include/binding/ruby.h index bb71a0b..a4ff745 100644 --- a/include/binding/ruby.h +++ b/include/binding/ruby.h @@ -4,6 +4,8 @@ #include "pch.h" #include "utils.h" +#include "binding/ruby_compiled.h" + #define DEF_SYM(name) inline mrb_sym sym_##name = mrb_intern_cstr(Ruby::mrb, #name); #define SYM(name) mrb_symbol_value(sym_##name) @@ -83,7 +85,15 @@ struct Block { }; inline void run_string(const char *code, size_t length) { + int ai = mrb_gc_arena_save(mrb); mrb_load_nstring(mrb, code, length); + mrb_gc_arena_restore(mrb, ai); +} + +inline void load_mgems() { + int ai = mrb_gc_arena_save(mrb); + mrb_load_irep(mrb, mgems); + mrb_gc_arena_restore(mrb, ai); } }; // namespace Ruby diff --git a/include/window/window.h b/include/window/window.h index 0dcaaee..e8bd776 100644 --- a/include/window/window.h +++ b/include/window/window.h @@ -341,8 +341,8 @@ public: // Time - uint64_t get_time(); // returns the time in milliseconds since the window was created - uint64_t delta_time(); // returns the time in milliseconds since the last render call + uint64_t get_time(); // returns the time in nanoseconds since the window was created + uint64_t delta_time(); // returns the time in nanoseconds since the last render call private: // internal data and functions diff --git a/libs/l_mgems/compile.sh b/libs/l_mgems/compile.sh new file mode 100755 index 0000000..0d62ef4 --- /dev/null +++ b/libs/l_mgems/compile.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -e + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT_TMP=$(mktemp) +OUTPUT="$SCRIPT_DIR/../../include/binding/ruby_compiled.h" + +if test "$1" = "clean"; then + rm -f "$OUTPUT" + exit 0 +fi + +trap 'rm -f "$OUTPUT_TMP"' EXIT + +mapfile -t FILES < <(find "$SCRIPT_DIR" -name "*.rb" | sort) + +LATEST_FILE_TIME=$(for file in "${FILES[@]}"; do + echo $(stat -c "%Y" "$file") +done | sort -n | tail -1) + +OUTPUT_TIME=$(stat -c "%Y" "$OUTPUT" 2>/dev/null || echo 0) + +if test "$OUTPUT_TIME" -ge "$LATEST_FILE_TIME"; then + echo "mgems is up to date." + exit 0 +fi + +"$SCRIPT_DIR/../mruby/bin/mrbc" -o "$OUTPUT_TMP" "${FILES[@]}" + +{ + echo "#ifndef MGEMS_H" + echo "#define MGEMS_H" + xxd -i -n mgems $OUTPUT_TMP \ + | sed -e 's/^unsigned char /constexpr unsigned char /' \ + -e 's/^unsigned int /constexpr unsigned int /' + echo "#endif" +} > "$OUTPUT" + +echo "Compiled mgems!" \ No newline at end of file diff --git a/libs/l_mgems/pp.rb b/libs/l_mgems/pp.rb new file mode 100644 index 0000000..a931f61 --- /dev/null +++ b/libs/l_mgems/pp.rb @@ -0,0 +1,1010 @@ +# Copyright (C) 2014 Kouhei Sutou + +# This file is based on prettyprint.rb in CRuby. + +# This class implements a pretty printing algorithm. It finds line breaks and +# nice indentations for grouped structure. +# +# By default, the class assumes that primitive elements are strings and each +# byte in the strings have single column in width. But it can be used for +# other situations by giving suitable arguments for some methods: +# * newline object and space generation block for PrettyPrint.new +# * optional width argument for PrettyPrint#text +# * PrettyPrint#breakable +# +# There are several candidate uses: +# * text formatting using proportional fonts +# * multibyte characters which has columns different to number of bytes +# * non-string formatting +# +# == Bugs +# * Box based formatting? +# * Other (better) model/algorithm? +# +# Report any bugs at http://bugs.ruby-lang.org +# +# == References +# Christian Lindig, Strictly Pretty, March 2000, +# http://www.st.cs.uni-sb.de/~lindig/papers/#pretty +# +# Philip Wadler, A prettier printer, March 1998, +# http://homepages.inf.ed.ac.uk/wadler/topics/language-design.html#prettier +# +# == Author +# Tanaka Akira +# +class PrettyPrint + + # This is a convenience method which is same as follows: + # + # begin + # q = PrettyPrint.new(output, maxwidth, newline, &genspace) + # ... + # q.flush + # output + # end + # + def PrettyPrint.format(output='', maxwidth=79, newline="\n", genspace=lambda {|n| ' ' * n}) + q = PrettyPrint.new(output, maxwidth, newline, &genspace) + yield q + q.flush + output + end + + # This is similar to PrettyPrint::format but the result has no breaks. + # + # +maxwidth+, +newline+ and +genspace+ are ignored. + # + # The invocation of +breakable+ in the block doesn't break a line and is + # treated as just an invocation of +text+. + # + def PrettyPrint.singleline_format(output='', maxwidth=nil, newline=nil, genspace=nil) + q = SingleLine.new(output) + yield q + output + end + + # Creates a buffer for pretty printing. + # + # +output+ is an output target. If it is not specified, '' is assumed. It + # should have a << method which accepts the first argument +obj+ of + # PrettyPrint#text, the first argument +sep+ of PrettyPrint#breakable, the + # first argument +newline+ of PrettyPrint.new, and the result of a given + # block for PrettyPrint.new. + # + # +maxwidth+ specifies maximum line length. If it is not specified, 79 is + # assumed. However actual outputs may overflow +maxwidth+ if long + # non-breakable texts are provided. + # + # +newline+ is used for line breaks. "\n" is used if it is not specified. + # + # The block is used to generate spaces. {|width| ' ' * width} is used if it + # is not given. + # + def initialize(output='', maxwidth=79, newline="\n", &genspace) + @output = output + @maxwidth = maxwidth + @newline = newline + @genspace = genspace || lambda {|n| ' ' * n} + + @output_width = 0 + @buffer_width = 0 + @buffer = [] + + root_group = Group.new(0) + @group_stack = [root_group] + @group_queue = GroupQueue.new(root_group) + @indent = 0 + end + + # The output object. + # + # This defaults to '', and should accept the << method + attr_reader :output + + # The maximum width of a line, before it is separated in to a newline + # + # This defaults to 79, and should be a Fixnum + attr_reader :maxwidth + + # The value that is appended to +output+ to add a new line. + # + # This defaults to "\n", and should be String + attr_reader :newline + + # A lambda or Proc, that takes one argument, of a Fixnum, and returns + # the corresponding number of spaces. + # + # By default this is: + # lambda {|n| ' ' * n} + attr_reader :genspace + + # The number of spaces to be indented + attr_reader :indent + + # The PrettyPrint::GroupQueue of groups in stack to be pretty printed + attr_reader :group_queue + + # Returns the group most recently added to the stack. + # + # Contrived example: + # out = "" + # => "" + # q = PrettyPrint.new(out) + # => #, @output_width=0, @buffer_width=0, @buffer=[], @group_stack=[#], @group_queue=#]]>, @indent=0> + # q.group { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # } + # } + # } + # } + # => 284 + # puts out + # # + # # + # # + # # + def current_group + @group_stack.last + end + + # Breaks the buffer into lines that are shorter than #maxwidth + def break_outmost_groups + while @maxwidth < @output_width + @buffer_width + return unless group = @group_queue.deq + until group.breakables.empty? + data = @buffer.shift + @output_width = data.output(@output, @output_width) + @buffer_width -= data.width + end + while !@buffer.empty? && Text === @buffer.first + text = @buffer.shift + @output_width = text.output(@output, @output_width) + @buffer_width -= text.width + end + end + end + + # This adds +obj+ as a text of +width+ columns in width. + # + # If +width+ is not specified, obj.length is used. + # + def text(obj, width=obj.length) + if @buffer.empty? + @output << obj + @output_width += width + else + text = @buffer.last + unless Text === text + text = Text.new + @buffer << text + end + text.add(obj, width) + @buffer_width += width + break_outmost_groups + end + end + + # This is similar to #breakable except + # the decision to break or not is determined individually. + # + # Two #fill_breakable under a group may cause 4 results: + # (break,break), (break,non-break), (non-break,break), (non-break,non-break). + # This is different to #breakable because two #breakable under a group + # may cause 2 results: + # (break,break), (non-break,non-break). + # + # The text +sep+ is inserted if a line is not broken at this point. + # + # If +sep+ is not specified, " " is used. + # + # If +width+ is not specified, +sep.length+ is used. You will have to + # specify this when +sep+ is a multibyte character, for example. + # + def fill_breakable(sep=' ', width=sep.length) + group { breakable sep, width } + end + + # This says "you can break a line here if necessary", and a +width+\-column + # text +sep+ is inserted if a line is not broken at the point. + # + # If +sep+ is not specified, " " is used. + # + # If +width+ is not specified, +sep.length+ is used. You will have to + # specify this when +sep+ is a multibyte character, for example. + # + def breakable(sep=' ', width=sep.length) + group = @group_stack.last + if group.break? + flush + @output << @newline + @output << @genspace.call(@indent) + @output_width = @indent + @buffer_width = 0 + else + @buffer << Breakable.new(sep, width, self) + @buffer_width += width + break_outmost_groups + end + end + + # Groups line break hints added in the block. The line break hints are all + # to be used or not. + # + # If +indent+ is specified, the method call is regarded as nested by + # nest(indent) { ... }. + # + # If +open_obj+ is specified, text open_obj, open_width is called + # before grouping. If +close_obj+ is specified, text close_obj, + # close_width is called after grouping. + # + def group(indent=0, open_obj='', close_obj='', open_width=open_obj.length, close_width=close_obj.length) + text open_obj, open_width + group_sub { + nest(indent) { + yield + } + } + text close_obj, close_width + end + + # Takes a block and queues a new group that is indented 1 level further. + def group_sub + group = Group.new(@group_stack.last.depth + 1) + @group_stack.push group + @group_queue.enq group + begin + yield + ensure + @group_stack.pop + if group.breakables.empty? + @group_queue.delete group + end + end + end + + # Increases left margin after newline with +indent+ for line breaks added in + # the block. + # + def nest(indent) + @indent += indent + begin + yield + ensure + @indent -= indent + end + end + + # outputs buffered data. + # + def flush + @buffer.each {|data| + @output_width = data.output(@output, @output_width) + } + @buffer.clear + @buffer_width = 0 + end + + # The Text class is the means by which to collect strings from objects. + # + # This class is intended for internal use of the PrettyPrint buffers. + class Text # :nodoc: + + # Creates a new text object. + # + # This constructor takes no arguments. + # + # The workflow is to append a PrettyPrint::Text object to the buffer, and + # being able to call the buffer.last() to reference it. + # + # As there are objects, use PrettyPrint::Text#add to include the objects + # and the width to utilized by the String version of this object. + def initialize + @objs = [] + @width = 0 + end + + # The total width of the objects included in this Text object. + attr_reader :width + + # Render the String text of the objects that have been added to this Text object. + # + # Output the text to +out+, and increment the width to +output_width+ + def output(out, output_width) + @objs.each {|obj| out << obj} + output_width + @width + end + + # Include +obj+ in the objects to be pretty printed, and increment + # this Text object's total width by +width+ + def add(obj, width) + @objs << obj + @width += width + end + end + + # The Breakable class is used for breaking up object information + # + # This class is intended for internal use of the PrettyPrint buffers. + class Breakable # :nodoc: + + # Create a new Breakable object. + # + # Arguments: + # * +sep+ String of the separator + # * +width+ Fixnum width of the +sep+ + # * +q+ parent PrettyPrint object, to base from + def initialize(sep, width, q) + @obj = sep + @width = width + @pp = q + @indent = q.indent + @group = q.current_group + @group.breakables.push self + end + + # Holds the separator String + # + # The +sep+ argument from ::new + attr_reader :obj + + # The width of +obj+ / +sep+ + attr_reader :width + + # The number of spaces to indent. + # + # This is inferred from +q+ within PrettyPrint, passed in ::new + attr_reader :indent + + # Render the String text of the objects that have been added to this + # Breakable object. + # + # Output the text to +out+, and increment the width to +output_width+ + def output(out, output_width) + @group.breakables.shift + if @group.break? + out << @pp.newline + out << @pp.genspace.call(@indent) + @indent + else + @pp.group_queue.delete @group if @group.breakables.empty? + out << @obj + output_width + @width + end + end + end + + # The Group class is used for making indentation easier. + # + # While this class does neither the breaking into newlines nor indentation, + # it is used in a stack (as well as a queue) within PrettyPrint, to group + # objects. + # + # For information on using groups, see PrettyPrint#group + # + # This class is intended for internal use of the PrettyPrint buffers. + class Group # :nodoc: + # Create a Group object + # + # Arguments: + # * +depth+ - this group's relation to previous groups + def initialize(depth) + @depth = depth + @breakables = [] + @break = false + end + + # This group's relation to previous groups + attr_reader :depth + + # Array to hold the Breakable objects for this Group + attr_reader :breakables + + # Makes a break for this Group, and returns true + def break + @break = true + end + + # Boolean of whether this Group has made a break + def break? + @break + end + + # Boolean of whether this Group has been queried for being first + # + # This is used as a predicate, and ought to be called first. + def first? + if defined? @first + false + else + @first = false + true + end + end + end + + # The GroupQueue class is used for managing the queue of Group to be pretty + # printed. + # + # This queue groups the Group objects, based on their depth. + # + # This class is intended for internal use of the PrettyPrint buffers. + class GroupQueue # :nodoc: + # Create a GroupQueue object + # + # Arguments: + # * +groups+ - one or more PrettyPrint::Group objects + def initialize(*groups) + @queue = [] + groups.each {|g| enq g} + end + + # Enqueue +group+ + # + # This does not strictly append the group to the end of the queue, + # but instead adds it in line, base on the +group.depth+ + def enq(group) + depth = group.depth + @queue << [] until depth < @queue.length + @queue[depth] << group + end + + # Returns the outer group of the queue + def deq + @queue.each {|gs| + (gs.length-1).downto(0) {|i| + unless gs[i].breakables.empty? + gs = gs[i, 1] + group = gs.first + group.break + return group + end + } + gs.each {|group| group.break} + gs.clear + } + return nil + end + + # Remote +group+ from this queue + def delete(group) + @queue[group.depth].delete(group) + end + end + + # PrettyPrint::SingleLine is used by PrettyPrint.singleline_format + # + # It is passed to be similar to a PrettyPrint object itself, by responding to: + # * #text + # * #breakable + # * #nest + # * #group + # * #flush + # * #first? + # + # but instead, the output has no line breaks + # + class SingleLine + # Create a PrettyPrint::SingleLine object + # + # Arguments: + # * +output+ - String (or similar) to store rendered text. Needs to respond to '<<' + # * +maxwidth+ - Argument position expected to be here for compatibility. + # This argument is a noop. + # * +newline+ - Argument position expected to be here for compatibility. + # This argument is a noop. + def initialize(output, maxwidth=nil, newline=nil) + @output = output + @first = [true] + end + + # Add +obj+ to the text to be output. + # + # +width+ argument is here for compatibility. It is a noop argument. + def text(obj, width=nil) + @output << obj + end + + # Appends +sep+ to the text to be output. By default +sep+ is ' ' + # + # +width+ argument is here for compatibility. It is a noop argument. + def breakable(sep=' ', width=nil) + @output << sep + end + + # Takes +indent+ arg, but does nothing with it. + # + # Yields to a block. + def nest(indent) # :nodoc: + yield + end + + # Opens a block for grouping objects to be pretty printed. + # + # Arguments: + # * +indent+ - noop argument. Present for compatibility. + # * +open_obj+ - text appended before the &blok. Default is '' + # * +close_obj+ - text appended after the &blok. Default is '' + # * +open_width+ - noop argument. Present for compatibility. + # * +close_width+ - noop argument. Present for compatibility. + def group(indent=nil, open_obj='', close_obj='', open_width=nil, close_width=nil) + @first.push true + @output << open_obj + yield + @output << close_obj + @first.pop + end + + # Method present for compatibility, but is a noop + def flush # :nodoc: + end + + # This is used as a predicate, and ought to be called first. + def first? + result = @first[-1] + @first[-1] = false + result + end + end +end + +# Copyright (C) 2014-2020 Sutou Kouhei + +# This file is based on pp.rb in CRuby. + +module Kernel + # Returns a pretty printed object as a string. + # + # In order to use this method you must first require the PP module: + # + # require 'pp' + # + # See the PP module for more information. + def pretty_inspect + PP.pp(self, '') + end + + private + # prints arguments in pretty form. + # + # pp returns argument(s). + def pp(*objs) # :nodoc: + objs.each {|obj| + PP.pp(obj) + } + objs.size <= 1 ? objs.first : objs + end + module_function :pp # :nodoc: +end + +## +# A pretty-printer for Ruby objects. +# +# All examples assume you have loaded the PP class with: +# require 'pp' +# +## +# == What PP Does +# +# Standard output by #p returns this: +# #, @group_queue=#], []]>, @buffer=[], @newline="\n", @group_stack=[#], @buffer_width=0, @indent=0, @maxwidth=79, @output_width=2, @output=#> +# +# Pretty-printed output returns this: +# #, +# @group_queue= +# #], +# []]>, +# @group_stack= +# [#], +# @indent=0, +# @maxwidth=79, +# @newline="\n", +# @output=#, +# @output_width=2> +# +## +# == Usage +# +# pp(obj) #=> obj +# pp obj #=> obj +# pp(obj1, obj2, ...) #=> [obj1, obj2, ...] +# pp() #=> nil +# +# Output obj(s) to $> in pretty printed format. +# +# It returns obj(s). +# +## +# == Output Customization +# +# To define a customized pretty printing function for your classes, +# redefine method #pretty_print(pp) in the class. +# +# #pretty_print takes the +pp+ argument, which is an instance of the PP class. +# The method uses #text, #breakable, #nest, #group and #pp to print the +# object. +# +## +# == Pretty-Print JSON +# +# To pretty-print JSON refer to JSON#pretty_generate. +# +## +# == Author +# Tanaka Akira + +class PP < PrettyPrint + class StandardOutput + def <<(string) + print(string) + self + end + end + + # Outputs +obj+ to +out+ in pretty printed format of + # +width+ columns in width. + # + # If +out+ is omitted, $> is assumed. + # If +width+ is omitted, 79 is assumed. + # + # PP.pp returns +out+. + def PP.pp(obj, out=nil, width=79) + out ||= StandardOutput.new + q = PP.new(out, width) + q.guard_inspect_key {q.pp obj} + q.flush + #$pp = q + out << "\n" + end + + # Outputs +obj+ to +out+ like PP.pp but with no indent and + # newline. + # + # PP.singleline_pp returns +out+. + def PP.singleline_pp(obj, out=$>) + q = SingleLine.new(out) + q.guard_inspect_key {q.pp obj} + q.flush + out + end + + @sharing_detection = false + class << self + # Returns the sharing detection flag as a boolean value. + # It is false by default. + attr_accessor :sharing_detection + end + + module PPMethods + def initialize(*args) + super + @inspecting = {} + end + + # Yields to a block + # and preserves the previous set of objects being printed. + def guard_inspect_key + save = @inspecting + + begin + @inspecting = {} + yield + ensure + @inspecting = save + end + end + + # Check whether the object_id +id+ is in the current buffer of objects + # to be pretty printed. Used to break cycles in chains of objects to be + # pretty printed. + def check_inspect_key(id) + @inspecting.include?(id) + end + + # Adds the object_id +id+ to the set of objects being pretty printed, so + # as to not repeat objects. + def push_inspect_key(id) + @inspecting[id] = true + end + + # Removes an object from the set of objects being pretty printed. + def pop_inspect_key(id) + @inspecting.delete id + end + + # Adds +obj+ to the pretty printing buffer + # using Object#pretty_print or Object#pretty_print_cycle. + # + # Object#pretty_print_cycle is used when +obj+ is already + # printed, a.k.a the object reference chain has a cycle. + def pp(obj) + id = obj.object_id + + if check_inspect_key(id) + group {obj.pretty_print_cycle self} + return + end + + begin + push_inspect_key(id) + group {obj.pretty_print self} + ensure + pop_inspect_key(id) unless PP.sharing_detection + end + end + + # A convenience method which is same as follows: + # + # group(1, '#<' + obj.class.name, '>') { ... } + def object_group(obj, &block) # :yield: + group(1, '#<' + obj.class.name, '>', &block) + end + + # A convenience method, like object_group, but also reformats the Object's + # object_id. + def object_address_group(obj, &block) + str = obj.to_s + str.chomp!('>') + group(1, str, '>', &block) + end + + # A convenience method which is same as follows: + # + # text ',' + # breakable + def comma_breakable + text ',' + breakable + end + + # Adds a separated list. + # The list is separated by comma with breakable space, by default. + # + # #seplist iterates the +list+ using +iter_method+. + # It yields each object to the block given for #seplist. + # The procedure +separator_proc+ is called between each yields. + # + # If the iteration is zero times, +separator_proc+ is not called at all. + # + # If +separator_proc+ is nil or not given, + # +lambda { comma_breakable }+ is used. + # If +iter_method+ is not given, :each is used. + # + # For example, following 3 code fragments has similar effect. + # + # q.seplist([1,2,3]) {|v| xxx v } + # + # q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v } + # + # xxx 1 + # q.comma_breakable + # xxx 2 + # q.comma_breakable + # xxx 3 + def seplist(list, sep=nil, iter_method=:each) # :yield: element + sep ||= lambda { comma_breakable } + first = true + list.__send__(iter_method) {|*v| + if first + first = false + else + sep.call + end + yield(*v) + } + end + + # A present standard failsafe for pretty printing any given Object + def pp_object(obj) + object_address_group(obj) { + seplist(obj.pretty_print_instance_variables, lambda { text ',' }) {|v| + breakable + v = v.to_s if Symbol === v + text v + text '=' + group(1) { + breakable '' + pp(obj.instance_variable_get(v)) + } + } + } + end + + # A pretty print for a Hash + def pp_hash(obj) + group(1, '{', '}') { + seplist(obj, nil, :each) {|k, v| + group { + pp k + text '=>' + group(1) { + breakable '' + pp v + } + } + } + } + end + end + + include PPMethods + + class SingleLine < PrettyPrint::SingleLine # :nodoc: + include PPMethods + end + + module ObjectMixin # :nodoc: + # 1. specific pretty_print + # 2. specific inspect + # 3. generic pretty_print + + # A default pretty printing method for general objects. + # It calls #pretty_print_instance_variables to list instance variables. + # + # If +self+ has a customized (redefined) #inspect method, + # the result of self.inspect is used but it obviously has no + # line break hints. + # + # This module provides predefined #pretty_print methods for some of + # the most commonly used built-in classes for convenience. + def pretty_print(q) + if methods(false).include?(:inspect) + q.text self.inspect + else + q.pp_object(self) + end + end + + # A default pretty printing method for general objects that are + # detected as part of a cycle. + def pretty_print_cycle(q) + q.object_address_group(self) { + q.breakable + q.text '...' + } + end + + # Returns a sorted array of instance variable names. + # + # This method should return an array of names of instance variables as symbols or strings as: + # +[:@a, :@b]+. + def pretty_print_instance_variables + instance_variables.sort + end + + # Is #inspect implementation using #pretty_print. + # If you implement #pretty_print, it can be used as follows. + # + # alias inspect pretty_print_inspect + # + # However, doing this requires that every class that #inspect is called on + # implement #pretty_print, or a RuntimeError will be raised. + def pretty_print_inspect + PP.singleline_pp(self, '') + end + end +end + +class String # :nodoc: + def pretty_print(q) # :nodoc: + q.text inspect + end + + def pretty_print_cycle(q) # :nodoc: + q.text inspect + end +end + +class Array # :nodoc: + def pretty_print(q) # :nodoc: + q.group(1, '[', ']') { + q.seplist(self) {|v| + q.pp v + } + } + end + + def pretty_print_cycle(q) # :nodoc: + q.text(empty? ? '[]' : '[...]') + end +end + +class Hash # :nodoc: + def pretty_print(q) # :nodoc: + q.pp_hash self + end + + def pretty_print_cycle(q) # :nodoc: + q.text(empty? ? '{}' : '{...}') + end +end + +class Struct # :nodoc: + def pretty_print(q) # :nodoc: + q.group(1, sprintf("#') { + q.seplist(members, lambda { q.text "," }) {|member| + q.breakable + q.text member.to_s + q.text '=' + q.group(1) { + q.breakable '' + q.pp self[member] + } + } + } + end + + def pretty_print_cycle(q) # :nodoc: + q.text sprintf("#", self.class) + end +end + +class Range # :nodoc: + def pretty_print(q) # :nodoc: + q.pp self.begin + q.breakable '' + q.text(self.exclude_end? ? '...' : '..') + q.breakable '' + q.pp self.end + end +end + +class MatchData # :nodoc: + def pretty_print(q) # :nodoc: + nc = [] + self.regexp.named_captures.each {|name, indexes| + indexes.each {|i| nc[i] = name } + } + q.object_group(self) { + q.breakable + q.seplist(0...self.size, lambda { q.breakable }) {|i| + if i == 0 + q.pp self[i] + else + if nc[i] + q.text nc[i] + else + q.pp i + end + q.text ':' + q.pp self[i] + end + } + } + end +end + +class Object < BasicObject # :nodoc: + include PP::ObjectMixin + + [Numeric, Symbol, FalseClass, TrueClass, NilClass, Module].each {|c| + c.class_eval { + def pretty_print_cycle(q) + q.text inspect + end + } + } + + [Numeric, FalseClass, TrueClass, NilClass, Module].each {|c| + c.class_eval { + def pretty_print(q) + q.text inspect + end + } + } +end \ No newline at end of file diff --git a/samples/button.rb b/samples/button.rb index c77b166..54dd67f 100644 --- a/samples/button.rb +++ b/samples/button.rb @@ -22,6 +22,7 @@ rect.on_click do rect.color = rect.color == 0xFF0000 ? 0x00FF00 : 0xFF0000 + main_scene << text unless main_scene.elements.include?(text) text.params[:name] = text.params[:name] == "Me" ? "You" : "Me" @@ -30,35 +31,45 @@ end App.map_key :mouse_left, :press +start = Time.now +c_ = 0.0 +c_c = 0 + main_scene.on_update do |delta_time| - # puts "Delta time: #{delta_time}ms" + c_ += delta_time + c_c += 1 + + # puts "Delta time: #{delta_time}s" # commented out to avoid spamming the console, but you can uncomment to see the delta time being printed every frame # runs at 60fps, so you should see around 16-17ms being printed el = main_scene.elements_at(App.mouse_position) - puts App.text_input if App.text_input != "" + pp App.text_input if App.text_input != "" next unless App.pressed?(:press) - puts "#{App.pressed?(:press)}, #{App.down?(:press)}, #{App.released?(:press)}" + pp "#{App.pressed?(:press)}, #{App.down?(:press)}, #{App.released?(:press)}" for elem in el if elem == image_element - puts "Down over image element" + pp "Down over image element" elsif elem == rect - puts "Down over rect element" + pp "Down over rect element" elsif elem == text - puts "Down over text element" + pp "Down over text element" end end - end main_scene << text main_scene << rect main_scene << image_element -# main_scene.delete(text) # Uncomment to test element deletion +main_scene.delete(text) -App.start main_scene \ No newline at end of file +App.start main_scene + +p "Average delta time: #{c_ / c_c}s" +p "Time since start (real): #{Time.now - start}s" +p "Time since start (from dt): #{c_}s" \ No newline at end of file diff --git a/src/main.cc b/src/main.cc index fcf5b60..9d50bb7 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,6 +1,7 @@ #include "binding/definitions.h" int main(int argc, char *argv[]) { + Ruby::load_mgems(); definitions::setup(); char *startup_script = nullptr; diff --git a/src/window/window.cc b/src/window/window.cc index bcec760..45226d1 100644 --- a/src/window/window.cc +++ b/src/window/window.cc @@ -451,14 +451,17 @@ const char *Window::paste() { } uint64_t Window::get_time() { - return SDL_GetTicks(); + return SDL_GetTicksNS(); } uint64_t Window::delta_time() { static uint64_t last_time = 0; - uint64_t current_time = SDL_GetTicks(); + static bool first_call = 1; + uint64_t current_time = SDL_GetTicksNS(); uint64_t delta = current_time - last_time; last_time = current_time; + if (first_call) + return (first_call = 0); return delta; } diff --git a/writeup/analysis.md b/writeup/analysis.md index 5e9e881..bb3320e 100644 --- a/writeup/analysis.md +++ b/writeup/analysis.md @@ -234,6 +234,7 @@ Other features that a game engine should have/has (need to go in depth) - audio playing - network interface, with a lib to work with rack backends (and generic socket api) - persistence store +- rng helpers ## Embedded scripting language choice.