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,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* recording.c — copying a prepared frame out of the queue and keeping it.
|
|
3
|
+
* See recording.h for what a recording is for and what it does not capture.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
#include "graphics/recording.h"
|
|
7
|
+
|
|
8
|
+
#include <stdlib.h>
|
|
9
|
+
#include <string.h>
|
|
10
|
+
|
|
11
|
+
void rgame_recording_destroy(rgame_recording *recording) {
|
|
12
|
+
if (!recording) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
free(recording->vertices);
|
|
17
|
+
free(recording->batches);
|
|
18
|
+
/* Zeroed rather than merely freed, so a second destroy is a no-op and a
|
|
19
|
+
* replay after destroy draws nothing instead of reading freed memory. */
|
|
20
|
+
recording->vertices = NULL;
|
|
21
|
+
recording->batches = NULL;
|
|
22
|
+
recording->vertex_count = 0;
|
|
23
|
+
recording->batch_count = 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
int rgame_recording_capture(rgame_recording *out, const rgame_draw_queue *queue) {
|
|
27
|
+
if (!out || !queue) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
out->vertices = NULL;
|
|
32
|
+
out->batches = NULL;
|
|
33
|
+
out->vertex_count = 0;
|
|
34
|
+
out->batch_count = 0;
|
|
35
|
+
|
|
36
|
+
unsigned int batch_count = rgame_draw_queue_batch_count(queue);
|
|
37
|
+
unsigned int vertex_count = rgame_draw_queue_vertex_count(queue);
|
|
38
|
+
if (batch_count == 0 || vertex_count == 0) {
|
|
39
|
+
return 1; /* nothing was drawn; an empty recording is a valid one */
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Exact allocations, not the queue's growth strategy: a recording is
|
|
43
|
+
* written once and read for the rest of the level, so the spare capacity
|
|
44
|
+
* that makes a per-frame queue cheap would just be a level-long leak. */
|
|
45
|
+
rgame_vertex *vertices = malloc(sizeof(rgame_vertex) * vertex_count);
|
|
46
|
+
rgame_recording_batch *batches = malloc(sizeof(rgame_recording_batch) * batch_count);
|
|
47
|
+
if (!vertices || !batches) {
|
|
48
|
+
free(vertices);
|
|
49
|
+
free(batches);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
memcpy(vertices, rgame_draw_queue_vertices(queue), sizeof(rgame_vertex) * vertex_count);
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* The queue's batches carry a clip as well; only the texture and the span
|
|
57
|
+
* come across. See recording.h — a clip recorded at one place on screen is
|
|
58
|
+
* wrong at every other place the recording is replayed.
|
|
59
|
+
*/
|
|
60
|
+
for (unsigned int i = 0; i < batch_count; i++) {
|
|
61
|
+
const rgame_draw_batch *source = rgame_draw_queue_batch(queue, i);
|
|
62
|
+
batches[i].texture = source->texture;
|
|
63
|
+
batches[i].first_vertex = source->first_vertex;
|
|
64
|
+
batches[i].vertex_count = source->vertex_count;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
out->vertices = vertices;
|
|
68
|
+
out->vertex_count = vertex_count;
|
|
69
|
+
out->batches = batches;
|
|
70
|
+
out->batch_count = batch_count;
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
unsigned int rgame_recording_batch_count(const rgame_recording *recording) {
|
|
75
|
+
return recording ? recording->batch_count : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
unsigned int rgame_recording_vertex_count(const rgame_recording *recording) {
|
|
79
|
+
return recording ? recording->vertex_count : 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
void rgame_recording_bounds(const rgame_recording *recording, float *min_x, float *min_y,
|
|
83
|
+
float *max_x, float *max_y) {
|
|
84
|
+
float left = 0.0f, top = 0.0f, right = 0.0f, bottom = 0.0f;
|
|
85
|
+
|
|
86
|
+
if (recording && recording->vertex_count > 0) {
|
|
87
|
+
left = right = recording->vertices[0].x;
|
|
88
|
+
top = bottom = recording->vertices[0].y;
|
|
89
|
+
|
|
90
|
+
for (unsigned int i = 1; i < recording->vertex_count; i++) {
|
|
91
|
+
const rgame_vertex *vertex = &recording->vertices[i];
|
|
92
|
+
if (vertex->x < left) {
|
|
93
|
+
left = vertex->x;
|
|
94
|
+
}
|
|
95
|
+
if (vertex->x > right) {
|
|
96
|
+
right = vertex->x;
|
|
97
|
+
}
|
|
98
|
+
if (vertex->y < top) {
|
|
99
|
+
top = vertex->y;
|
|
100
|
+
}
|
|
101
|
+
if (vertex->y > bottom) {
|
|
102
|
+
bottom = vertex->y;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (min_x) {
|
|
108
|
+
*min_x = left;
|
|
109
|
+
}
|
|
110
|
+
if (min_y) {
|
|
111
|
+
*min_y = top;
|
|
112
|
+
}
|
|
113
|
+
if (max_x) {
|
|
114
|
+
*max_x = right;
|
|
115
|
+
}
|
|
116
|
+
if (max_y) {
|
|
117
|
+
*max_y = bottom;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#ifndef RGAME_RECORDING_H
|
|
2
|
+
#define RGAME_RECORDING_H
|
|
3
|
+
|
|
4
|
+
#include "graphics/draw_queue.h"
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* A baked block of drawing, kept between frames and replayed as a few calls.
|
|
8
|
+
*
|
|
9
|
+
* The problem it solves is the tile map. A screen of 16px tiles is a couple of
|
|
10
|
+
* thousand quads, and while the batching queue already merges them into one GL
|
|
11
|
+
* call, the *CPU* still walks every tile, transforms four corners and appends a
|
|
12
|
+
* command — sixty times a second, for a layer that has not changed since the
|
|
13
|
+
* level loaded.
|
|
14
|
+
*
|
|
15
|
+
* A recording captures that work once. What comes out is the finished vertex
|
|
16
|
+
* array, already sorted and grouped by texture, so replaying it is one command
|
|
17
|
+
* per texture instead of one per tile:
|
|
18
|
+
*
|
|
19
|
+
* bake once 2000 tiles -> 2000 commands -> 1 batch
|
|
20
|
+
* replay 1 command -> 1 batch, every frame
|
|
21
|
+
*
|
|
22
|
+
* ---------------------------------------------------------------------------
|
|
23
|
+
* What is captured, and what is not
|
|
24
|
+
* ---------------------------------------------------------------------------
|
|
25
|
+
*
|
|
26
|
+
* Positions and texture coordinates are baked. Transforms *inside* the recorded
|
|
27
|
+
* block are baked with them — a rotated sprite records its rotated corners —
|
|
28
|
+
* and the transform in effect at *replay* time is applied on top, which is what
|
|
29
|
+
* lets a baked layer scroll under a camera.
|
|
30
|
+
*
|
|
31
|
+
* Clips are not captured at all: a batch carries a texture and a span of
|
|
32
|
+
* vertices, and nothing else. Clipping happens at rasterisation, so it cannot
|
|
33
|
+
* be baked into a vertex, and a clip rectangle recorded in one place would be
|
|
34
|
+
* wrong everywhere the recording is later replayed. Pushing a clip inside a
|
|
35
|
+
* recorded block is therefore refused rather than silently dropped; clip the
|
|
36
|
+
* *replay* instead, which does exactly what the caller meant.
|
|
37
|
+
*
|
|
38
|
+
* Colours are baked, but a replay may tint them — multiplying each recorded
|
|
39
|
+
* component by the tint's. The layer being replaced could not do this (its
|
|
40
|
+
* recorded images drew white-only, and callers worked around it); there is no
|
|
41
|
+
* reason to inherit the limitation.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/* A run of recorded vertices sharing one texture — one GL call's worth. */
|
|
45
|
+
typedef struct {
|
|
46
|
+
unsigned int texture; /* 0 means untextured */
|
|
47
|
+
unsigned int first_vertex, vertex_count;
|
|
48
|
+
} rgame_recording_batch;
|
|
49
|
+
|
|
50
|
+
/* Tagged so the public header can forward-declare the same type without
|
|
51
|
+
* seeing what is in it — src/main.c and the Ruby binding only ever hold a
|
|
52
|
+
* pointer. */
|
|
53
|
+
typedef struct rgame_recording {
|
|
54
|
+
rgame_vertex *vertices; /* in painter order, grouped by batch */
|
|
55
|
+
unsigned int vertex_count;
|
|
56
|
+
|
|
57
|
+
rgame_recording_batch *batches;
|
|
58
|
+
unsigned int batch_count;
|
|
59
|
+
} rgame_recording;
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* Copies a *prepared* draw queue into `out`, which must be uninitialised or
|
|
63
|
+
* already destroyed. Returns 1 on success, 0 if out of memory — in which case
|
|
64
|
+
* `out` is left empty and is still safe to destroy.
|
|
65
|
+
*
|
|
66
|
+
* "Prepared" means rgame_draw_queue_prepare has run: the sorting and grouping
|
|
67
|
+
* is exactly the work being saved, so a recording captures its result rather
|
|
68
|
+
* than the raw command list.
|
|
69
|
+
*
|
|
70
|
+
* An empty queue records successfully and replays as nothing.
|
|
71
|
+
*/
|
|
72
|
+
int rgame_recording_capture(rgame_recording *out, const rgame_draw_queue *queue);
|
|
73
|
+
|
|
74
|
+
/* Frees the buffers. Safe on a zeroed struct, and safe to call twice. */
|
|
75
|
+
void rgame_recording_destroy(rgame_recording *recording);
|
|
76
|
+
|
|
77
|
+
unsigned int rgame_recording_batch_count(const rgame_recording *recording);
|
|
78
|
+
unsigned int rgame_recording_vertex_count(const rgame_recording *recording);
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* The bounding box of everything recorded, in the recording's own coordinates.
|
|
82
|
+
* Zero-sized for an empty recording. Games use it to decide whether a baked
|
|
83
|
+
* layer is on screen at all before replaying it.
|
|
84
|
+
*/
|
|
85
|
+
void rgame_recording_bounds(const rgame_recording *recording, float *min_x, float *min_y,
|
|
86
|
+
float *max_x, float *max_y);
|
|
87
|
+
|
|
88
|
+
#endif /* RGAME_RECORDING_H */
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* texture.c — sheet refcounting, sub-rectangle composition, pixel-to-UV.
|
|
3
|
+
* See texture.h for what the two structs are and why the refcount lives here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
#include "graphics/texture.h"
|
|
7
|
+
|
|
8
|
+
#include <stdlib.h>
|
|
9
|
+
|
|
10
|
+
/* See rgame_texture_live_sheets in the header for why this exists. */
|
|
11
|
+
static long live_sheets = 0;
|
|
12
|
+
|
|
13
|
+
long rgame_texture_live_sheets(void) {
|
|
14
|
+
return live_sheets;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
rgame_texture_sheet *rgame_texture_sheet_create(unsigned int name, int width, int height) {
|
|
18
|
+
if (width <= 0 || height <= 0) {
|
|
19
|
+
return NULL;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
rgame_texture_sheet *sheet = malloc(sizeof(rgame_texture_sheet));
|
|
23
|
+
if (!sheet) {
|
|
24
|
+
return NULL;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
sheet->name = name;
|
|
28
|
+
sheet->width = width;
|
|
29
|
+
sheet->height = height;
|
|
30
|
+
sheet->refs = 1;
|
|
31
|
+
live_sheets++;
|
|
32
|
+
return sheet;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void rgame_texture_sheet_retain(rgame_texture_sheet *sheet) {
|
|
36
|
+
if (sheet) {
|
|
37
|
+
sheet->refs++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
int rgame_texture_sheet_release(rgame_texture_sheet *sheet, unsigned int *out_name) {
|
|
42
|
+
if (!sheet) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (--sheet->refs > 0) {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Read the name out before freeing — after free(sheet) it is gone, and
|
|
51
|
+
* reading it back is the classic use-after-free this ordering avoids. */
|
|
52
|
+
unsigned int name = sheet->name;
|
|
53
|
+
free(sheet);
|
|
54
|
+
live_sheets--;
|
|
55
|
+
if (out_name) {
|
|
56
|
+
*out_name = name;
|
|
57
|
+
}
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
rgame_texture rgame_texture_whole(rgame_texture_sheet *sheet) {
|
|
62
|
+
rgame_texture view = {0};
|
|
63
|
+
if (!sheet) {
|
|
64
|
+
return view;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
rgame_texture_sheet_retain(sheet);
|
|
68
|
+
view.sheet = sheet;
|
|
69
|
+
view.rect = rgame_rect_make(0, 0, sheet->width, sheet->height);
|
|
70
|
+
return view;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
int rgame_texture_destroy(rgame_texture *view, unsigned int *out_name) {
|
|
74
|
+
if (!view || !view->sheet) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
int died = rgame_texture_sheet_release(view->sheet, out_name);
|
|
79
|
+
/* Clearing the pointer makes a second destroy a no-op rather than a double
|
|
80
|
+
* free. Ruby's GC and an explicit close can both reach the same view. */
|
|
81
|
+
view->sheet = NULL;
|
|
82
|
+
view->rect = rgame_rect_make(0, 0, 0, 0);
|
|
83
|
+
return died;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
rgame_texture rgame_texture_clone(const rgame_texture *view) {
|
|
87
|
+
rgame_texture copy = {0};
|
|
88
|
+
if (!view || !view->sheet) {
|
|
89
|
+
return copy;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rgame_texture_sheet_retain(view->sheet);
|
|
93
|
+
copy = *view;
|
|
94
|
+
return copy;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
int rgame_texture_subimage(const rgame_texture *view, int x, int y, int w, int h,
|
|
98
|
+
rgame_texture *out) {
|
|
99
|
+
if (!view || !view->sheet || !out || w <= 0 || h <= 0 || x < 0 || y < 0) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Compared against the *view's* size, not the sheet's: a sub-rect is
|
|
104
|
+
* relative to what it is being cut from, so a tile of a tile can never
|
|
105
|
+
* wander outside the tile it came from. */
|
|
106
|
+
if (x + w > view->rect.w || y + h > view->rect.h) {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
rgame_texture_sheet_retain(view->sheet);
|
|
111
|
+
out->sheet = view->sheet;
|
|
112
|
+
out->rect = rgame_rect_make(view->rect.x + x, view->rect.y + y, w, h);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
unsigned int rgame_texture_name(const rgame_texture *view) {
|
|
117
|
+
return view && view->sheet ? view->sheet->name : 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
int rgame_texture_width(const rgame_texture *view) {
|
|
121
|
+
return view && view->sheet ? view->rect.w : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
int rgame_texture_height(const rgame_texture *view) {
|
|
125
|
+
return view && view->sheet ? view->rect.h : 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
int rgame_texture_tile_count(const rgame_texture *view, int tile_width, int tile_height) {
|
|
129
|
+
if (!view || !view->sheet || tile_width <= 0 || tile_height <= 0) {
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (view->rect.w / tile_width) * (view->rect.h / tile_height);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
int rgame_texture_tile(const rgame_texture *view, int tile_width, int tile_height, int index,
|
|
137
|
+
rgame_texture *out) {
|
|
138
|
+
/* `index < 0` is redundant today and deliberately kept: a negative index
|
|
139
|
+
* yields negative offsets, which rgame_texture_subimage refuses anyway, so
|
|
140
|
+
* mutating it away survives the suite. Half a range check is still the
|
|
141
|
+
* wrong thing to read at the place the range is computed. */
|
|
142
|
+
if (index < 0 || index >= rgame_texture_tile_count(view, tile_width, tile_height)) {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
int columns = view->rect.w / tile_width;
|
|
147
|
+
return rgame_texture_subimage(view, (index % columns) * tile_width,
|
|
148
|
+
(index / columns) * tile_height, tile_width, tile_height, out);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
void rgame_texture_uv(const rgame_texture *view, float *uv8) {
|
|
152
|
+
if (!uv8) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* A view with no sheet has no pixels to sample; hand back a degenerate
|
|
157
|
+
* 0,0 square rather than dividing by a zero sheet size. */
|
|
158
|
+
if (!view || !view->sheet) {
|
|
159
|
+
for (int i = 0; i < 8; i++) {
|
|
160
|
+
uv8[i] = 0.0f;
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Normalise against the *sheet*: UVs address the whole uploaded texture,
|
|
166
|
+
* so a 16px tile in a 512px sheet spans 1/32 of the coordinate space, not
|
|
167
|
+
* all of it. Dividing by the view size instead is the mistake this
|
|
168
|
+
* function exists to make impossible to repeat. */
|
|
169
|
+
float sheet_w = (float)view->sheet->width;
|
|
170
|
+
float sheet_h = (float)view->sheet->height;
|
|
171
|
+
|
|
172
|
+
float u0 = (float)view->rect.x / sheet_w;
|
|
173
|
+
float v0 = (float)view->rect.y / sheet_h;
|
|
174
|
+
float u1 = (float)(view->rect.x + view->rect.w) / sheet_w;
|
|
175
|
+
float v1 = (float)(view->rect.y + view->rect.h) / sheet_h;
|
|
176
|
+
|
|
177
|
+
uv8[0] = u0; uv8[1] = v0; /* top-left */
|
|
178
|
+
uv8[2] = u1; uv8[3] = v0; /* top-right */
|
|
179
|
+
uv8[4] = u1; uv8[5] = v1; /* bottom-right */
|
|
180
|
+
uv8[6] = u0; uv8[7] = v1; /* bottom-left */
|
|
181
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#ifndef RGAME_TEXTURE_H
|
|
2
|
+
#define RGAME_TEXTURE_H
|
|
3
|
+
|
|
4
|
+
#include "graphics/clip.h"
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Textures: what part of an uploaded image a draw call should sample, and who
|
|
8
|
+
* owns the upload. Pure arithmetic and pure bookkeeping — no GL, no stb, no
|
|
9
|
+
* I/O. The decode-and-upload half lives in image.c, which is deliberately as
|
|
10
|
+
* dumb as it can be (CLAUDE.md's layer 3).
|
|
11
|
+
*
|
|
12
|
+
* ---------------------------------------------------------------------------
|
|
13
|
+
* Two structs, because there are two lifetimes
|
|
14
|
+
* ---------------------------------------------------------------------------
|
|
15
|
+
*
|
|
16
|
+
* A sprite sheet is decoded and uploaded to the GPU *once*, and then sliced
|
|
17
|
+
* into dozens of sprites. Each of those sprites is an ordinary object a game
|
|
18
|
+
* holds, drops and garbage-collects on its own schedule — but the pixels
|
|
19
|
+
* behind them are one shared allocation on the GPU.
|
|
20
|
+
*
|
|
21
|
+
* So:
|
|
22
|
+
*
|
|
23
|
+
* rgame_texture_sheet one GL texture. Reference-counted, heap-allocated,
|
|
24
|
+
* shared by every view into it.
|
|
25
|
+
* rgame_texture a *view*: a sheet plus the sub-rectangle of it this
|
|
26
|
+
* particular sprite occupies. Small, copyable, holds
|
|
27
|
+
* one reference to its sheet.
|
|
28
|
+
*
|
|
29
|
+
* `subimage` therefore costs a rectangle intersection and a refcount bump —
|
|
30
|
+
* no second decode and no second upload. Slicing a 512x512 sheet into 1024
|
|
31
|
+
* tiles produces 1024 views over one texture.
|
|
32
|
+
*
|
|
33
|
+
* ---------------------------------------------------------------------------
|
|
34
|
+
* Why the refcount is here and not in image.c
|
|
35
|
+
* ---------------------------------------------------------------------------
|
|
36
|
+
*
|
|
37
|
+
* "Free the GPU texture exactly when the last sprite using it goes away" is
|
|
38
|
+
* the kind of rule that is wrong in one branch and leaks for a year. It is
|
|
39
|
+
* also, written out, pure integer bookkeeping — nothing about it needs a GPU.
|
|
40
|
+
* So it lives here, where `test/test_texture.c` can drop views in every order
|
|
41
|
+
* and assert the sheet dies once and only once.
|
|
42
|
+
*
|
|
43
|
+
* The one thing this module cannot do is call `glDeleteTextures`. Instead
|
|
44
|
+
* `rgame_texture_sheet_release` *reports* that the last reference went away
|
|
45
|
+
* and hands back the GL name it was holding; the caller in image.c does the
|
|
46
|
+
* one-line deletion. That keeps the decision here and the syscall there.
|
|
47
|
+
*
|
|
48
|
+
* ---------------------------------------------------------------------------
|
|
49
|
+
* Pixels in, UVs out
|
|
50
|
+
* ---------------------------------------------------------------------------
|
|
51
|
+
*
|
|
52
|
+
* Callers talk in pixels — "the tile at 32,16, sixteen by sixteen" — because
|
|
53
|
+
* that is how sprite sheets are drawn and described. OpenGL samples in
|
|
54
|
+
* normalised texture coordinates, 0..1 across the whole texture. Converting
|
|
55
|
+
* between them is one division that is easy to get subtly wrong (by the sheet
|
|
56
|
+
* size, not the view size), so it happens in exactly one place:
|
|
57
|
+
* `rgame_texture_uv`.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
typedef struct {
|
|
61
|
+
/* The GL texture name. Opaque here: this module never calls GL, it only
|
|
62
|
+
* carries the number so that release can hand it back for deletion. */
|
|
63
|
+
unsigned int name;
|
|
64
|
+
int width, height; /* the whole uploaded image, in pixels */
|
|
65
|
+
int refs;
|
|
66
|
+
} rgame_texture_sheet;
|
|
67
|
+
|
|
68
|
+
typedef struct {
|
|
69
|
+
rgame_texture_sheet *sheet;
|
|
70
|
+
/* The part of the sheet this view samples, in sheet pixels. */
|
|
71
|
+
rgame_rect rect;
|
|
72
|
+
} rgame_texture;
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Creates a sheet with one reference, or NULL if out of memory or given a
|
|
76
|
+
* degenerate size. `name` is whatever the GL layer allocated; this module
|
|
77
|
+
* treats it as an opaque token, so tests can pass any number they like.
|
|
78
|
+
*/
|
|
79
|
+
rgame_texture_sheet *rgame_texture_sheet_create(unsigned int name, int width, int height);
|
|
80
|
+
|
|
81
|
+
void rgame_texture_sheet_retain(rgame_texture_sheet *sheet);
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* Drops one reference. Returns 1 if that was the last one — in which case the
|
|
85
|
+
* sheet has been freed and `*out_name` holds the GL name the caller must now
|
|
86
|
+
* delete — and 0 if others remain (`*out_name` untouched).
|
|
87
|
+
*
|
|
88
|
+
* Returning the name rather than deleting it is what keeps this file free of
|
|
89
|
+
* GL. `out_name` may not be NULL when the caller intends to honour the
|
|
90
|
+
* deletion; passing NULL is allowed and simply discards it, which is what the
|
|
91
|
+
* tests that only care about lifetimes do.
|
|
92
|
+
*/
|
|
93
|
+
int rgame_texture_sheet_release(rgame_texture_sheet *sheet, unsigned int *out_name);
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* A view covering the whole sheet, retaining it. Every view — this one
|
|
97
|
+
* included — must eventually be passed to rgame_texture_destroy.
|
|
98
|
+
*/
|
|
99
|
+
rgame_texture rgame_texture_whole(rgame_texture_sheet *sheet);
|
|
100
|
+
|
|
101
|
+
/* Drops this view's reference. Same return contract as sheet_release, so a
|
|
102
|
+
* caller can delete the GL texture when the last view goes. */
|
|
103
|
+
int rgame_texture_destroy(rgame_texture *view, unsigned int *out_name);
|
|
104
|
+
|
|
105
|
+
/* An independent view of the same region — retains the sheet. */
|
|
106
|
+
rgame_texture rgame_texture_clone(const rgame_texture *view);
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* A sub-rectangle of `view`, in coordinates *relative to that view* — so
|
|
110
|
+
* slicing a slice composes without the caller tracking absolute offsets.
|
|
111
|
+
*
|
|
112
|
+
* Returns 1 on success. Returns 0, leaving `*out` untouched, if the rect is
|
|
113
|
+
* degenerate or reaches outside the view: garbage UVs sample a neighbouring
|
|
114
|
+
* sprite, which looks like a drawing bug rather than the indexing bug it is,
|
|
115
|
+
* so this refuses instead of clamping.
|
|
116
|
+
*/
|
|
117
|
+
int rgame_texture_subimage(const rgame_texture *view, int x, int y, int w, int h,
|
|
118
|
+
rgame_texture *out);
|
|
119
|
+
|
|
120
|
+
/* The GL texture name to bind when drawing this view, or 0 if it has no sheet
|
|
121
|
+
* — which the draw queue reads as "untextured". */
|
|
122
|
+
unsigned int rgame_texture_name(const rgame_texture *view);
|
|
123
|
+
|
|
124
|
+
int rgame_texture_width(const rgame_texture *view);
|
|
125
|
+
int rgame_texture_height(const rgame_texture *view);
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* How many whole `tile_width` x `tile_height` tiles fit, row-major. A partial
|
|
129
|
+
* tile at the right or bottom edge is not counted — a sheet whose size is not
|
|
130
|
+
* a multiple of the tile size has a strip of padding, and half a sprite is
|
|
131
|
+
* never what was meant. Zero or negative tile sizes yield 0.
|
|
132
|
+
*/
|
|
133
|
+
int rgame_texture_tile_count(const rgame_texture *view, int tile_width, int tile_height);
|
|
134
|
+
|
|
135
|
+
/* The `index`-th tile, counting left to right then top to bottom. Returns 1 on
|
|
136
|
+
* success, 0 if the index is outside the count above. */
|
|
137
|
+
int rgame_texture_tile(const rgame_texture *view, int tile_width, int tile_height, int index,
|
|
138
|
+
rgame_texture *out);
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* The view's four corners in normalised texture coordinates, written to `uv8`
|
|
142
|
+
* as x,y pairs in the same corner order the canvas takes a quad's points:
|
|
143
|
+
* top-left, top-right, bottom-right, bottom-left.
|
|
144
|
+
*
|
|
145
|
+
* v runs downwards, matching the pixel rect and the screen: image row 0 is
|
|
146
|
+
* v = 0. That holds because image.c uploads the decoded rows in the order stb
|
|
147
|
+
* produces them (top row first), rather than flipping them the way a
|
|
148
|
+
* bottom-left-origin convention would need.
|
|
149
|
+
*/
|
|
150
|
+
void rgame_texture_uv(const rgame_texture *view, float *uv8);
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
* How many sheets are alive right now — for tests, not for gameplay.
|
|
154
|
+
*
|
|
155
|
+
* A leaked GPU texture is invisible: the game runs, the frame rate is fine,
|
|
156
|
+
* and video memory fills up over an hour of play. This counter makes "the
|
|
157
|
+
* texture went away when the last sprite using it did" an assertion, both in
|
|
158
|
+
* the Check suite and from Ruby, where the collector chooses the order in
|
|
159
|
+
* which an app and its images are swept.
|
|
160
|
+
*
|
|
161
|
+
* Single-threaded, like the rest of the engine.
|
|
162
|
+
*/
|
|
163
|
+
long rgame_texture_live_sheets(void);
|
|
164
|
+
|
|
165
|
+
#endif /* RGAME_TEXTURE_H */
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#include "graphics/transform.h"
|
|
2
|
+
|
|
3
|
+
#include <math.h>
|
|
4
|
+
|
|
5
|
+
/* Degrees in, radians out — the C library's trig wants radians, the API takes
|
|
6
|
+
* degrees because that is what callers already pass. */
|
|
7
|
+
#define RGAME_DEGREES_TO_RADIANS (3.14159265358979323846f / 180.0f)
|
|
8
|
+
|
|
9
|
+
/* Tolerance for "is this the identity". Composing and un-composing transforms
|
|
10
|
+
* accumulates float error, so an exact == would answer "no" for a stack that is
|
|
11
|
+
* identity in every way that matters. */
|
|
12
|
+
#define RGAME_IDENTITY_EPSILON 1e-6f
|
|
13
|
+
|
|
14
|
+
rgame_transform rgame_transform_identity(void) {
|
|
15
|
+
rgame_transform t = { .a = 1.0f, .b = 0.0f, .c = 0.0f, .d = 1.0f, .tx = 0.0f, .ty = 0.0f };
|
|
16
|
+
return t;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
rgame_transform rgame_transform_multiply(rgame_transform outer, rgame_transform inner) {
|
|
20
|
+
/*
|
|
21
|
+
* Substituting inner's output into outer's formula and collecting terms:
|
|
22
|
+
*
|
|
23
|
+
* x' = outer.a*(inner.a*x + inner.c*y + inner.tx)
|
|
24
|
+
* + outer.c*(inner.b*x + inner.d*y + inner.ty) + outer.tx
|
|
25
|
+
*
|
|
26
|
+
* and likewise for y'. Written out rather than looped, because six named
|
|
27
|
+
* multiplies read better than a 2x3 loop and this is the hot path.
|
|
28
|
+
*/
|
|
29
|
+
rgame_transform result;
|
|
30
|
+
result.a = (outer.a * inner.a) + (outer.c * inner.b);
|
|
31
|
+
result.b = (outer.b * inner.a) + (outer.d * inner.b);
|
|
32
|
+
result.c = (outer.a * inner.c) + (outer.c * inner.d);
|
|
33
|
+
result.d = (outer.b * inner.c) + (outer.d * inner.d);
|
|
34
|
+
result.tx = (outer.a * inner.tx) + (outer.c * inner.ty) + outer.tx;
|
|
35
|
+
result.ty = (outer.b * inner.tx) + (outer.d * inner.ty) + outer.ty;
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
void rgame_transform_stack_init(rgame_transform_stack *stack) {
|
|
40
|
+
stack->depth = 0;
|
|
41
|
+
stack->entries[0] = rgame_transform_identity();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Shared tail of the three pushes: compose with the current top, store, and
|
|
45
|
+
* report whether there was room. */
|
|
46
|
+
static int push_composed(rgame_transform_stack *stack, rgame_transform step) {
|
|
47
|
+
if (stack->depth + 1 >= RGAME_TRANSFORM_STACK_DEPTH) {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
stack->entries[stack->depth + 1] =
|
|
51
|
+
rgame_transform_multiply(stack->entries[stack->depth], step);
|
|
52
|
+
stack->depth++;
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
int rgame_transform_push_translate(rgame_transform_stack *stack, float dx, float dy) {
|
|
57
|
+
rgame_transform step = rgame_transform_identity();
|
|
58
|
+
step.tx = dx;
|
|
59
|
+
step.ty = dy;
|
|
60
|
+
return push_composed(stack, step);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
int rgame_transform_push_scale(rgame_transform_stack *stack, float sx, float sy) {
|
|
64
|
+
rgame_transform step = rgame_transform_identity();
|
|
65
|
+
step.a = sx;
|
|
66
|
+
step.d = sy;
|
|
67
|
+
return push_composed(stack, step);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
int rgame_transform_push_rotate(rgame_transform_stack *stack, float degrees,
|
|
71
|
+
float pivot_x, float pivot_y) {
|
|
72
|
+
float radians = degrees * RGAME_DEGREES_TO_RADIANS;
|
|
73
|
+
float cosine = cosf(radians);
|
|
74
|
+
float sine = sinf(radians);
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Rotating about a pivot is three steps: move the pivot to the origin,
|
|
78
|
+
* rotate, move it back. Composed here rather than pushed as three entries,
|
|
79
|
+
* so one push still matches one pop.
|
|
80
|
+
*
|
|
81
|
+
* The rotation itself is the textbook matrix. In screen space, where y
|
|
82
|
+
* points down, it turns *clockwise* for a positive angle — see the header.
|
|
83
|
+
*/
|
|
84
|
+
rgame_transform rotation = { .a = cosine, .b = sine, .c = -sine, .d = cosine,
|
|
85
|
+
.tx = 0.0f, .ty = 0.0f };
|
|
86
|
+
|
|
87
|
+
rgame_transform to_origin = rgame_transform_identity();
|
|
88
|
+
to_origin.tx = -pivot_x;
|
|
89
|
+
to_origin.ty = -pivot_y;
|
|
90
|
+
|
|
91
|
+
rgame_transform back = rgame_transform_identity();
|
|
92
|
+
back.tx = pivot_x;
|
|
93
|
+
back.ty = pivot_y;
|
|
94
|
+
|
|
95
|
+
rgame_transform step =
|
|
96
|
+
rgame_transform_multiply(back, rgame_transform_multiply(rotation, to_origin));
|
|
97
|
+
return push_composed(stack, step);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
void rgame_transform_pop(rgame_transform_stack *stack) {
|
|
101
|
+
if (stack->depth > 0) {
|
|
102
|
+
stack->depth--;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
rgame_transform rgame_transform_current(const rgame_transform_stack *stack) {
|
|
107
|
+
return stack->entries[stack->depth];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
int rgame_transform_depth(const rgame_transform_stack *stack) {
|
|
111
|
+
return stack->depth;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
void rgame_transform_apply(const rgame_transform_stack *stack, float x, float y,
|
|
115
|
+
float *out_x, float *out_y) {
|
|
116
|
+
const rgame_transform *t = &stack->entries[stack->depth];
|
|
117
|
+
*out_x = (t->a * x) + (t->c * y) + t->tx;
|
|
118
|
+
*out_y = (t->b * x) + (t->d * y) + t->ty;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
int rgame_transform_is_identity(const rgame_transform_stack *stack) {
|
|
122
|
+
const rgame_transform *t = &stack->entries[stack->depth];
|
|
123
|
+
return fabsf(t->a - 1.0f) < RGAME_IDENTITY_EPSILON &&
|
|
124
|
+
fabsf(t->b) < RGAME_IDENTITY_EPSILON &&
|
|
125
|
+
fabsf(t->c) < RGAME_IDENTITY_EPSILON &&
|
|
126
|
+
fabsf(t->d - 1.0f) < RGAME_IDENTITY_EPSILON &&
|
|
127
|
+
fabsf(t->tx) < RGAME_IDENTITY_EPSILON && fabsf(t->ty) < RGAME_IDENTITY_EPSILON;
|
|
128
|
+
}
|