rgame 0.1.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 +7 -0
- data/LICENSE +26 -0
- data/README.md +406 -0
- data/docs/api/README.md +167 -0
- data/docs/api/app.md +192 -0
- data/docs/api/assets.md +426 -0
- data/docs/api/audio.md +208 -0
- data/docs/api/components.md +321 -0
- data/docs/api/drawing.md +330 -0
- data/docs/api/game.md +99 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +179 -0
- data/docs/api/internals.md +110 -0
- data/docs/api/scene_graph.md +159 -0
- data/docs/api/signals.md +142 -0
- data/docs/api/systems.md +98 -0
- data/docs/api/text.md +116 -0
- data/docs/api/toolbox.md +240 -0
- data/docs/api/values.md +101 -0
- data/ext/README.md +225 -0
- data/ext/rgame_core/app/app.c +721 -0
- data/ext/rgame_core/app/app_gl.h +64 -0
- data/ext/rgame_core/app/frame_loop.c +42 -0
- data/ext/rgame_core/app/frame_loop.h +54 -0
- data/ext/rgame_core/audio/audio.c +466 -0
- data/ext/rgame_core/audio/audio_internal.h +45 -0
- data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
- data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
- data/ext/rgame_core/example.rb +188 -0
- data/ext/rgame_core/extconf.rb +167 -0
- data/ext/rgame_core/graphics/backend.c +52 -0
- data/ext/rgame_core/graphics/backend.h +64 -0
- data/ext/rgame_core/graphics/canvas.c +247 -0
- data/ext/rgame_core/graphics/canvas.h +143 -0
- data/ext/rgame_core/graphics/clip.c +87 -0
- data/ext/rgame_core/graphics/clip.h +89 -0
- data/ext/rgame_core/graphics/draw_queue.c +216 -0
- data/ext/rgame_core/graphics/draw_queue.h +174 -0
- data/ext/rgame_core/graphics/gl_backend.c +122 -0
- data/ext/rgame_core/graphics/gl_backend.h +43 -0
- data/ext/rgame_core/graphics/image.c +304 -0
- data/ext/rgame_core/graphics/image_internal.h +30 -0
- data/ext/rgame_core/graphics/primitives.c +189 -0
- data/ext/rgame_core/graphics/primitives.h +111 -0
- data/ext/rgame_core/graphics/recording.c +119 -0
- data/ext/rgame_core/graphics/recording.h +88 -0
- data/ext/rgame_core/graphics/texture.c +181 -0
- data/ext/rgame_core/graphics/texture.h +165 -0
- data/ext/rgame_core/graphics/transform.c +128 -0
- data/ext/rgame_core/graphics/transform.h +106 -0
- data/ext/rgame_core/include/rgame/core.h +577 -0
- data/ext/rgame_core/input/device_slots.c +103 -0
- data/ext/rgame_core/input/device_slots.h +93 -0
- data/ext/rgame_core/input/gamepad.c +145 -0
- data/ext/rgame_core/input/gamepad.h +63 -0
- data/ext/rgame_core/input/input.c +109 -0
- data/ext/rgame_core/input/input.h +99 -0
- data/ext/rgame_core/ruby/audio_ext.c +321 -0
- data/ext/rgame_core/ruby/core_ext.c +513 -0
- data/ext/rgame_core/ruby/core_ext.h +51 -0
- data/ext/rgame_core/ruby/font_ext.c +168 -0
- data/ext/rgame_core/ruby/image_ext.c +230 -0
- data/ext/rgame_core/ruby/recording_ext.c +186 -0
- data/ext/rgame_core/ruby/renderer_ext.c +376 -0
- data/ext/rgame_core/text/atlas.c +59 -0
- data/ext/rgame_core/text/atlas.h +85 -0
- data/ext/rgame_core/text/font.c +281 -0
- data/ext/rgame_core/text/font.h +139 -0
- data/ext/rgame_core/text/font_atlas.c +385 -0
- data/ext/rgame_core/text/font_internal.h +47 -0
- data/ext/rgame_core/text/glyph_cache.c +142 -0
- data/ext/rgame_core/text/glyph_cache.h +89 -0
- data/ext/rgame_core/vendor/README.md +159 -0
- data/ext/rgame_core/vendor/miniaudio.h +95864 -0
- data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
- data/ext/rgame_core/vendor/stb_image.h +7988 -0
- data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
- data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
- data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
- data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
- data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
- data/ext/rgame_util/color.c +19 -0
- data/ext/rgame_util/color.h +60 -0
- data/ext/rgame_util/color_ext.c +156 -0
- data/ext/rgame_util/extconf.rb +27 -0
- data/ext/rgame_util/tensor.c +186 -0
- data/ext/rgame_util/util_ext.c +27 -0
- data/ext/rgame_util/util_ext.h +16 -0
- data/lib/rgame/boot.rb +13 -0
- data/lib/rgame/core/app.rb +82 -0
- data/lib/rgame/core/asset_manager.rb +224 -0
- data/lib/rgame/core/audio.rb +124 -0
- data/lib/rgame/core/font.rb +49 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +77 -0
- data/lib/rgame/core/nine_slice.rb +163 -0
- data/lib/rgame/core/recording.rb +52 -0
- data/lib/rgame/core/renderer.rb +363 -0
- data/lib/rgame/core/sprite_sheet.rb +108 -0
- data/lib/rgame/core/tile_map_renderer.rb +160 -0
- data/lib/rgame/core/ui_atlas.rb +86 -0
- data/lib/rgame/core.rb +24 -0
- data/lib/rgame/engine/actor.rb +53 -0
- data/lib/rgame/engine/animation_set.rb +49 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +29 -0
- data/lib/rgame/engine/body.rb +49 -0
- data/lib/rgame/engine/cached_label.rb +33 -0
- data/lib/rgame/engine/camera.rb +33 -0
- data/lib/rgame/engine/camera_view.rb +28 -0
- data/lib/rgame/engine/circle_collider.rb +32 -0
- data/lib/rgame/engine/collision_box.rb +34 -0
- data/lib/rgame/engine/collision_system.rb +44 -0
- data/lib/rgame/engine/component.rb +30 -0
- data/lib/rgame/engine/components/action_trigger.rb +41 -0
- data/lib/rgame/engine/components/animated_sprite.rb +63 -0
- data/lib/rgame/engine/components/character_body.rb +70 -0
- data/lib/rgame/engine/components/circle_collider.rb +44 -0
- data/lib/rgame/engine/components/collision_world.rb +103 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
- data/lib/rgame/engine/components/path_follow.rb +84 -0
- data/lib/rgame/engine/components/player_controller.rb +24 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +27 -0
- data/lib/rgame/engine/components/sprite.rb +31 -0
- data/lib/rgame/engine/components/targeting.rb +54 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +68 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +27 -0
- data/lib/rgame/engine/components/wander_controller.rb +60 -0
- data/lib/rgame/engine/debug_overlay.rb +106 -0
- data/lib/rgame/engine/i18n.rb +97 -0
- data/lib/rgame/engine/input/action_mapper.rb +46 -0
- data/lib/rgame/engine/input/actions.rb +41 -0
- data/lib/rgame/engine/input/player_controller.rb +14 -0
- data/lib/rgame/engine/matrix.rb +32 -0
- data/lib/rgame/engine/node2d.rb +271 -0
- data/lib/rgame/engine/path.rb +78 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/resettable.rb +67 -0
- data/lib/rgame/engine/scene/scene_stack.rb +65 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +71 -0
- data/lib/rgame/engine/tile_collision.rb +78 -0
- data/lib/rgame/engine/tile_map.rb +149 -0
- data/lib/rgame/engine/tileset.rb +101 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine.rb +68 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +129 -0
- data/lib/rgame/util/color.rb +27 -0
- data/lib/rgame/util/controls.rb +107 -0
- data/lib/rgame/util/tensor.rb +12 -0
- data/lib/rgame/util.rb +8 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +20 -0
- metadata +215 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* font_ext.c — the Ruby binding for RGame::Core::Font.
|
|
3
|
+
*
|
|
4
|
+
* font = RGame::Core::Font.new(app, 18) # the shipped font
|
|
5
|
+
* font = RGame::Core::Font.new(app, 18, path: 'assets/x.ttf')
|
|
6
|
+
* font.height # => 18
|
|
7
|
+
* font.text_width('Score: 1200')
|
|
8
|
+
*
|
|
9
|
+
* The atlas, the cache and the metrics are in font_atlas.c, font.c, atlas.c and
|
|
10
|
+
* glyph_cache.c — three of those four are pure and Check-tested. This file is
|
|
11
|
+
* the wrapper: argument checking, turning a NULL return into an exception that
|
|
12
|
+
* says why, and keeping the app reachable for as long as the font is.
|
|
13
|
+
*
|
|
14
|
+
* The default path is *not* here. It is a packaging question — where a gem
|
|
15
|
+
* installs its data — so lib/rgame/core/font.rb owns it and passes a path down.
|
|
16
|
+
*
|
|
17
|
+
* The C layer has no opinion about where fonts live and there is **no
|
|
18
|
+
* font-name lookup at all**: a font is a file. Asking the operating system for
|
|
19
|
+
* one by name would mean a font-database backend per platform — fontconfig on
|
|
20
|
+
* Linux, CoreText on macOS, GDI on Windows — and a system dependency this
|
|
21
|
+
* engine otherwise does not have. It would also mean text renders differently
|
|
22
|
+
* on different machines, which is a layout bug nobody can reproduce. See
|
|
23
|
+
* vendor/README.md.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
#include "ruby/core_ext.h"
|
|
27
|
+
|
|
28
|
+
#include "text/font_internal.h"
|
|
29
|
+
#include "rgame/core.h"
|
|
30
|
+
|
|
31
|
+
typedef struct {
|
|
32
|
+
rgame_font *font;
|
|
33
|
+
VALUE app_object; /* marked: the atlas pages live in this window's context */
|
|
34
|
+
} rgame_font_ref;
|
|
35
|
+
|
|
36
|
+
static void font_ref_mark(void *ptr) {
|
|
37
|
+
rgame_font_ref *ref = ptr;
|
|
38
|
+
rb_gc_mark(ref->app_object);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static void font_ref_free(void *ptr) {
|
|
42
|
+
rgame_font_ref *ref = ptr;
|
|
43
|
+
rgame_font_destroy(ref->font); /* NULL-safe */
|
|
44
|
+
xfree(ref);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static size_t font_ref_size(const void *ptr) {
|
|
48
|
+
(void)ptr;
|
|
49
|
+
return sizeof(rgame_font_ref);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static const rb_data_type_t font_data_type = {
|
|
53
|
+
.wrap_struct_name = "rgame_font",
|
|
54
|
+
.function = {
|
|
55
|
+
.dmark = font_ref_mark,
|
|
56
|
+
.dfree = font_ref_free,
|
|
57
|
+
.dsize = font_ref_size,
|
|
58
|
+
},
|
|
59
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
static VALUE cFont;
|
|
63
|
+
|
|
64
|
+
static rgame_font_ref *font_unwrap(VALUE self) {
|
|
65
|
+
rgame_font_ref *ref;
|
|
66
|
+
TypedData_Get_Struct(self, rgame_font_ref, &font_data_type, ref);
|
|
67
|
+
if (!ref->font) {
|
|
68
|
+
rb_raise(rb_eRuntimeError, "font is not initialized");
|
|
69
|
+
}
|
|
70
|
+
return ref;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
rgame_font *rgame_font_unwrap(VALUE font) {
|
|
74
|
+
return font_unwrap(font)->font;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static VALUE font_alloc(VALUE klass) {
|
|
78
|
+
rgame_font_ref *ref;
|
|
79
|
+
VALUE object = TypedData_Make_Struct(klass, rgame_font_ref, &font_data_type, ref);
|
|
80
|
+
ref->font = NULL;
|
|
81
|
+
ref->app_object = Qnil;
|
|
82
|
+
return object;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Font.new(app, pixel_height, path) — the path is supplied by the Ruby half,
|
|
86
|
+
* which defaults it to the shipped font. */
|
|
87
|
+
static VALUE font_initialize(VALUE self, VALUE app, VALUE pixel_height, VALUE path) {
|
|
88
|
+
rgame_font_ref *ref;
|
|
89
|
+
TypedData_Get_Struct(self, rgame_font_ref, &font_data_type, ref);
|
|
90
|
+
|
|
91
|
+
rgame_app *engine_app = rgame_app_unwrap(app); /* raises TypeError otherwise */
|
|
92
|
+
const char *path_str = StringValueCStr(path);
|
|
93
|
+
int height = NUM2INT(pixel_height);
|
|
94
|
+
|
|
95
|
+
char error[256] = {0};
|
|
96
|
+
rgame_font *font = rgame_font_load(engine_app, path_str, height, error, sizeof(error));
|
|
97
|
+
if (!font) {
|
|
98
|
+
rb_raise(rb_const_get(cFont, rb_intern("LoadError")), "%s", error);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ref->font = font;
|
|
102
|
+
ref->app_object = app;
|
|
103
|
+
return self;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* The size it was loaded at, and the amount to step by for a second line. */
|
|
107
|
+
static VALUE font_height(VALUE self) {
|
|
108
|
+
return INT2NUM(rgame_font_height(font_unwrap(self)->font));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* #text_width(string) — what #text would occupy, in pixels.
|
|
113
|
+
*
|
|
114
|
+
* Deliberately usable outside `draw`: measuring touches no GL, and laying out a
|
|
115
|
+
* menu happens while updating, not while drawing.
|
|
116
|
+
*/
|
|
117
|
+
static VALUE font_text_width(VALUE self, VALUE string) {
|
|
118
|
+
rgame_font_ref *ref = font_unwrap(self);
|
|
119
|
+
|
|
120
|
+
/* Raises TypeError rather than letting RSTRING_PTR read a non-String's
|
|
121
|
+
* innards as a char*, which segfaults. See renderer_ext.c's #draw_text. */
|
|
122
|
+
StringValue(string);
|
|
123
|
+
|
|
124
|
+
/* The bytes as Ruby holds them. No copy, no per-call allocation, and the
|
|
125
|
+
* codepoint walk happens in C where it costs nothing. */
|
|
126
|
+
const char *text = RSTRING_PTR(string);
|
|
127
|
+
long length = RSTRING_LEN(string);
|
|
128
|
+
double width = rgame_font_measure(ref->font, text, (size_t)length);
|
|
129
|
+
|
|
130
|
+
/* Keeps `string` alive across the call above, which matters because
|
|
131
|
+
* RSTRING_PTR hands out a pointer the GC does not know we are holding. */
|
|
132
|
+
RB_GC_GUARD(string);
|
|
133
|
+
return DBL2NUM(width);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* How many atlas pages exist across every live font. Test-only, and named so:
|
|
137
|
+
* a leaked page is invisible — nothing looks wrong until video memory runs
|
|
138
|
+
* out. The image side has the same counter for the same reason. */
|
|
139
|
+
static VALUE font_s_debug_live_pages(VALUE klass) {
|
|
140
|
+
(void)klass;
|
|
141
|
+
return LONG2NUM(rgame_font_live_pages());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static VALUE font_inspect(VALUE self) {
|
|
145
|
+
rgame_font_ref *ref;
|
|
146
|
+
TypedData_Get_Struct(self, rgame_font_ref, &font_data_type, ref);
|
|
147
|
+
if (!ref->font) {
|
|
148
|
+
return rb_sprintf("#<%" PRIsVALUE " (uninitialized)>", rb_obj_class(self));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return rb_sprintf("#<%" PRIsVALUE " %dpx>", rb_obj_class(self),
|
|
152
|
+
rgame_font_height(ref->font));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
void rgame_init_font(VALUE mCore) {
|
|
156
|
+
cFont = rb_define_class_under(mCore, "Font", rb_cObject);
|
|
157
|
+
|
|
158
|
+
/* Raised when a file cannot be read or is not a TrueType font — an everyday
|
|
159
|
+
* condition, so it gets a name a game can rescue. */
|
|
160
|
+
rb_define_class_under(cFont, "LoadError", rb_eStandardError);
|
|
161
|
+
|
|
162
|
+
rb_define_alloc_func(cFont, font_alloc);
|
|
163
|
+
rb_define_method(cFont, "initialize", font_initialize, 3);
|
|
164
|
+
rb_define_method(cFont, "height", font_height, 0);
|
|
165
|
+
rb_define_method(cFont, "text_width", font_text_width, 1);
|
|
166
|
+
rb_define_method(cFont, "inspect", font_inspect, 0);
|
|
167
|
+
rb_define_singleton_method(cFont, "debug_live_pages", font_s_debug_live_pages, 0);
|
|
168
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* image_ext.c — the Ruby binding for RGame::Core::Image.
|
|
3
|
+
*
|
|
4
|
+
* The decode and upload are in image.c; the rectangle arithmetic behind
|
|
5
|
+
* subimages and tiles is in texture.c and is Check-tested with no GPU. This
|
|
6
|
+
* file is only the wrapper: argument checking, turning C's "returned NULL"
|
|
7
|
+
* into a Ruby exception that says why, and keeping the app object reachable
|
|
8
|
+
* for as long as any image made from it is.
|
|
9
|
+
*
|
|
10
|
+
* img = RGame::Core::Image.new(app, 'hero.png')
|
|
11
|
+
* img.width; img.height
|
|
12
|
+
* sub = img.subimage(0, 0, 16, 16)
|
|
13
|
+
* tiles = RGame::Core::Image.load_tiles(app, 'sheet.png', 16, 16)
|
|
14
|
+
*
|
|
15
|
+
* Slicing never re-decodes: every image above shares one GPU texture, and the
|
|
16
|
+
* texture goes when the last of them does.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
#include "ruby/core_ext.h"
|
|
20
|
+
|
|
21
|
+
#include "rgame/core.h"
|
|
22
|
+
#include "graphics/texture.h"
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* The Ruby object's payload. Two members, and the second is the interesting
|
|
26
|
+
* one: `app` is the Ruby App object this image was loaded from, marked so the
|
|
27
|
+
* collector treats the app as reachable from every image.
|
|
28
|
+
*
|
|
29
|
+
* That is a deliberate ownership statement, not just GC hygiene. An image is
|
|
30
|
+
* meaningless without the context its pixels live in, and a game that keeps a
|
|
31
|
+
* sprite around has, by definition, not finished with the window. (The C layer
|
|
32
|
+
* survives the other order too — see rgame_app_gl_retain — but a Ruby user
|
|
33
|
+
* should never be in a position to observe it.)
|
|
34
|
+
*/
|
|
35
|
+
typedef struct {
|
|
36
|
+
rgame_image *image;
|
|
37
|
+
VALUE app;
|
|
38
|
+
} rgame_image_ref;
|
|
39
|
+
|
|
40
|
+
static void image_ref_mark(void *ptr) {
|
|
41
|
+
rgame_image_ref *ref = ptr;
|
|
42
|
+
rb_gc_mark(ref->app);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static void image_ref_free(void *ptr) {
|
|
46
|
+
rgame_image_ref *ref = ptr;
|
|
47
|
+
rgame_image_destroy(ref->image); /* NULL-safe */
|
|
48
|
+
xfree(ref);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static size_t image_ref_size(const void *ptr) {
|
|
52
|
+
(void)ptr;
|
|
53
|
+
return sizeof(rgame_image_ref);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static const rb_data_type_t image_data_type = {
|
|
57
|
+
.wrap_struct_name = "rgame_image",
|
|
58
|
+
.function = {
|
|
59
|
+
.dmark = image_ref_mark,
|
|
60
|
+
.dfree = image_ref_free,
|
|
61
|
+
.dsize = image_ref_size,
|
|
62
|
+
},
|
|
63
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
static VALUE cImage;
|
|
67
|
+
|
|
68
|
+
static rgame_image_ref *image_ref_unwrap(VALUE self) {
|
|
69
|
+
rgame_image_ref *ref;
|
|
70
|
+
TypedData_Get_Struct(self, rgame_image_ref, &image_data_type, ref);
|
|
71
|
+
if (!ref->image) {
|
|
72
|
+
rb_raise(rb_eRuntimeError, "image is not initialized");
|
|
73
|
+
}
|
|
74
|
+
return ref;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* The accessor other files in this extension use (see core_ext.h) — the
|
|
78
|
+
* renderer needs the C handle to draw. TypedData_Get_Struct raises TypeError on
|
|
79
|
+
* anything that is not an Image, so the type check comes for free. */
|
|
80
|
+
rgame_image *rgame_image_unwrap(VALUE image) {
|
|
81
|
+
return image_ref_unwrap(image)->image;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* Wraps a C handle in a fresh Ruby object. Takes ownership: if allocating the
|
|
86
|
+
* Ruby side raises, the handle would leak, so the payload is filled in before
|
|
87
|
+
* anything else can fail.
|
|
88
|
+
*/
|
|
89
|
+
static VALUE image_wrap(rgame_image *image, VALUE app) {
|
|
90
|
+
rgame_image_ref *ref;
|
|
91
|
+
VALUE object = TypedData_Make_Struct(cImage, rgame_image_ref, &image_data_type, ref);
|
|
92
|
+
ref->image = image;
|
|
93
|
+
ref->app = app;
|
|
94
|
+
return object;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* Image.new(app, path)
|
|
99
|
+
*
|
|
100
|
+
* `app` comes first and is required, rather than being implied by "the window
|
|
101
|
+
* that happens to be open". Textures belong to one GL context, so an image
|
|
102
|
+
* genuinely is an image *of* an app — and saying so is what lets the object
|
|
103
|
+
* hold the app reachable, and what makes two windows work at all.
|
|
104
|
+
*/
|
|
105
|
+
static VALUE image_initialize(VALUE self, VALUE app, VALUE path) {
|
|
106
|
+
rgame_image_ref *ref;
|
|
107
|
+
TypedData_Get_Struct(self, rgame_image_ref, &image_data_type, ref);
|
|
108
|
+
|
|
109
|
+
/* Unwrapping raises TypeError on anything that is not an App, so there is
|
|
110
|
+
* no separate type check to keep in step with this one. */
|
|
111
|
+
rgame_app *engine_app = rgame_app_unwrap(app);
|
|
112
|
+
const char *path_str = StringValueCStr(path);
|
|
113
|
+
|
|
114
|
+
char error[256] = {0};
|
|
115
|
+
rgame_image *image = rgame_image_load(engine_app, path_str, error, sizeof(error));
|
|
116
|
+
if (!image) {
|
|
117
|
+
rb_raise(rb_const_get(cImage, rb_intern("LoadError")), "%s", error);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ref->image = image;
|
|
121
|
+
ref->app = app;
|
|
122
|
+
return self;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static VALUE image_alloc(VALUE klass) {
|
|
126
|
+
rgame_image_ref *ref;
|
|
127
|
+
VALUE object = TypedData_Make_Struct(klass, rgame_image_ref, &image_data_type, ref);
|
|
128
|
+
ref->image = NULL;
|
|
129
|
+
ref->app = Qnil;
|
|
130
|
+
return object;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static VALUE image_width(VALUE self) {
|
|
134
|
+
return INT2NUM(rgame_image_width(image_ref_unwrap(self)->image));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static VALUE image_height(VALUE self) {
|
|
138
|
+
return INT2NUM(rgame_image_height(image_ref_unwrap(self)->image));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
* #subimage(x, y, width, height)
|
|
143
|
+
*
|
|
144
|
+
* Raises rather than returning nil on a rect that does not fit. A nil here
|
|
145
|
+
* would travel a long way — into an asset table, out of it three scenes
|
|
146
|
+
* later — before failing as a NoMethodError with nothing left pointing at the
|
|
147
|
+
* bad coordinates.
|
|
148
|
+
*/
|
|
149
|
+
static VALUE image_subimage(VALUE self, VALUE x, VALUE y, VALUE width, VALUE height) {
|
|
150
|
+
rgame_image_ref *ref = image_ref_unwrap(self);
|
|
151
|
+
|
|
152
|
+
rgame_image *sub = rgame_image_subimage(ref->image, NUM2INT(x), NUM2INT(y), NUM2INT(width),
|
|
153
|
+
NUM2INT(height));
|
|
154
|
+
if (!sub) {
|
|
155
|
+
rb_raise(rb_eArgError,
|
|
156
|
+
"subimage %" PRIsVALUE ",%" PRIsVALUE " %" PRIsVALUE "x%" PRIsVALUE
|
|
157
|
+
" does not fit in a %dx%d image",
|
|
158
|
+
x, y, width, height, rgame_image_width(ref->image),
|
|
159
|
+
rgame_image_height(ref->image));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return image_wrap(sub, ref->app);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* #tile_count(tile_width, tile_height) — whole tiles only. */
|
|
166
|
+
static VALUE image_tile_count(VALUE self, VALUE tile_width, VALUE tile_height) {
|
|
167
|
+
return INT2NUM(rgame_image_tile_count(image_ref_unwrap(self)->image, NUM2INT(tile_width),
|
|
168
|
+
NUM2INT(tile_height)));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* #tile(tile_width, tile_height, index) — row-major, left to right then down. */
|
|
172
|
+
static VALUE image_tile(VALUE self, VALUE tile_width, VALUE tile_height, VALUE index) {
|
|
173
|
+
rgame_image_ref *ref = image_ref_unwrap(self);
|
|
174
|
+
|
|
175
|
+
rgame_image *tile = rgame_image_tile(ref->image, NUM2INT(tile_width), NUM2INT(tile_height),
|
|
176
|
+
NUM2INT(index));
|
|
177
|
+
if (!tile) {
|
|
178
|
+
rb_raise(rb_eIndexError, "tile %" PRIsVALUE " is out of range (%d tiles of %" PRIsVALUE
|
|
179
|
+
"x%" PRIsVALUE ")",
|
|
180
|
+
index,
|
|
181
|
+
rgame_image_tile_count(ref->image, NUM2INT(tile_width), NUM2INT(tile_height)),
|
|
182
|
+
tile_width, tile_height);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return image_wrap(tile, ref->app);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static VALUE image_inspect(VALUE self) {
|
|
189
|
+
rgame_image_ref *ref;
|
|
190
|
+
TypedData_Get_Struct(self, rgame_image_ref, &image_data_type, ref);
|
|
191
|
+
if (!ref->image) {
|
|
192
|
+
return rb_sprintf("#<%" PRIsVALUE " (uninitialized)>", rb_obj_class(self));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return rb_sprintf("#<%" PRIsVALUE " %dx%d>", rb_obj_class(self),
|
|
196
|
+
rgame_image_width(ref->image), rgame_image_height(ref->image));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
* Image.debug_live_textures — how many GPU textures the engine is holding.
|
|
201
|
+
*
|
|
202
|
+
* Test-only, and named so. A leaked texture is silent: nothing is slower and
|
|
203
|
+
* nothing looks wrong until video memory runs out an hour into play. This is
|
|
204
|
+
* what lets a spec assert that dropping a sheet and all its tiles actually
|
|
205
|
+
* releases the upload — including in the case that only a garbage collector
|
|
206
|
+
* can produce, where an app and its images are swept in an arbitrary order.
|
|
207
|
+
*/
|
|
208
|
+
static VALUE image_s_debug_live_textures(VALUE klass) {
|
|
209
|
+
(void)klass;
|
|
210
|
+
return LONG2NUM(rgame_texture_live_sheets());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
void rgame_init_image(VALUE mCore) {
|
|
214
|
+
cImage = rb_define_class_under(mCore, "Image", rb_cObject);
|
|
215
|
+
|
|
216
|
+
/* Raised when a file cannot be read or decoded — an everyday condition
|
|
217
|
+
* (a typo'd path, a truncated asset), so it gets a name a game can rescue
|
|
218
|
+
* rather than a bare RuntimeError. */
|
|
219
|
+
rb_define_class_under(cImage, "LoadError", rb_eStandardError);
|
|
220
|
+
|
|
221
|
+
rb_define_alloc_func(cImage, image_alloc);
|
|
222
|
+
rb_define_method(cImage, "initialize", image_initialize, 2);
|
|
223
|
+
rb_define_method(cImage, "width", image_width, 0);
|
|
224
|
+
rb_define_method(cImage, "height", image_height, 0);
|
|
225
|
+
rb_define_method(cImage, "subimage", image_subimage, 4);
|
|
226
|
+
rb_define_method(cImage, "tile_count", image_tile_count, 2);
|
|
227
|
+
rb_define_method(cImage, "tile", image_tile, 3);
|
|
228
|
+
rb_define_method(cImage, "inspect", image_inspect, 0);
|
|
229
|
+
rb_define_singleton_method(cImage, "debug_live_textures", image_s_debug_live_textures, 0);
|
|
230
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* recording_ext.c — the Ruby binding for RGame::Core::Recording.
|
|
3
|
+
*
|
|
4
|
+
* A recording is made by `RGame::Core::Renderer#record`, never by `new`:
|
|
5
|
+
*
|
|
6
|
+
* ground = renderer.record { tiles.each { |t| renderer.image(t.image, t.x, t.y) } }
|
|
7
|
+
* ground.draw(-camera.x, -camera.y)
|
|
8
|
+
*
|
|
9
|
+
* The baking and the replay are in recording.c and canvas.c, both pure. What
|
|
10
|
+
* this file adds is the two things only Ruby can do: hold the images the
|
|
11
|
+
* recording drew, so their GPU textures cannot be freed while a recording still
|
|
12
|
+
* references them, and refuse a replay outside a frame.
|
|
13
|
+
*
|
|
14
|
+
* ---------------------------------------------------------------------------
|
|
15
|
+
* Why the images have to be held
|
|
16
|
+
* ---------------------------------------------------------------------------
|
|
17
|
+
*
|
|
18
|
+
* A baked batch stores a GL texture *name* — a number. If the Image it came
|
|
19
|
+
* from is collected, its texture is deleted and that number now refers to
|
|
20
|
+
* nothing; replaying would draw whatever the driver put there next, or nothing
|
|
21
|
+
* at all, with no error. So the renderer collects the images drawn during a
|
|
22
|
+
* recording, and the recording marks them for as long as it lives. Holding a
|
|
23
|
+
* texture alive while something still draws it is the same rule as everywhere
|
|
24
|
+
* else here; it just has to be spelled out at this layer, because the C side
|
|
25
|
+
* only ever saw a number.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
#include "ruby/core_ext.h"
|
|
29
|
+
|
|
30
|
+
#include "graphics/recording.h"
|
|
31
|
+
#include "rgame/core.h"
|
|
32
|
+
|
|
33
|
+
typedef struct {
|
|
34
|
+
rgame_recording *recording;
|
|
35
|
+
rgame_app *app;
|
|
36
|
+
VALUE app_object; /* marked: the textures live in this window's context */
|
|
37
|
+
VALUE images; /* marked: an Array of every Image baked into this */
|
|
38
|
+
} rgame_recording_ref;
|
|
39
|
+
|
|
40
|
+
static void recording_mark(void *ptr) {
|
|
41
|
+
rgame_recording_ref *ref = ptr;
|
|
42
|
+
rb_gc_mark(ref->app_object);
|
|
43
|
+
rb_gc_mark(ref->images);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static void recording_free(void *ptr) {
|
|
47
|
+
rgame_recording_ref *ref = ptr;
|
|
48
|
+
rgame_recording_free(ref->recording); /* NULL-safe */
|
|
49
|
+
xfree(ref);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static size_t recording_size(const void *ptr) {
|
|
53
|
+
(void)ptr;
|
|
54
|
+
return sizeof(rgame_recording_ref);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static const rb_data_type_t recording_data_type = {
|
|
58
|
+
.wrap_struct_name = "rgame_recording",
|
|
59
|
+
.function = {
|
|
60
|
+
.dmark = recording_mark,
|
|
61
|
+
.dfree = recording_free,
|
|
62
|
+
.dsize = recording_size,
|
|
63
|
+
},
|
|
64
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
static VALUE cRecording;
|
|
68
|
+
|
|
69
|
+
static rgame_recording_ref *recording_unwrap(VALUE self) {
|
|
70
|
+
rgame_recording_ref *ref;
|
|
71
|
+
TypedData_Get_Struct(self, rgame_recording_ref, &recording_data_type, ref);
|
|
72
|
+
if (!ref->recording) {
|
|
73
|
+
rb_raise(rb_eRuntimeError, "recording is not initialized");
|
|
74
|
+
}
|
|
75
|
+
return ref;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* Declared even though `new` is refused below: wrapping a TypedData payload in
|
|
80
|
+
* a class that still has Object's allocator makes Ruby undefine it for us and
|
|
81
|
+
* warn about it. Saying so up front is one line and keeps the load quiet.
|
|
82
|
+
*/
|
|
83
|
+
static VALUE recording_alloc(VALUE klass) {
|
|
84
|
+
rgame_recording_ref *ref;
|
|
85
|
+
VALUE object = TypedData_Make_Struct(klass, rgame_recording_ref, &recording_data_type, ref);
|
|
86
|
+
ref->recording = NULL;
|
|
87
|
+
ref->app = NULL;
|
|
88
|
+
ref->app_object = Qnil;
|
|
89
|
+
ref->images = Qnil;
|
|
90
|
+
return object;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
VALUE rgame_recording_wrap(rgame_recording *recording, VALUE app_object, rgame_app *app,
|
|
94
|
+
VALUE images) {
|
|
95
|
+
rgame_recording_ref *ref;
|
|
96
|
+
VALUE object = TypedData_Make_Struct(cRecording, rgame_recording_ref, &recording_data_type,
|
|
97
|
+
ref);
|
|
98
|
+
ref->recording = recording;
|
|
99
|
+
ref->app = app;
|
|
100
|
+
ref->app_object = app_object;
|
|
101
|
+
ref->images = images;
|
|
102
|
+
return object;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* #draw_at(x, y, z, rgba) — the raw form; RGame::Core::Recording#draw in
|
|
107
|
+
* lib/rgame/core/recording.rb is the one with keywords and colour coercion.
|
|
108
|
+
*/
|
|
109
|
+
static VALUE recording_draw_at(VALUE self, VALUE x, VALUE y, VALUE z, VALUE color) {
|
|
110
|
+
rgame_recording_ref *ref = recording_unwrap(self);
|
|
111
|
+
|
|
112
|
+
/* The app that owns these textures has to be the one mid-frame. Anything
|
|
113
|
+
* else would append to a canvas nobody is about to submit, and draw
|
|
114
|
+
* nothing at all. */
|
|
115
|
+
if (!rgame_app_is_drawing(ref->app)) {
|
|
116
|
+
rb_raise(rb_eRuntimeError,
|
|
117
|
+
"drawing is only allowed inside #draw (the frame is not open)");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
rgame_app_draw_recording(ref->app, ref->recording, (float)NUM2DBL(x), (float)NUM2DBL(y),
|
|
121
|
+
(unsigned int)(NUM2ULONG(color) & 0xFFFFFFFFul), NUM2DBL(z));
|
|
122
|
+
return self;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* How many GL calls a replay costs — one per texture. Mostly for tests and for
|
|
126
|
+
* seeing whether a bake did what was hoped. */
|
|
127
|
+
static VALUE recording_batch_count(VALUE self) {
|
|
128
|
+
return UINT2NUM(rgame_recording_batch_count(recording_unwrap(self)->recording));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static VALUE recording_vertex_count(VALUE self) {
|
|
132
|
+
return UINT2NUM(rgame_recording_vertex_count(recording_unwrap(self)->recording));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static VALUE recording_empty_p(VALUE self) {
|
|
136
|
+
return rgame_recording_vertex_count(recording_unwrap(self)->recording) == 0 ? Qtrue : Qfalse;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* The size of what was baked, so a game can skip replaying an off-screen
|
|
140
|
+
* layer. Measured in the recording's own coordinates. */
|
|
141
|
+
static VALUE recording_width(VALUE self) {
|
|
142
|
+
float min_x = 0.0f, max_x = 0.0f;
|
|
143
|
+
rgame_recording_bounds(recording_unwrap(self)->recording, &min_x, NULL, &max_x, NULL);
|
|
144
|
+
return DBL2NUM((double)(max_x - min_x));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static VALUE recording_height(VALUE self) {
|
|
148
|
+
float min_y = 0.0f, max_y = 0.0f;
|
|
149
|
+
rgame_recording_bounds(recording_unwrap(self)->recording, NULL, &min_y, NULL, &max_y);
|
|
150
|
+
return DBL2NUM((double)(max_y - min_y));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static VALUE recording_inspect(VALUE self) {
|
|
154
|
+
rgame_recording_ref *ref;
|
|
155
|
+
TypedData_Get_Struct(self, rgame_recording_ref, &recording_data_type, ref);
|
|
156
|
+
if (!ref->recording) {
|
|
157
|
+
return rb_sprintf("#<%" PRIsVALUE " (uninitialized)>", rb_obj_class(self));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return rb_sprintf("#<%" PRIsVALUE " %u batches, %u vertices>", rb_obj_class(self),
|
|
161
|
+
rgame_recording_batch_count(ref->recording),
|
|
162
|
+
rgame_recording_vertex_count(ref->recording));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Recordings come out of Renderer#record, so `new` is not part of the API —
|
|
166
|
+
* there is nothing a caller could pass that would make a valid one. */
|
|
167
|
+
static VALUE recording_s_new(int argc, VALUE *argv, VALUE klass) {
|
|
168
|
+
(void)argc;
|
|
169
|
+
(void)argv;
|
|
170
|
+
rb_raise(rb_eNoMethodError, "%" PRIsVALUE " is created by Renderer#record, not by .new",
|
|
171
|
+
klass);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
void rgame_init_recording(VALUE mCore) {
|
|
175
|
+
cRecording = rb_define_class_under(mCore, "Recording", rb_cObject);
|
|
176
|
+
|
|
177
|
+
rb_define_alloc_func(cRecording, recording_alloc);
|
|
178
|
+
rb_define_singleton_method(cRecording, "new", recording_s_new, -1);
|
|
179
|
+
rb_define_method(cRecording, "draw_at", recording_draw_at, 4);
|
|
180
|
+
rb_define_method(cRecording, "batch_count", recording_batch_count, 0);
|
|
181
|
+
rb_define_method(cRecording, "vertex_count", recording_vertex_count, 0);
|
|
182
|
+
rb_define_method(cRecording, "empty?", recording_empty_p, 0);
|
|
183
|
+
rb_define_method(cRecording, "width", recording_width, 0);
|
|
184
|
+
rb_define_method(cRecording, "height", recording_height, 0);
|
|
185
|
+
rb_define_method(cRecording, "inspect", recording_inspect, 0);
|
|
186
|
+
}
|