rgame 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +228 -0
- data/README.md +142 -243
- data/docs/api/README.md +141 -90
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -166
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1045 -254
- data/docs/api/drawing.md +187 -119
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +165 -45
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +344 -85
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +503 -123
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +197 -64
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +400 -147
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +234 -19
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/README.md +6 -5
- data/ext/rgame_core/app/app.c +215 -11
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +113 -2
- data/ext/rgame_core/example.rb +18 -56
- data/ext/rgame_core/extconf.rb +16 -113
- data/ext/rgame_core/graphics/canvas.c +45 -4
- data/ext/rgame_core/graphics/canvas.h +65 -10
- data/ext/rgame_core/graphics/clip.c +22 -13
- data/ext/rgame_core/include/rgame/core.h +151 -5
- data/ext/rgame_core/input/gamepad.c +57 -3
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +46 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +42 -19
- data/ext/rgame_util/color_ext.c +12 -3
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -42
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/input.rb +35 -41
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/recording.rb +3 -1
- data/lib/rgame/core/renderer.rb +75 -84
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +77 -65
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +55 -10
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +35 -1
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +31 -23
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +21 -41
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +22 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +70 -29
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +23 -18
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +76 -22
- data/lib/rgame/engine/input/actions.rb +63 -12
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +364 -80
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +29 -7
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +53 -8
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +290 -0
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +43 -25
- data/lib/rgame/game.rb +164 -22
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +106 -44
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +5 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +176 -20
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/camera_view.rb +0 -28
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
data/ext/rgame_core/example.rb
CHANGED
|
@@ -1,30 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Manual/visual smoke test for the core extension (layer 3 — the real
|
|
4
|
-
# SDL/GL path). Build it first, then run this from the project root:
|
|
5
|
-
#
|
|
6
|
-
# make ext-core
|
|
7
|
-
# ruby ext/rgame_core/example.rb
|
|
8
|
-
#
|
|
9
|
-
# It opens a window with one of each drawing primitive in it. Arrow keys (or a
|
|
10
|
-
# controller's dpad/stick) move the marker; Escape or closing the window quits.
|
|
11
|
-
# This is the Ruby-side mirror of src/main.c, and it exists to exercise every
|
|
12
|
-
# callback and every draw call the engine offers.
|
|
13
|
-
#
|
|
14
|
-
# Pass a sound file (Ogg Vorbis or WAV) to check audio as well:
|
|
15
|
-
#
|
|
16
|
-
# ruby ext/rgame_core/example.rb assets/theme.ogg
|
|
17
|
-
#
|
|
18
|
-
# Space plays it as a one-shot sample, Return starts and stops it as looping
|
|
19
|
-
# music. There is no asset here to default to, and that is on purpose: this is
|
|
20
|
-
# the only place a *real* sound device is driven — every automated test runs
|
|
21
|
-
# against a null or offline one — so what it is worth checking is your own
|
|
22
|
-
# file, out of your own speakers.
|
|
23
|
-
#
|
|
24
|
-
# The load path points at lib/, not at this directory: `make ext-core`
|
|
25
|
-
# copies the built core_ext.so to lib/rgame/, so this runs against exactly
|
|
26
|
-
# the layout a user of the library would see.
|
|
27
|
-
|
|
28
3
|
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
29
4
|
require 'rgame'
|
|
30
5
|
require 'rgame/core'
|
|
@@ -41,8 +16,6 @@ class Example < RGame::Core::App
|
|
|
41
16
|
YELLOW = Color.new(224, 192, 64)
|
|
42
17
|
TRANSLUCENT_WHITE = Color.new(255, 255, 255, 128)
|
|
43
18
|
|
|
44
|
-
# Frozen and chosen between rather than built: #draw runs sixty times a
|
|
45
|
-
# second, and a string per frame is a garbage collection waiting to happen.
|
|
46
19
|
AUDIO_HINT_NONE = 'audio: pass a sound file on the command line to try it'
|
|
47
20
|
AUDIO_HINT_STOPPED = 'audio: Space plays a sample, Return starts the music'
|
|
48
21
|
AUDIO_HINT_PLAYING = 'audio: Space plays a sample, Return stops the music'
|
|
@@ -62,10 +35,6 @@ class Example < RGame::Core::App
|
|
|
62
35
|
@baked = nil
|
|
63
36
|
@device = Controls::KEYBOARD
|
|
64
37
|
|
|
65
|
-
# The device is opened whether or not there is anything to play through it,
|
|
66
|
-
# so that starting with no sound card takes the same path as starting with
|
|
67
|
-
# one. Nothing here is tied to the window: audio has no GL context and
|
|
68
|
-
# survives one being recreated.
|
|
69
38
|
@audio = RGame::Core::Audio.new
|
|
70
39
|
puts "audio backend: #{@audio.backend}"
|
|
71
40
|
return unless sound_path
|
|
@@ -78,8 +47,6 @@ class Example < RGame::Core::App
|
|
|
78
47
|
# here rather than per tick is the pattern the engine is shaped around — one
|
|
79
48
|
# sample per frame, reused by however many catch-up ticks follow.
|
|
80
49
|
def frame_begin
|
|
81
|
-
# Poll rather than track: Gamepad answers straight from the engine, so
|
|
82
|
-
# there is no local copy of the connection state to keep in step.
|
|
83
50
|
@device = @pads.connected?(0) ? @pads.device(0) : Controls::KEYBOARD
|
|
84
51
|
end
|
|
85
52
|
|
|
@@ -88,51 +55,48 @@ class Example < RGame::Core::App
|
|
|
88
55
|
@ticks += 1
|
|
89
56
|
@spin += dt * 45.0
|
|
90
57
|
speed = 200.0 * dt
|
|
91
|
-
@cursor_x -= speed if
|
|
92
|
-
@cursor_x += speed if
|
|
93
|
-
@cursor_y -= speed if
|
|
94
|
-
@cursor_y += speed if
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@
|
|
98
|
-
@cursor_y += @input.axis(:move_y, device: @device) * speed
|
|
58
|
+
@cursor_x -= speed if held?(Controls::KEY_LEFT, Controls::PAD_DPAD_LEFT)
|
|
59
|
+
@cursor_x += speed if held?(Controls::KEY_RIGHT, Controls::PAD_DPAD_RIGHT)
|
|
60
|
+
@cursor_y -= speed if held?(Controls::KEY_UP, Controls::PAD_DPAD_UP)
|
|
61
|
+
@cursor_y += speed if held?(Controls::KEY_DOWN, Controls::PAD_DPAD_DOWN)
|
|
62
|
+
|
|
63
|
+
@cursor_x += @input.axis(Controls::AXIS_LEFT_X, device: @device) * speed
|
|
64
|
+
@cursor_y += @input.axis(Controls::AXIS_LEFT_Y, device: @device) * speed
|
|
99
65
|
end
|
|
100
66
|
|
|
67
|
+
# Core is the raw layer: Input answers "is this scancode down on this device",
|
|
68
|
+
# and naming the key *and* the pad button for one intent is the caller's job.
|
|
69
|
+
# Asking both costs nothing, because a device only answers for its own kind of
|
|
70
|
+
# input — a gamepad reads false for a scancode.
|
|
71
|
+
#
|
|
72
|
+
# A game does not write this. RGame::Engine::InputMap is a table of exactly
|
|
73
|
+
# these pairs, one entry per action, and the scene graph reads named actions
|
|
74
|
+
# instead. This example is here to exercise Core on its own, so it does
|
|
75
|
+
# without that and shows what the layer above is for.
|
|
76
|
+
def held?(key, pad) = @input.down?(key, device: @device) || @input.down?(pad, device: @device)
|
|
77
|
+
|
|
101
78
|
# One of everything, so a look at the window checks the whole drawing path.
|
|
102
79
|
# Colours are RGame::Util::Color values rather than arrays: a Color is frozen
|
|
103
80
|
# and allocates nothing when it is drawn, which is what per-frame code wants.
|
|
104
81
|
def draw
|
|
105
82
|
r = @renderer
|
|
106
83
|
|
|
107
|
-
# Baked on the first frame and replayed after that: forty rectangles cost
|
|
108
|
-
# one call per frame instead of forty. A real game bakes its tile layers
|
|
109
|
-
# this way. Recording has to happen inside #draw, which is why it is here
|
|
110
|
-
# rather than in initialize.
|
|
111
84
|
@baked ||= r.record do
|
|
112
85
|
40.times { |i| r.rect(i * 18, 0, 12, 12, color: GREEN, z: 0) }
|
|
113
86
|
end
|
|
114
|
-
# Scrolls with the same value the spinning square turns by, so it is
|
|
115
|
-
# visibly the *replay* moving and not a rebake.
|
|
116
87
|
@baked.draw((@spin % 18) - 18, 560)
|
|
117
88
|
r.rect(40, 40, 160, 100, color: RED, z: 0)
|
|
118
|
-
# Higher z wins wherever they overlap, whatever order the calls came in.
|
|
119
89
|
r.rect(120, 90, 160, 100, color: TRANSLUCENT_WHITE, z: 1)
|
|
120
90
|
r.triangle(400, 40, 480, 180, 320, 180, color: GREEN, z: 0)
|
|
121
91
|
r.circle(620, 110, 70, color: BLUE, z: 0)
|
|
122
92
|
r.line(40, 240, 760, 240, thickness: 6, color: YELLOW, z: 0)
|
|
123
93
|
|
|
124
|
-
# The transform stack: a square turning about its own centre.
|
|
125
94
|
r.rotated(@spin, 400, 380) { r.rect(340, 320, 120, 120, color: GREEN, z: 0) }
|
|
126
95
|
|
|
127
|
-
# The clip stack: the rectangle is twice the size of what shows.
|
|
128
96
|
r.clipped(60, 480, 200, 80) { r.rect(60, 440, 400, 160, color: RED, z: 0) }
|
|
129
97
|
|
|
130
|
-
# The cursor the arrow keys move.
|
|
131
98
|
r.debug_box(@cursor_x - 8, @cursor_y - 8, 16, 16)
|
|
132
99
|
|
|
133
|
-
# Text, including accents and punctuation the shipped font has to cover, so
|
|
134
|
-
# a look at the window checks kerning and coverage at once. The second line
|
|
135
|
-
# is centred using text_width, which is what a UI actually does with it.
|
|
136
100
|
r.text('rgame — Grüße, œuvre, 5 €', 40, 270, color: YELLOW)
|
|
137
101
|
label = format('%d fps', fps)
|
|
138
102
|
r.text(label, 400 - (r.text_width(label) / 2), 270 + r.text_height)
|
|
@@ -144,8 +108,6 @@ class Example < RGame::Core::App
|
|
|
144
108
|
def button_down(id)
|
|
145
109
|
case id
|
|
146
110
|
when Controls::KEY_ESCAPE then close
|
|
147
|
-
# Held down, this is the overlap check: each press is another voice rather
|
|
148
|
-
# than a restart, so a fast run of them should pile up rather than stutter.
|
|
149
111
|
when Controls::KEY_SPACE then @sample&.play
|
|
150
112
|
when Controls::KEY_RETURN then toggle_music
|
|
151
113
|
end
|
data/ext/rgame_core/extconf.rb
CHANGED
|
@@ -1,136 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# extconf.rb — run by `ruby extconf.rb` to generate a Makefile via mkmf.
|
|
4
|
-
#
|
|
5
|
-
# mkmf is Ruby's "make makefile" library. It probes the system (headers,
|
|
6
|
-
# libraries, pkg-config) and writes a Makefile that knows how to compile a
|
|
7
|
-
# loadable extension (`core_ext.so`) against the running Ruby's headers.
|
|
8
|
-
# This is the same tool that `gem install` uses under the hood, which is why the
|
|
9
|
-
# root Makefile was written to look like what mkmf emits — the mental model
|
|
10
|
-
# carries straight over.
|
|
11
|
-
#
|
|
12
|
-
# This is the SDL/OpenGL half of the project: everything under RGame::Core.
|
|
13
|
-
# The graphics-free half lives in ../rgame_util (RGame::Util) and links none of
|
|
14
|
-
# these libraries.
|
|
15
|
-
|
|
16
3
|
require 'mkmf'
|
|
17
4
|
|
|
18
|
-
# The engine sources live in this directory rather than a top-level src/:
|
|
19
|
-
# `gem install` unpacks the gem and runs this script, and an extension must be
|
|
20
|
-
# buildable from its own directory.
|
|
21
|
-
#
|
|
22
|
-
# They are grouped into subdirectories by subsystem (app, graphics, text, input,
|
|
23
|
-
# audio) plus ruby/ for the Ruby-facing glue and vendor/ for third-party code.
|
|
24
|
-
# mkmf's default is to compile every .c in the extension's *own* directory and
|
|
25
|
-
# nothing deeper, so the source list has to be spelled out — which is what
|
|
26
|
-
# $srcs and $VPATH below do.
|
|
27
|
-
#
|
|
28
|
-
# $srcs — what to compile. mkmf names each object after the source's
|
|
29
|
-
# *basename*, so the .o files land flat in this directory no matter
|
|
30
|
-
# how deep the .c was. Basenames therefore have to be unique across
|
|
31
|
-
# the subdirectories; mkmf aborts with "source files duplication" if
|
|
32
|
-
# they ever aren't, so that rule enforces itself.
|
|
33
|
-
# $VPATH — where make looks for a source when a rule names it by basename.
|
|
34
|
-
# The generic `.c.o` rule mkmf emits does exactly that, so without
|
|
35
|
-
# this every object would come up as a missing prerequisite.
|
|
36
|
-
#
|
|
37
|
-
# vendor/ contributes only its <name>_impl.c translation units. The libraries
|
|
38
|
-
# themselves are #included *by* those files (stb_vorbis even ships as a .c), so
|
|
39
|
-
# compiling them separately would duplicate every symbol in them.
|
|
40
5
|
SOURCE_DIRS = %w[app graphics text input audio ruby].freeze
|
|
41
6
|
|
|
42
|
-
# Dir.glob sorts its results, so the object list is the same on every machine.
|
|
43
7
|
$srcs = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.c") } +
|
|
44
8
|
Dir.glob("#{$srcdir}/vendor/*_impl.c")
|
|
45
9
|
|
|
46
10
|
(SOURCE_DIRS + %w[vendor]).each { |dir| $VPATH << "$(srcdir)/#{dir}" }
|
|
47
11
|
|
|
48
|
-
# The extension directory itself, so a cross-subsystem include names the folder
|
|
49
|
-
# it comes from: graphics/canvas.c says `#include "graphics/clip.h"`, and text/
|
|
50
|
-
# reaching into graphics/ is visible in the source rather than hidden in a
|
|
51
|
-
# search path. It is also what resolves `#include "vendor/stb_image.h"`.
|
|
52
|
-
# $(srcdir) stays literal here — make expands it, not Ruby.
|
|
53
12
|
$INCFLAGS << ' -I$(srcdir)'
|
|
54
13
|
|
|
55
|
-
# Public API header (include/rgame/core.h), so `#include "rgame/core.h"`
|
|
56
|
-
# resolves.
|
|
57
14
|
$INCFLAGS << ' -I$(srcdir)/include'
|
|
58
15
|
|
|
59
|
-
# RGame::Util's colour header, for the RGBA byte order the renderer writes into
|
|
60
|
-
# every vertex. It is header-only (static inline), so this creates no link
|
|
61
|
-
# dependency between the two extensions — they stay separate .so files. Both
|
|
62
|
-
# directories ship together in the gem, so the relative path holds there too.
|
|
63
16
|
$INCFLAGS << ' -I$(srcdir)/../rgame_util'
|
|
64
17
|
|
|
65
|
-
# SDL2: pkg_config("sdl2") shells out to pkg-config and folds the resulting
|
|
66
|
-
# cflags and libs into mkmf's globals ($CFLAGS/$libs) for us. Returns nil if
|
|
67
|
-
# SDL2 isn't found, so we can fail with a clear message instead of a confusing
|
|
68
|
-
# later compile error.
|
|
69
18
|
abort 'SDL2 not found (pkg-config --exists sdl2 failed). Install libsdl2-dev.' unless pkg_config('sdl2')
|
|
70
19
|
|
|
71
|
-
# OpenGL + libm, matching the link line in the root Makefile. append_library
|
|
72
|
-
# adds `-lGL`/`-lm` in the right spot on the link command.
|
|
73
|
-
#
|
|
74
|
-
# Checked rather than assumed, unlike in the root Makefile. This script is what
|
|
75
|
-
# runs on someone else's machine during `gem install`, and a missing OpenGL
|
|
76
|
-
# development package is a routine thing to hit there. Without a check it
|
|
77
|
-
# surfaces as a linker error about an undefined `glClear` at the end of a long
|
|
78
|
-
# build; with one, the install stops on a sentence naming the package to
|
|
79
|
-
# install. Same reasoning as the SDL2 abort above.
|
|
80
|
-
#
|
|
81
|
-
# The header probed is the one the sources actually include — SDL's own
|
|
82
|
-
# <SDL2/SDL_opengl.h>, which resolves to GL/gl.h or OpenGL/gl.h per platform, so
|
|
83
|
-
# probing GL/gl.h directly would ask the wrong question off Linux. It needs
|
|
84
|
-
# SDL's cflags, which pkg_config folded into $CFLAGS just above.
|
|
85
20
|
abort 'SDL2 OpenGL header not found (SDL2/SDL_opengl.h). Install libsdl2-dev.' unless have_header('SDL2/SDL_opengl.h')
|
|
86
21
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
22
|
+
case RbConfig::CONFIG['host_os']
|
|
23
|
+
when /darwin/
|
|
24
|
+
unless have_framework('OpenGL')
|
|
25
|
+
abort 'OpenGL framework not found. It ships with macOS, so this usually means ' \
|
|
26
|
+
'the command line tools are missing: xcode-select --install'
|
|
27
|
+
end
|
|
28
|
+
when /mingw|mswin|cygwin/
|
|
29
|
+
unless have_library('opengl32', 'glClear')
|
|
30
|
+
abort 'opengl32 not found. It is part of Windows itself, so this means an ' \
|
|
31
|
+
'incomplete MSYS2 toolchain: pacman -S mingw-w64-ucrt-x86_64-gcc'
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
unless have_library('GL', 'glClear')
|
|
35
|
+
abort 'OpenGL library not found (-lGL). Install the OpenGL development package: ' \
|
|
36
|
+
'libgl1-mesa-dev on Debian/Ubuntu, mesa-libGL-devel on Fedora.'
|
|
37
|
+
end
|
|
95
38
|
end
|
|
96
39
|
|
|
97
40
|
$libs = append_library($libs, 'm')
|
|
98
41
|
|
|
99
|
-
# miniaudio's threading, and the dlopen it uses to find ALSA or PulseAudio at
|
|
100
|
-
# runtime — the property that makes audio cost no new system dependency. Both
|
|
101
|
-
# live in glibc on Linux/BSD. Windows and macOS need neither, and appending them
|
|
102
|
-
# unconditionally would fail the link there, so each is probed first.
|
|
103
42
|
$libs = append_library($libs, 'pthread') if have_library('pthread')
|
|
104
43
|
$libs = append_library($libs, 'dl') if have_library('dl')
|
|
105
44
|
|
|
106
|
-
# Match the project's C standard and warning flags. Note: gnu17, not plain
|
|
107
|
-
# c17 — Ruby's headers occasionally lean on GNU extensions, and gnu17 is a
|
|
108
|
-
# superset of c17 so core.c (which targets c17) still compiles fine.
|
|
109
45
|
$CFLAGS << ' -std=gnu17 -Wall -Wextra'
|
|
110
46
|
|
|
111
|
-
# Emits the Makefile. "rgame/core_ext" -> loaded via
|
|
112
|
-
# `require "rgame/core_ext"`, entry point Init_core_ext (the basename)
|
|
113
|
-
# in core_ext.c. Namespacing it under rgame/ mirrors rgame/util_ext, keeps
|
|
114
|
-
# both extensions off the top of the load path, and — importantly — leaves the
|
|
115
|
-
# bare name "rgame" to lib/rgame.rb, which is the pure-Ruby entry point.
|
|
116
47
|
create_makefile('rgame/core_ext')
|
|
117
48
|
|
|
118
|
-
# The vendored implementations, compiled with warnings off (see
|
|
119
|
-
# vendor/README.md). mkmf has no per-file flag setting, so the rules are
|
|
120
|
-
# appended to the Makefile it just wrote. An explicit rule for a specific
|
|
121
|
-
# target beats mkmf's generic .c.o suffix rule, so these are what get used for
|
|
122
|
-
# those objects and nothing else.
|
|
123
|
-
#
|
|
124
|
-
# Written from one list rather than one block per library: a second hand-copied
|
|
125
|
-
# rule is how the first one drifts. They are explicit rules rather than a `%`
|
|
126
|
-
# pattern because mkmf's Makefiles are meant to work with whatever `make` the
|
|
127
|
-
# platform has, and pattern rules are a GNU extension.
|
|
128
|
-
#
|
|
129
|
-
# `-w` comes last on the command line and switches every warning back off,
|
|
130
|
-
# which is simpler and more robust than trying to subtract -Wall -Wextra from
|
|
131
|
-
# $(CFLAGS) — everything else about how the extension compiles stays identical.
|
|
132
|
-
# Each vendored library's implementation TU (<name>_impl.c) and the file it
|
|
133
|
-
# instantiates. stb_vorbis is the odd one out: it ships as a .c, not a .h.
|
|
134
49
|
VENDORED = {
|
|
135
50
|
'stb_image' => 'stb_image.h',
|
|
136
51
|
'stb_truetype' => 'stb_truetype.h',
|
|
@@ -148,18 +63,6 @@ File.open('Makefile', 'a') do |makefile|
|
|
|
148
63
|
MAKE
|
|
149
64
|
end
|
|
150
65
|
|
|
151
|
-
# Rebuild everything when any of our headers changes.
|
|
152
|
-
#
|
|
153
|
-
# mkmf emits `$(OBJS): $(HDRS)` for this, but it builds HDRS by globbing the
|
|
154
|
-
# extension's own directory only — with the headers a level down in
|
|
155
|
-
# graphics/, text/ and the rest, HDRS comes out empty and editing a header
|
|
156
|
-
# rebuilds *nothing*. That failure is silent: the build succeeds and links
|
|
157
|
-
# objects compiled against the previous version of the struct.
|
|
158
|
-
#
|
|
159
|
-
# One coarse dependency rather than a real per-object dependency scan, for the
|
|
160
|
-
# same reason the root Makefile treats the vendored sources as one list: this
|
|
161
|
-
# is a small project where a full rebuild costs seconds, and a hand-maintained
|
|
162
|
-
# dependency graph is a thing to get quietly wrong.
|
|
163
66
|
headers = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.h") }
|
|
164
67
|
.map { |path| "$(srcdir)/#{path.delete_prefix("#{$srcdir}/")}" }
|
|
165
68
|
|
|
@@ -6,15 +6,26 @@
|
|
|
6
6
|
enum {
|
|
7
7
|
RGAME_PUSH_TRANSFORM,
|
|
8
8
|
RGAME_PUSH_CLIP,
|
|
9
|
+
RGAME_PUSH_LAYER,
|
|
9
10
|
/* The underlying stack was full. Nothing to undo, but it still occupies a
|
|
10
11
|
* slot so that the caller's matching pop stays paired with this push. */
|
|
11
12
|
RGAME_PUSH_NOTHING
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
/* The layer stack and the per-band counters, both back to their frame start. */
|
|
16
|
+
static void reset_layers(rgame_canvas *canvas) {
|
|
17
|
+
canvas->layers[0] = 0.0;
|
|
18
|
+
canvas->layer_depth = 0;
|
|
19
|
+
for (int i = 0; i < RGAME_LAYER_BANDS; i++) {
|
|
20
|
+
canvas->slots[i] = 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
void rgame_canvas_init(rgame_canvas *canvas) {
|
|
15
25
|
rgame_draw_queue_init(&canvas->queue);
|
|
16
26
|
rgame_transform_stack_init(&canvas->transforms);
|
|
17
27
|
rgame_clip_stack_init(&canvas->clips, 0, 0);
|
|
28
|
+
reset_layers(canvas);
|
|
18
29
|
canvas->push_depth = 0;
|
|
19
30
|
canvas->unrecorded_pushes = 0;
|
|
20
31
|
canvas->width = 0;
|
|
@@ -29,6 +40,7 @@ void rgame_canvas_begin_frame(rgame_canvas *canvas, int width, int height) {
|
|
|
29
40
|
rgame_draw_queue_reset(&canvas->queue);
|
|
30
41
|
rgame_transform_stack_init(&canvas->transforms);
|
|
31
42
|
rgame_clip_stack_init(&canvas->clips, width, height);
|
|
43
|
+
reset_layers(canvas);
|
|
32
44
|
canvas->push_depth = 0;
|
|
33
45
|
canvas->unrecorded_pushes = 0;
|
|
34
46
|
canvas->width = width;
|
|
@@ -126,6 +138,26 @@ void rgame_canvas_push_clip(rgame_canvas *canvas, rgame_rect rect) {
|
|
|
126
138
|
account_push(canvas, ok, RGAME_PUSH_CLIP);
|
|
127
139
|
}
|
|
128
140
|
|
|
141
|
+
void rgame_canvas_push_layer(rgame_canvas *canvas, double base) {
|
|
142
|
+
int ok = canvas->layer_depth + 1 < RGAME_LAYER_STACK_DEPTH;
|
|
143
|
+
if (ok) {
|
|
144
|
+
/* Replaces rather than accumulates: see canvas.h. */
|
|
145
|
+
canvas->layers[++canvas->layer_depth] = base;
|
|
146
|
+
}
|
|
147
|
+
account_push(canvas, ok, RGAME_PUSH_LAYER);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
double rgame_canvas_layer(const rgame_canvas *canvas) {
|
|
151
|
+
return canvas->layers[canvas->layer_depth];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
unsigned int rgame_canvas_next_slot(rgame_canvas *canvas, int band) {
|
|
155
|
+
if (band < 0 || band >= RGAME_LAYER_BANDS) {
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
return canvas->slots[band]++;
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
void rgame_canvas_pop(rgame_canvas *canvas) {
|
|
130
162
|
/* Unrecorded pushes unwind first: they are the most recent ones, since a
|
|
131
163
|
* push is only left unrecorded once the record stack is already full. */
|
|
@@ -144,11 +176,20 @@ void rgame_canvas_pop(rgame_canvas *canvas) {
|
|
|
144
176
|
case RGAME_PUSH_CLIP:
|
|
145
177
|
rgame_clip_pop(&canvas->clips);
|
|
146
178
|
break;
|
|
179
|
+
case RGAME_PUSH_LAYER:
|
|
180
|
+
canvas->layer_depth--;
|
|
181
|
+
break;
|
|
147
182
|
default:
|
|
148
183
|
break; /* RGAME_PUSH_NOTHING: the push never took effect */
|
|
149
184
|
}
|
|
150
185
|
}
|
|
151
186
|
|
|
187
|
+
/* Every z that reaches the queue is an offset from the current layer base. One
|
|
188
|
+
* addition, in the one place all four queueing paths pass through. */
|
|
189
|
+
static double layer_z(const rgame_canvas *canvas, double z) {
|
|
190
|
+
return canvas->layers[canvas->layer_depth] + z;
|
|
191
|
+
}
|
|
192
|
+
|
|
152
193
|
/* Fills one vertex: map the point into screen space, copy through the texture
|
|
153
194
|
* coordinate, and write the colour as the four bytes GL reads. */
|
|
154
195
|
static void write_vertex(const rgame_canvas *canvas, rgame_vertex *vertex, float x, float y,
|
|
@@ -161,7 +202,7 @@ static void write_vertex(const rgame_canvas *canvas, rgame_vertex *vertex, float
|
|
|
161
202
|
|
|
162
203
|
void rgame_canvas_triangle(rgame_canvas *canvas, const float *xy6, rgame_color color,
|
|
163
204
|
double z) {
|
|
164
|
-
rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 3, z, 0,
|
|
205
|
+
rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 3, layer_z(canvas, z), 0,
|
|
165
206
|
rgame_clip_current(&canvas->clips));
|
|
166
207
|
for (int i = 0; i < 3; i++) {
|
|
167
208
|
write_vertex(canvas, &out[i], xy6[i * 2], xy6[(i * 2) + 1], 0.0f, 0.0f, color);
|
|
@@ -172,7 +213,7 @@ void rgame_canvas_triangle(rgame_canvas *canvas, const float *xy6, rgame_color c
|
|
|
172
213
|
static const int RGAME_QUAD_TRIANGLES[6] = { 0, 1, 2, 0, 2, 3 };
|
|
173
214
|
|
|
174
215
|
void rgame_canvas_quad(rgame_canvas *canvas, const float *xy8, rgame_color color, double z) {
|
|
175
|
-
rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, z, 0,
|
|
216
|
+
rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, layer_z(canvas, z), 0,
|
|
176
217
|
rgame_clip_current(&canvas->clips));
|
|
177
218
|
for (int i = 0; i < 6; i++) {
|
|
178
219
|
int corner = RGAME_QUAD_TRIANGLES[i];
|
|
@@ -183,7 +224,7 @@ void rgame_canvas_quad(rgame_canvas *canvas, const float *xy8, rgame_color color
|
|
|
183
224
|
|
|
184
225
|
void rgame_canvas_textured_quad(rgame_canvas *canvas, unsigned int texture, const float *xy8,
|
|
185
226
|
const float *uv8, rgame_color color, double z) {
|
|
186
|
-
rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, z, texture,
|
|
227
|
+
rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, layer_z(canvas, z), texture,
|
|
187
228
|
rgame_clip_current(&canvas->clips));
|
|
188
229
|
for (int i = 0; i < 6; i++) {
|
|
189
230
|
int corner = RGAME_QUAD_TRIANGLES[i];
|
|
@@ -213,7 +254,7 @@ void rgame_canvas_replay(rgame_canvas *canvas, const rgame_recording *recording,
|
|
|
213
254
|
/* One command per baked batch, rather than one per original draw call:
|
|
214
255
|
* the whole point of a recording is that the per-tile work happened
|
|
215
256
|
* once, at bake time. */
|
|
216
|
-
rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, batch->vertex_count, z,
|
|
257
|
+
rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, batch->vertex_count, layer_z(canvas, z),
|
|
217
258
|
batch->texture,
|
|
218
259
|
rgame_clip_current(&canvas->clips));
|
|
219
260
|
|
|
@@ -46,25 +46,67 @@
|
|
|
46
46
|
* rotates a clip today.
|
|
47
47
|
*
|
|
48
48
|
* ---------------------------------------------------------------------------
|
|
49
|
+
* The layer stack: where a caller's z is measured from
|
|
50
|
+
* ---------------------------------------------------------------------------
|
|
51
|
+
*
|
|
52
|
+
* A `z` handed to a primitive is an *offset*, added to whatever base is on top
|
|
53
|
+
* of the layer stack. The stack starts at 0, so a caller that never pushes one
|
|
54
|
+
* gets exactly the z it passed.
|
|
55
|
+
*
|
|
56
|
+
* What it is for: the scene graph gives every node its own narrow window of z
|
|
57
|
+
* values, so a node's drawing can only ever be ordered against its own other
|
|
58
|
+
* drawing — never against another node's, and never out of the band it is in.
|
|
59
|
+
* Composing that is one addition here, and the alternative is every caller
|
|
60
|
+
* remembering to add the same number to every z it passes.
|
|
61
|
+
*
|
|
62
|
+
* `rgame_canvas_next_slot` is the other half: a small set of counters, one per
|
|
63
|
+
* band, reset at the start of each frame and handing out increasing indices. It
|
|
64
|
+
* is deliberately ignorant of what a band *is* — that vocabulary lives in
|
|
65
|
+
* RGame::Util::Z, a layer up — and does nothing but count, because "which node
|
|
66
|
+
* comes next" is already the order the draw traversal visits nodes in.
|
|
67
|
+
*
|
|
68
|
+
* A push *replaces* rather than adds. Nesting bases would re-introduce the
|
|
69
|
+
* additive relative z this design exists to remove: the point is that a node's
|
|
70
|
+
* window is decided by where the traversal reached it, not by summing what its
|
|
71
|
+
* ancestors happened to pick.
|
|
72
|
+
*
|
|
73
|
+
* ---------------------------------------------------------------------------
|
|
49
74
|
* One pop for any push
|
|
50
75
|
* ---------------------------------------------------------------------------
|
|
51
76
|
*
|
|
52
|
-
* `push_translate`, `push_rotate`, `push_scale` and `
|
|
53
|
-
* by the same `pop`. The canvas remembers which stack each push
|
|
54
|
-
* caller never has to — and cannot pop the wrong one. Pushes that
|
|
55
|
-
* honoured (a full stack) are still counted, so pop stays balanced
|
|
56
|
-
* drawing comes out untransformed rather than desynchronised.
|
|
77
|
+
* `push_translate`, `push_rotate`, `push_scale`, `push_clip` and `push_layer`
|
|
78
|
+
* are all undone by the same `pop`. The canvas remembers which stack each push
|
|
79
|
+
* went to, so a caller never has to — and cannot pop the wrong one. Pushes that
|
|
80
|
+
* could not be honoured (a full stack) are still counted, so pop stays balanced
|
|
81
|
+
* and the drawing comes out untransformed rather than desynchronised.
|
|
57
82
|
*/
|
|
58
83
|
|
|
59
|
-
/* Deep enough
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
/* Deep enough for any sane scene graph, like the two stacks it sits beside. */
|
|
85
|
+
#define RGAME_LAYER_STACK_DEPTH 32
|
|
86
|
+
|
|
87
|
+
/* How many independent slot counters a frame has. RGame::Util::Z uses four;
|
|
88
|
+
* the spare room costs four unsigned ints. */
|
|
89
|
+
#define RGAME_LAYER_BANDS 8
|
|
90
|
+
|
|
91
|
+
/* Deep enough that it cannot fill before all three underlying stacks have;
|
|
92
|
+
* pushes beyond that are counted rather than recorded, so balance always
|
|
93
|
+
* holds. */
|
|
94
|
+
#define RGAME_CANVAS_STACK_DEPTH \
|
|
95
|
+
(RGAME_TRANSFORM_STACK_DEPTH + RGAME_CLIP_STACK_DEPTH + RGAME_LAYER_STACK_DEPTH)
|
|
62
96
|
|
|
63
97
|
typedef struct {
|
|
64
98
|
rgame_transform_stack transforms;
|
|
65
99
|
rgame_clip_stack clips;
|
|
66
100
|
rgame_draw_queue queue;
|
|
67
101
|
|
|
102
|
+
/* entries[0] is 0 — the base an un-layered draw sits on — so `layer_depth`
|
|
103
|
+
* is the number of pushes outstanding and entries[layer_depth] is current. */
|
|
104
|
+
double layers[RGAME_LAYER_STACK_DEPTH];
|
|
105
|
+
int layer_depth;
|
|
106
|
+
|
|
107
|
+
/* Slots handed out per band this frame. Reset by begin_frame. */
|
|
108
|
+
unsigned int slots[RGAME_LAYER_BANDS];
|
|
109
|
+
|
|
68
110
|
/* Which stack each outstanding push went to, so pop can undo the right one. */
|
|
69
111
|
unsigned char pushes[RGAME_CANVAS_STACK_DEPTH];
|
|
70
112
|
int push_depth;
|
|
@@ -79,8 +121,8 @@ void rgame_canvas_init(rgame_canvas *canvas);
|
|
|
79
121
|
void rgame_canvas_destroy(rgame_canvas *canvas);
|
|
80
122
|
|
|
81
123
|
/*
|
|
82
|
-
* Starts a frame: empties the queue (keeping its buffers), resets
|
|
83
|
-
* and sets the clip base to the window. Re-stating the size every frame is also
|
|
124
|
+
* Starts a frame: empties the queue (keeping its buffers), resets all three
|
|
125
|
+
* stacks and the slot counters, and sets the clip base to the window. Re-stating the size every frame is also
|
|
84
126
|
* how a resize takes effect, so there is no separate path to forget.
|
|
85
127
|
*/
|
|
86
128
|
void rgame_canvas_begin_frame(rgame_canvas *canvas, int width, int height);
|
|
@@ -97,6 +139,19 @@ void rgame_canvas_push_scale(rgame_canvas *canvas, float sx, float sy);
|
|
|
97
139
|
* transform before narrowing the clip. */
|
|
98
140
|
void rgame_canvas_push_clip(rgame_canvas *canvas, rgame_rect rect);
|
|
99
141
|
|
|
142
|
+
/* Sets the base every subsequent z is measured from, until the matching pop. */
|
|
143
|
+
void rgame_canvas_push_layer(rgame_canvas *canvas, double base);
|
|
144
|
+
|
|
145
|
+
/* The base currently in effect. */
|
|
146
|
+
double rgame_canvas_layer(const rgame_canvas *canvas);
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* The next slot index in `band`, counting from 0 each frame. Out-of-range bands
|
|
150
|
+
* answer 0 rather than reading past the array — a caller with a bad band gets
|
|
151
|
+
* everything piled in one slot, which is wrong but bounded.
|
|
152
|
+
*/
|
|
153
|
+
unsigned int rgame_canvas_next_slot(rgame_canvas *canvas, int band);
|
|
154
|
+
|
|
100
155
|
void rgame_canvas_pop(rgame_canvas *canvas);
|
|
101
156
|
|
|
102
157
|
/*
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#include "graphics/clip.h"
|
|
2
2
|
|
|
3
|
+
#include <stdint.h>
|
|
4
|
+
|
|
3
5
|
/* The canonical empty rect. Every path that produces "nothing" produces this
|
|
4
6
|
* exact value, so two empties compare equal and cannot split a batch. */
|
|
5
7
|
static const rgame_rect RGAME_EMPTY_RECT = { 0, 0, 0, 0 };
|
|
@@ -30,20 +32,27 @@ rgame_rect rgame_rect_intersect(rgame_rect a, rgame_rect b) {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/*
|
|
33
|
-
* Edges are computed in `
|
|
34
|
-
* pass a very large width to mean "as wide as possible", and
|
|
35
|
-
* then overflow — which is undefined behaviour, not merely a
|
|
36
|
-
* The result always fits back in an int because it is
|
|
37
|
-
* smaller of the two inputs.
|
|
35
|
+
* Edges are computed in `int64_t` rather than `int`. A caller can
|
|
36
|
+
* legitimately pass a very large width to mean "as wide as possible", and
|
|
37
|
+
* x + w would then overflow — which is undefined behaviour, not merely a
|
|
38
|
+
* wrong answer. The result always fits back in an int because it is
|
|
39
|
+
* bounded by the smaller of the two inputs.
|
|
40
|
+
*
|
|
41
|
+
* `int64_t` rather than `long`: `long` is only 32 bits on Windows (LLP64),
|
|
42
|
+
* the same width as `int` there, so it does not actually buy the extra
|
|
43
|
+
* headroom this needs — found by UBSan under clang64, which is the only
|
|
44
|
+
* configuration that caught it (the intermediate overflow this guards
|
|
45
|
+
* against never occurs in Check's own test values on a 64-bit `long`
|
|
46
|
+
* platform, so plain Linux/macOS runs never exercise the bug either).
|
|
38
47
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
int64_t left = a.x > b.x ? a.x : b.x;
|
|
49
|
+
int64_t top = a.y > b.y ? a.y : b.y;
|
|
50
|
+
int64_t a_right = (int64_t)a.x + a.w;
|
|
51
|
+
int64_t b_right = (int64_t)b.x + b.w;
|
|
52
|
+
int64_t a_bottom = (int64_t)a.y + a.h;
|
|
53
|
+
int64_t b_bottom = (int64_t)b.y + b.h;
|
|
54
|
+
int64_t right = a_right < b_right ? a_right : b_right;
|
|
55
|
+
int64_t bottom = a_bottom < b_bottom ? a_bottom : b_bottom;
|
|
47
56
|
|
|
48
57
|
if (right <= left || bottom <= top) {
|
|
49
58
|
return RGAME_EMPTY_RECT;
|