Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ jobs:

odin check sdl3/microui $FLAGS
odin check sdl3/gpu $FLAGS
odin check sdl3/callback-appmodel $FLAGS

odin check simd/approaches $FLAGS
odin check simd/basic-sum $FLAGS
Expand Down
43 changes: 43 additions & 0 deletions sdl3/callback-appmodel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Callback Appmodel example
=========================

Implements SDL3's callback based application model in Odin.

The control flow goes like this:

```
+ Odin main
|
+---+> sdl3.RunApp (external)
|
+---+> SDL_AppMain
|
+---+> sdl3.EnterAppMainCallbacks(SDL_AppInit, SDLAppIterate, SDL_AppEvent, SDL_AppQuit) (external)
|
+---> SDL_AppInit
|
+---> SDL_AppIterate
+---> SDL_AppEvent
+---> SDL_AppEvent
+---> SDL_AppIterate
+---> SDL_AppIterate
...
|
+---> SDL_AppQuit
|
+---+
|
+---+
|
+---+
|
+
```

The rationale behind this application model is that newer platforms, especially mobile, diverge
from the classic "program has one main function and when it returns, it's over" application model.
This gives the OS more control over the resources an application uses, which can be important
for power management and multitasking.

The purpose behind this architecture is to keep the application's main driver code the same
between different platforms.
Binary file added sdl3/callback-appmodel/bill32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
245 changes: 245 additions & 0 deletions sdl3/callback-appmodel/main.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package sdl3_callback_appmodel_example

import "base:runtime"
import "core:fmt"
import "core:log"
import "core:os"
import "vendor:sdl3"
import "core:c"
import queue "core:container/queue"

main :: proc() {
result := sdl3.RunApp(0, nil, SDL_AppMain, nil)
os.exit(cast(int)result)
}

SDL_AppMain :: proc "c" (argc: c.int, argv: [^]cstring) -> c.int {
return sdl3.EnterAppMainCallbacks(argc, argv, SDL_AppInit, SDL_AppIterate, SDL_AppEvent, SDL_AppQuit)
}

App_State :: struct {
runtime_context: runtime.Context,
window: ^sdl3.Window,
renderer: ^sdl3.Renderer,
bill_texture: ^sdl3.Texture,
window_size_valid: bool,
window_size: [2]i32,
iterate_time: SDL_Time,
keyboard: []bool,
position: [2]f32,
}

DEFAULT_CANVAS_W :: 800
DEFAULT_CANVAS_H :: 600

SDL_AppInit :: proc "cdecl" (appstate: ^rawptr, argc: c.int, argv: [^]cstring) -> sdl3.AppResult {

context = runtime.default_context()
context.logger = log.create_console_logger()

state := new(App_State)
assert(state != nil)
state.runtime_context = context
appstate^ = state

assert(sdl3.Init({ .VIDEO }))

state.window = sdl3.CreateWindow("SDL3 Callback AppModel example", DEFAULT_CANVAS_W, DEFAULT_CANVAS_H, {.RESIZABLE})
assert(state.window != nil)

state.renderer = sdl3.CreateRenderer(state.window, sdl3.SOFTWARE_RENDERER)
assert(state.renderer != nil)

assert(sdl3.SetRenderVSync(state.renderer, 1))

state.bill_texture = load_texture_from_path(state.renderer, "bill32.png")
assert(state.bill_texture != nil)

return .CONTINUE
}

SDL_AppIterate :: proc "cdecl" (appstate: rawptr) -> sdl3.AppResult {

state := cast(^App_State)appstate
assert_contextless(state != nil)
context = state.runtime_context

statistics_sw := get_time()

// Process window changes.

if !state.window_size_valid {
w, h : c.int
assert(sdl3.GetWindowSizeInPixels(state.window, &w, &h))
state.window_size = { w, h }
// Resize the viewport.
state.window_size_valid = true
statistics_record_time(.window, &statistics_sw)
}

// Accumulate elapsed game time (RTC).

now := get_time()
elapsed_duration := get_duration(state.iterate_time, now)
elapsed_ms := duration_to_ms32(elapsed_duration)
state.iterate_time = now
statistics_record_time(.timing, &statistics_sw)

// Capture keyboard input state.

num_keys : c.int
keyboard_state := sdl3.GetKeyboardState(&num_keys)
assert(keyboard_state != nil)
state.keyboard = keyboard_state[:num_keys]
statistics_record_time(.input, &statistics_sw)

// Apply keyboard input

SPEED_PX_PER_MS :: 0.1
if state.keyboard[sdl3.Scancode.LEFT] {
state.position.x -= SPEED_PX_PER_MS * elapsed_ms
}
if state.keyboard[sdl3.Scancode.UP] {
state.position.y -= SPEED_PX_PER_MS * elapsed_ms
}
if state.keyboard[sdl3.Scancode.RIGHT] {
state.position.x += SPEED_PX_PER_MS * elapsed_ms
}
if state.keyboard[sdl3.Scancode.DOWN] {
state.position.y += SPEED_PX_PER_MS * elapsed_ms
}
statistics_record_time(.update_position, &statistics_sw)

// Clear background

sdl3.SetRenderDrawColor(state.renderer, 0, 0, 0, 0)
sdl3.RenderClear(state.renderer)
statistics_record_time(.render_clear, &statistics_sw)

// Calculate bill position
// state.position == (0, 0) means that bill is in the center of the window.

texture_w, texture_h: f32
sdl3.GetTextureSize(state.bill_texture, &texture_w, &texture_h)

render_x := cast(f32)state.window_size.x / 2.0 - texture_w / 2 + state.position.x
render_y := cast(f32)state.window_size.y / 2.0 - texture_h / 2 + state.position.y

// Render bill

sdl3.RenderTexture(state.renderer, state.bill_texture, nil, &sdl3.FRect{ render_x, render_y, texture_w, texture_h })
statistics_record_time(.render_texture, &statistics_sw)

statistics_render(state.renderer)
statistics_record_time(.render_statistics, &statistics_sw)

sdl3.RenderPresent(state.renderer)
statistics_record_time(.present, &statistics_sw)

return .CONTINUE
}

SDL_AppEvent :: proc "cdecl" (appstate: rawptr, event: ^sdl3.Event) -> sdl3.AppResult {

state := cast(^App_State)appstate
assert_contextless(state != nil)
context = state.runtime_context

sw := get_time()

#partial switch event.type {

case .QUIT:
return .SUCCESS

case .KEY_DOWN:
if event.key.scancode == .ESCAPE {
// Quit application.
return .SUCCESS
}

case .WINDOW_RESIZED:
state.window_size_valid = false
}

statistics_record_time(.event, &sw)

return .CONTINUE
}

SDL_AppQuit :: proc "cdecl" (appstate: rawptr, result: sdl3.AppResult) {

state := cast(^App_State)appstate
assert_contextless(state != nil)
context = state.runtime_context

sdl3.DestroyTexture(state.bill_texture)
state.bill_texture = nil
sdl3.DestroyRenderer(state.renderer)
state.renderer = nil
sdl3.DestroyWindow(state.window)
state.window = nil

free(state)
}

// Timestamp in SDL time (nanosecond precision).
SDL_Time :: distinct u64
// Duration in nanoseconds.
SDL_Duration :: distinct i64

@(require_results)
get_time :: #force_inline proc "contextless" () -> SDL_Time {
return cast(SDL_Time)sdl3.GetTicksNS()
}

@(require_results)
get_duration :: #force_inline proc "contextless" (start, end : SDL_Time) -> SDL_Duration {
return cast(SDL_Duration)end - cast(SDL_Duration)start
}

@(require_results)
get_duration_ms32 :: #force_inline proc "contextless" (start, end : SDL_Time) -> f32 {
return cast(f32)(end - start) / cast(f32)sdl3.NS_PER_MS
}

@(require_results)
duration_from_ms :: #force_inline proc "contextless" (ms : f64) -> SDL_Duration {
return cast(SDL_Duration)(ms * sdl3.NS_PER_MS)
}

@(require_results)
duration_to_ms :: #force_inline proc "contextless" (duration : SDL_Duration) -> f64 {
return cast(f64)duration / cast(f64)sdl3.NS_PER_MS
}

@(require_results)
duration_to_ms32 :: #force_inline proc "contextless" (duration : SDL_Duration) -> f32 {
return cast(f32)duration / cast(f32)sdl3.NS_PER_MS
}

load_texture_from_path :: proc(renderer: ^sdl3.Renderer, path: cstring) -> ^sdl3.Texture {
surface := sdl3.LoadPNG(path)
assert(surface != nil)
texture := sdl3.CreateTextureFromSurface(renderer, surface)
assert(texture != nil)
return texture
}

statistics_record_time :: proc(id: Statistic, sw: ^SDL_Time) {
now := get_time()
ms := get_duration_ms32(sw^, now)
sw^ = now
statistics_record(id, ms)
}

statistics_render :: proc(renderer: ^sdl3.Renderer) {
sdl3.SetRenderDrawColor(renderer, 0, 255, 255, 255)
for i in 0 ..< len(statistics) {
name := statistics[i].name
avg := statistics[i].sum_in_queue / cast(f32)queue.len(statistics[i].queue)
max := statistics[i].max
s := fmt.tprintf("%30v: Avg %.2f - Max %.2f", name, avg, max)
sdl3.RenderDebugText(renderer, 0, cast(f32)(i * 10), auto_cast raw_data(s))
}
}
70 changes: 70 additions & 0 deletions sdl3/callback-appmodel/statistics.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package sdl3_callback_appmodel_example

import reflect "core:reflect"
import slice "core:slice"
import queue "core:container/queue"
import fmt "core:fmt"

Statistic :: enum {
window,
timing,
input,
render_statistics,
present,
event,

update_position,
render_clear,
render_texture,
}

Statistic_Sample :: struct {
name: string,
queue: queue.Queue(f32),
sum_in_queue: f32,
max: f32,
}

STATISTICS_QUEUE_SIZE :: 100

statistics : [dynamic]Statistic_Sample

statistics_get_associative_array_length :: proc() -> int {
values := reflect.enum_field_values(Statistic)
max, ok := slice.max(values)
if !ok {
return 0
}
return cast(int)max + 1
}

statistics_record :: proc(id : Statistic, value : f32) {
if statistics == nil {
length := statistics_get_associative_array_length()
statistics = make([dynamic]Statistic_Sample, length)
for i in 0 ..< length {
statistics[i].name = reflect.enum_string(cast(Statistic)i)
queue.init(&statistics[i].queue, STATISTICS_QUEUE_SIZE)
}
}
sample := &statistics[transmute(int)id]
for queue.len(sample.queue) >= STATISTICS_QUEUE_SIZE {
popped := queue.dequeue(&sample.queue)
sample.sum_in_queue -= popped
}
queue.enqueue(&sample.queue, value)
sample.sum_in_queue += value
if value > sample.max {
sample.max = value
}
}

statistics_print :: proc() {
fmt.println("Statistics:")
for i in 0 ..< len(statistics) {
name := statistics[i].name
avg := statistics[i].sum_in_queue / cast(f32)queue.len(statistics[i].queue)
max := statistics[i].max
fmt.printfln(" %v: Avg %.2f - Max %.2f", name, avg, max)
}
}
Loading