-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.odin
714 lines (631 loc) · 24.3 KB
/
main.odin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
package sprite_renderer
// The code not really relevant to the rendering, like defining our sprites,
// tick and input can be found in `non_renderer_code.odin`
import "base:runtime"
import "core:c"
import "core:fmt"
import "core:slice"
import "core:time"
import stbi "vendor:stb/image"
import sapp "third_party/sokol/app"
import sg "third_party/sokol/gfx"
import sglue "third_party/sokol/glue"
import slog "third_party/sokol/log"
import "shaders"
// Our renderer will have a resolution of 320x180, 16:9
// - 40 tiles wide
// - 22.5 tiles high, with the half tile being on the top
TILES_X :: 40
TILES_Y :: 22.5
TILE_UNIT :: 8
GAME_WIDTH :: TILES_X * TILE_UNIT
GAME_HEIGHT :: TILES_Y * TILE_UNIT
// I just like having this so I don't accidentally define my quads incorrectly
QUAD_INDEX_SIZE :: 6
BUDGET_SPRITES :: 1024
Sprite_Batch :: struct {
// Odin uses manual memory management but there is no memory to manage if it's in "rodata" heh!
// We could also use `[dynamic]` in combination with the `temp_allocator` and begin our frame with
// `defer free_all(context.temp_allocator)`, whatever floats your boat.
// I like the idea of having a "budget" for small games.
// https://odin-lang.org/docs/overview/#allocators
instances: [BUDGET_SPRITES]Sprite_Instance,
len: uint,
}
// This is the thing we will upload to the GPU!
Sprite_Instance :: struct {
location: [2]f32,
size: [2]f32,
position: [2]f32,
scale: [2]f32,
color: [4]f32,
}
// This is what we render our game to
Offscreen :: struct {
pixel_to_viewport_multiplier: [2]f32,
sprite_atlas_size: [2]i32,
pass: sg.Pass,
pipeline: sg.Pipeline,
bindings: sg.Bindings,
}
// Our display scales up the `Offscreen` to match our physical display
Display :: struct {
pass_action: sg.Pass_Action,
pipeline: sg.Pipeline,
bindings: sg.Bindings,
}
Renderer :: struct {
offscreen: Offscreen,
display: Display,
sprite_batch: Sprite_Batch,
}
///////////////////////////////////////////////////////////////////////////////
// Global struct to store the things we need to render
renderer: Renderer
// see `not_renderer_code.odin`
timer: Timer
input: Input
///////////////////////////////////////////////////////////////////////////////
// https://odin-lang.org/docs/overview/#calling-conventions
init :: proc "c" () {
// https://odin-lang.org/docs/overview/#explicit-context-definition
context = runtime.default_context()
sg.setup({environment = sglue.environment(), logger = {func = slog.func}})
////////////////////////////////////////////////////////////////////////////
// Offscreen renderer
// The offscreen renderer renders our game to a low resolution
// "render target.
// This render target is used as a texture in the display renderer to do a
// pixel perfact upscale as close as possible to the window size.
////////////////////////////////////////////////////////////////////////////
assert(GAME_WIDTH > 0, fmt.tprintf("game_width > 0: %v", GAME_WIDTH))
assert(GAME_HEIGHT > 0, fmt.tprintf("game_height > 0: %v", GAME_HEIGHT))
// This is a multiplier to translate our GAME coordinates to viewport coordinates
pixel_to_viewport_multiplier := gfx_get_pixel_to_viewport_multiplier(
GAME_WIDTH,
GAME_HEIGHT,
)
OFFSCREEN_PIXEL_FORMAT :: sg.Pixel_Format.RGBA8
OFFSCREEN_SAMPLE_COUNT :: 1
////////////////////////////////////////////////////////////////////////////
// `render_target` is a color attachment in the offscreen rendering pass.
// But also a fragement shader texture in the display rendering pass.
// Everything that is rendered in our GAME is rendered to this image.
image_description := sg.Image_Desc {
render_target = true,
width = i32(GAME_WIDTH),
height = i32(GAME_HEIGHT),
pixel_format = OFFSCREEN_PIXEL_FORMAT,
sample_count = OFFSCREEN_SAMPLE_COUNT,
label = "color-image-render-target",
}
render_target := sg.make_image(image_description)
// Depth stencil for alpha blending, so we can have transparent sprites.
image_description.pixel_format = .DEPTH
image_description.label = "depth-image-render-target"
depth_image := sg.make_image(image_description)
// Attach the render target to our offscreen pass.
offscreen_pass := sg.Pass {
attachments = sg.make_attachments(
{
colors = {0 = {image = render_target}},
depth_stencil = {image = depth_image},
label = "offscreen-attachments",
},
),
action = {
colors = {
0 = {
load_action = .CLEAR,
clear_value = sg.Color{0.2, 0.2, 0.2, 1},
},
},
},
label = "offscreen-pass",
}
// Single quad reused by all our sprites.
// odinfmt: disable
// The `offscreen_index_buffer_vertices` will map the values
// `0, 1, 3` and `1, 2, 3` to these coordinates.
offscreen_vertex_buffer_vertices := [8]f32{
1, 1, // [0]
1, 0, // [1]
0, 0, // [2]
0, 1, // [3]
}
// Two triangles creates a quad
// [2] {0, 0} [1] {1, 0}
// \ /
// x----------x
// | /|
// | 2 / |
// | / |
// | / 1 |
// | / |
// x__________x
// / \
// [3] {0, 1} [0] {1, 1}
offscreen_index_buffer_vertices := [6]u16{
0, 1, 3, // triangle 1
1, 2, 3, // triangle 2
}
// odinfmt: enable
offscreen_vertex_buffer := sg.make_buffer(
{
type = .VERTEXBUFFER,
data = as_range(&offscreen_vertex_buffer_vertices),
label = "offscreen-vertex-buffer",
},
)
offscreen_index_buffer := sg.make_buffer(
{
type = .INDEXBUFFER,
data = as_range(&offscreen_index_buffer_vertices),
label = "offscreen-index-buffer",
},
)
// Another vertex buffer, instanced for all data for each sprite.
// see `usage = .STREAM`
// This buffer will contain the actual position, color, size etc.
// We will put a bunch of `Sprite_Instance`s in this each frame.
offscreen_instance_buffer := sg.make_buffer(
{
usage = .STREAM,
type = .VERTEXBUFFER,
size = BUDGET_SPRITES * size_of(Sprite_Instance),
label = "offscreen-instance-buffer",
},
)
// Offscreen pipeline
offscreen_pipeline := sg.make_pipeline(
{
layout = {
buffers = {1 = {step_func = .PER_INSTANCE}},
attrs = {
// Our quad vertex buffer, index 0
shaders.ATTR_offscreen_vertex_position = {
format = .FLOAT2,
buffer_index = 0,
},
// All these other values are tied to our instance buffer
// Notice how each `format =` lines up with our `Sprite_Instance` struct
shaders.ATTR_offscreen_location = {
format = .FLOAT2,
buffer_index = 1,
},
shaders.ATTR_offscreen_size = {
format = .FLOAT2,
buffer_index = 1,
},
shaders.ATTR_offscreen_position = {
format = .FLOAT2,
buffer_index = 1,
},
shaders.ATTR_offscreen_scale = {
format = .FLOAT2,
buffer_index = 1,
},
shaders.ATTR_offscreen_color = {
format = .FLOAT4,
buffer_index = 1,
},
},
},
index_type = .UINT16,
// Load the shader!
shader = sg.make_shader(
shaders.offscreen_shader_desc(sg.query_backend()),
),
depth = {
pixel_format = .DEPTH,
compare = .LESS_EQUAL,
write_enabled = true,
},
colors = {
0 = {
// This is what enables our sprites to be transparent.
// This is also what decides _how_ the are ordered.
// https://learnopengl.com/Advanced-OpenGL/Blending
blend = {
enabled = true,
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
op_rgb = .ADD,
src_factor_alpha = .SRC_ALPHA,
dst_factor_alpha = .ONE_MINUS_SRC_ALPHA,
op_alpha = .ADD,
},
pixel_format = OFFSCREEN_PIXEL_FORMAT,
},
},
color_count = 1,
sample_count = OFFSCREEN_SAMPLE_COUNT,
label = "offscreen-pipeline",
},
)
offscreen_sampler := sg.make_sampler(
{
min_filter = .NEAREST,
mag_filter = .NEAREST,
label = "offscreen-sampler",
},
)
// Load and create our sprite atlas texture using `stb_image`
// `#load` embeds the image in our binary!
// This allows us to ship a single .exe without any assets laying around.
// https://odin-lang.org/docs/overview/#loadstring-path-or-loadstring-path-type
asset_sprite_atlas := #load("assets/Pink_Monster.png")
sa_w, sa_h, channels: i32
stbi.set_flip_vertically_on_load(1)
sprite_atlas := stbi.load_from_memory(
raw_data(asset_sprite_atlas),
i32(len(asset_sprite_atlas)),
&sa_w,
&sa_h,
&channels,
4,
)
// free the loaded image at the end of init.
// `sg.make_image` will do the allocation needed and return a handle
// after this we don't need the stbi_loaded image.
// In general all `sg.make_` function do an allocation and return a handle!
// https://github.com/floooh/sokol/blob/master/sokol_gfx.h#L1320
// These are (I think lol) the only allocation that are done in this code.
// https://odin-lang.org/docs/overview/#defer-statement
defer stbi.image_free(sprite_atlas)
// Create the image (in the sokol sense), this will be our texture in the shader.
sprite_atlas_image := sg.make_image(
{
width = sa_w,
height = sa_h,
data = {
subimage = {
0 = {
0 = {
ptr = sprite_atlas,
size = c.size_t(sa_w * sa_h * 4),
},
},
},
},
pixel_format = OFFSCREEN_PIXEL_FORMAT,
label = "sprite-atlas",
},
)
// Here we set the buffers, sampler and image.
// To get the image into the shader we need:
// - A texture - our image!
// - A sampler
offscreen_bindings := sg.Bindings {
vertex_buffers = {
0 = offscreen_vertex_buffer,
1 = offscreen_instance_buffer,
},
index_buffer = offscreen_index_buffer,
samplers = {shaders.SMP_smp = offscreen_sampler},
images = {shaders.IMG_tex = sprite_atlas_image},
}
////////////////////////////////////////////////////////////////////////////
// Store all the things we need in our global struct.
renderer.offscreen = {
pixel_to_viewport_multiplier = pixel_to_viewport_multiplier,
sprite_atlas_size = {sa_w, sa_h},
pass = offscreen_pass,
pipeline = offscreen_pipeline,
bindings = offscreen_bindings,
}
////////////////////////////////////////////////////////////////////////////
// Display renderer
// The display renderer is simpler.
// The only thing this does is to render our image from the offscreen pass.
// This image is then scaled up to match our viewport size.
display_pass_action: sg.Pass_Action
display_pass_action.colors[0] = {
load_action = .CLEAR,
clear_value = {r = 0, g = 0, b = 0, a = 0},
}
// The same rules with the two quads as in the offscreen pass applies here too.
// The only difference is that the viewport coordinate space is
// {-1, 1} instead of {0, 1}
// Hence we pass two values:
// The first is the viewport coordinates
// The second is the quad from before (offscreen pass) coordinates.
// Honestly I am not even sure if the name `uv` is correct here.
// The following code is kinda the same as the Offscreen renderer with one big difference:
// in the `display_bindings` we set the `image` to the `render_target`!
// odinfmt: disable
quad_vertices := [16]f32 {
// position uv
+1, +1, 1, 1,
+1, -1, 1, 0,
-1, -1, 0, 0,
-1, +1, 0, 1,
}
// odinfmt: enable
display_vertex_buffer := sg.make_buffer(
{
type = .VERTEXBUFFER,
data = as_range(&quad_vertices),
label = "display-vertex-buffer",
},
)
display_index_buffer_vertex := [QUAD_INDEX_SIZE]u16{0, 1, 3, 1, 2, 3}
display_index_buffer := sg.make_buffer(
{
type = .INDEXBUFFER,
data = as_range(&display_index_buffer_vertex),
label = "display-index-buffer",
},
)
display_pipeline := sg.make_pipeline(
{
layout = {
attrs = {
shaders.ATTR_display_vertex_position = {format = .FLOAT2},
shaders.ATTR_display_vertex_uv = {format = .FLOAT2},
},
},
index_type = .UINT16,
shader = sg.make_shader(
shaders.display_shader_desc(sg.query_backend()),
),
depth = {compare = .LESS_EQUAL, write_enabled = true},
label = "display-pipeline",
},
)
display_sampler := sg.make_sampler(
{
min_filter = .NEAREST,
mag_filter = .NEAREST,
label = "display-sampler",
},
)
display_bindings := sg.Bindings {
vertex_buffers = {0 = display_vertex_buffer},
index_buffer = display_index_buffer,
samplers = {shaders.IMG_tex = display_sampler},
// Notice how the refer to the `render_target` here!
// This is the thing we rendered everything to in the offscreen pass.
images = {shaders.SMP_smp = render_target},
}
////////////////////////////////////////////////////////////////////////////
// Store all the things we need in our global struct.
renderer.display = {
pass_action = display_pass_action,
pipeline = display_pipeline,
bindings = display_bindings,
}
// Phew, that's it.
// We have defined all the resources we need for our two render passes!
}
frame :: proc "c" () {
context = runtime.default_context()
////////////////////////////////////////////////////////////////////////////
// Timers & Input
// see `non_renderer_code.odin`
tick()
handle_input()
// Setup resolution scale depending on current display size
dpi_scale := sapp.dpi_scale()
display_width := sapp.widthf()
display_height := sapp.heightf()
resolution_scale := gfx_get_resolution_scaling(
display_width,
display_height,
dpi_scale,
)
////////////////////////////////////////////////////////////////////////////
// Sprite batch
// A "sprite batch" is a big slice of `Sprite_Instance`s which we send to
// our vertex buffer with index 1 (offscreen_instance_buffer).
// This way we can send ALL sprites to the shaders at once.
// This allows to draw several sprites with only one draw call.
// People told me draw calls can be a bottleneck in rendering so only doing
// one sounds good!
// Don't forget to reset the batch every frame!
renderer.sprite_batch.len = 0
// A little helper to animate our sprites with the `frames` we defined.
for &sprite, idx in sprites_to_render {
is_last := idx == len(sprites_to_render) - 1
frames := len(sprite.sprite.frames)
location := sprite.sprite.frames[int(timer.animation) % frames]
// move sprite last in the list at the four times of the speed of the TICK_ANIMATION 200 units to the right and then reset it
x :=
is_last ? sprite.position.x + int(timer.animation * 4) % 200 : sprite.position.x
// This is where the sprite is added to the batch, navigate to this function.
// On could easily do this manually.
// See the documentation of the function!
gfx_draw_sprite(
position = {x, sprite.position.y},
scale = sprite.scale,
color = sprite.color,
location = location,
size = sprite.sprite.size,
sprite_batch = &renderer.sprite_batch,
)
}
// Upload the sprite batch to the GPU!
if renderer.sprite_batch.len > 0 {
sprite_batch := renderer.sprite_batch.instances[:renderer.sprite_batch.len]
sg.update_buffer(
renderer.offscreen.bindings.vertex_buffers[1],
// Notice the `as_range` which is a [Explicit procedure overloading].
// https://odin-lang.org/docs/overview/#explicit-procedure-overloading
as_range(sprite_batch),
)
}
////////////////////////////////////////////////////////////////////////////
// Offscreen rendering pass
// Pass a single uniform struct to the shaders.
// These are the values that will be reused by _all_ our sprites.
// Meaning, they are the same for all sprites, we only need to upload them once.
vertex_shader_uniforms := shaders.Vs_Params {
pixel_to_viewport_multiplier = renderer.offscreen.pixel_to_viewport_multiplier,
sprite_atlas_size = {
f32(renderer.offscreen.sprite_atlas_size.x),
f32(renderer.offscreen.sprite_atlas_size.y),
},
}
// Begin the pass.
sg.begin_pass(renderer.offscreen.pass)
// Apply the pipelines.
sg.apply_pipeline(renderer.offscreen.pipeline)
sg.apply_bindings(renderer.offscreen.bindings)
// Apply the uniforms we declared above.
sg.apply_uniforms(
shaders.UB_vs_params,
{
ptr = &vertex_shader_uniforms,
size = size_of(vertex_shader_uniforms),
},
)
// Do the drawing.
// We do an instanced drawing.
// We draw the 6 item quad we talked about earlier
// Instanced the amount of sprite we have in our sprite batch.
// I am not good at explaning -> https://learnopengl.com/Advanced-OpenGL/Instancing
sg.draw(0, QUAD_INDEX_SIZE, renderer.sprite_batch.len)
// The offscreen pass is over, we have now drawn all of our sprites on the `render_target`.
sg.end_pass()
////////////////////////////////////////////////////////////////////////////
// Display rendering pass
sg.begin_pass(
{
action = renderer.display.pass_action,
// Notice how this pass has a swapchain.
// This means we are no longer drawing to an attached image
// (the `render_target`), we are drawing on the real screen instead!
swapchain = sglue.swapchain(),
label = "display-pass",
},
)
sg.apply_pipeline(renderer.display.pipeline)
sg.apply_bindings(renderer.display.bindings)
// The viewport is the area in which we are drawing.
// Try resizing the window and you will see that the viewport snaps to
// different pixel perfect resolutions (different multipliers of {320, 180}
vp := gfx_get_pixel_perfect_viewport(
display_width,
display_height,
dpi_scale,
resolution_scale,
)
sg.apply_viewport(vp.x, vp.y, vp.z, vp.w, false)
// In comparison to the other draw call - we only draw one instance here.
// We simply draw the image from the offscreen renderer to the newly scaled viewport.
// Our sprites are shown!
sg.draw(0, QUAD_INDEX_SIZE, 1)
sg.end_pass()
// We did it!
sg.commit()
}
////////////////////////////////////////////////////////////////////////////////
// Draw a sprite by adding it to the sprite batch for the upcoming frame
// position: X and Y coordinates with {0, 0} being bottom-left - I like this for platformers, I guess???
// scale: Scale of the sprite being rendered
// color: Color multiplier of the sprite, {255, 255, 255, 1} format
// location: Location in the sprite sheet with {0, 0} being top-left
// size: Size of the area of the sprite sheet to render
gfx_draw_sprite :: proc(
position: [2]int,
scale: [2]i32 = {1, 1},
color: [4]f32 = {255, 255, 255, 1},
location: [2]u16,
size: [2]u16,
sprite_batch: ^Sprite_Batch,
) {
if sprite_batch.len > BUDGET_SPRITES do return
vertex: Sprite_Instance = {
location = {f32(location.x), f32(location.y)},
size = {f32(size.x), f32(size.y)},
position = {f32(position.x), f32(position.y)},
scale = {f32(scale.x), f32(scale.y)},
color = color,
}
sprite_batch.instances[sprite_batch.len] = vertex
sprite_batch.len += 1
}
////////////////////////////////////////////////////////////////////////////////
// Multiplier to convert from from pixel to viewport coordinates
gfx_get_pixel_to_viewport_multiplier :: proc(
display_width, display_height: f32,
) -> [2]f32 {
// some Y-axis flipping to put {0, 0} at the bottom-left
return {2 / display_width, -2 / display_height}
}
////////////////////////////////////////////////////////////////////////////////
// Get viewport size to the largest pixel perfect resolution given game size
gfx_get_pixel_perfect_viewport :: proc(
display_width, display_height, dpi_scale: f32,
resolution_scale: u16,
) -> [4]f32 {
width := display_width / dpi_scale
height := display_height / dpi_scale
game_width := GAME_WIDTH * f32(resolution_scale)
game_height := GAME_HEIGHT * f32(resolution_scale)
vp_x := dpi_scale * (width - game_width) / 2
vp_y := dpi_scale * (height - game_height) / 2
vp_w := dpi_scale * game_width
vp_h := dpi_scale * game_height
return {vp_x, vp_y, vp_w, vp_h}
}
////////////////////////////////////////////////////////////////////////////////
// Get the largest possible resolution scaling based on display and GAME size
// For example running on a 1440p monitor will result in a resolution scaling of
// 8 x 180 = 1440 -> 8
gfx_get_resolution_scaling :: proc(
display_width, display_height, dpi_scale: f32,
) -> u16 {
width := display_width / dpi_scale
height := display_height / dpi_scale
display_aspect := width / height
offscreen_aspect := f32(GAME_WIDTH / GAME_HEIGHT)
res :=
u16(height / GAME_HEIGHT) if offscreen_aspect < display_aspect else u16(width / GAME_WIDTH)
return res if res > 1 else 1
}
////////////////////////////////////////////////////////////////////////////////
// Convert common types to sokol_gfx Range
// https://odin-lang.org/docs/overview/#explicit-procedure-overloading
as_range :: proc {
slice_as_range,
dynamic_array_as_range,
array_ptr_as_range,
}
// https://odin-lang.org/docs/overview/#parametric-polymorphism
// https://odin-lang.org/docs/overview/#calling-conventions
slice_as_range :: proc "contextless" (val: $T/[]$E) -> (range: sg.Range) {
range.ptr = raw_data(val)
range.size = c.size_t(len(val)) * size_of(E)
return
}
dynamic_array_as_range :: proc "contextless" (
val: $T/[dynamic]$E,
) -> (
range: sg.Range,
) {
range.ptr = raw_data(val)
range.size = u64(len(val)) * size_of(E)
return
}
array_ptr_as_range :: proc "contextless" (
val: ^$T/[$N]$E,
) -> (
range: sg.Range,
) {
range.ptr = raw_data(val)
range.size = c.size_t(len(val)) * size_of(E)
return
}
main :: proc() {
sapp.run(
{
init_cb = init,
frame_cb = frame,
event_cb = event,
high_dpi = true,
window_title = "odin-sprite-renderer",
logger = {func = slog.func},
},
)
}