From 3a1f9fe6800ef5fe7bbf1c0a5976720383ba84fe Mon Sep 17 00:00:00 2001 From: Joel Stålnacke Date: Mon, 23 Mar 2026 19:50:18 +0200 Subject: Rewrite native code in Odin and use ECS --- Djup.Native/src/Djup.Native.csproj | 2 +- Djup.Native/src/LibDjup.cs | 74 ++++++----- Djup.Native/src/Types.cs | 126 ++++++++++++------ Djup/src/Program.fs | 136 ++++++++++--------- lib/.gitignore | 2 +- lib/build.sh | 29 ++++- lib/common.h | 1 - lib/common.odin | 24 ++++ lib/components.odin | 18 +++ lib/constants.h | 12 -- lib/log.c | 55 -------- lib/log.h | 23 ---- lib/log.odin | 116 +++++++++++++++++ lib/ols.json | 13 ++ lib/physics.c | 146 --------------------- lib/physics.h | 17 --- lib/player.c | 36 ----- lib/player.h | 5 - lib/simulation.odin | 261 +++++++++++++++++++++++++++++++++++++ lib/snapshot.c | 142 -------------------- lib/snapshot.h | 43 ------ lib/systems.odin | 44 +++++++ lib/test.c | 24 ---- lib/types.h | 21 --- lib/vec2.c | 68 ---------- 25 files changed, 702 insertions(+), 736 deletions(-) delete mode 100644 lib/common.h create mode 100644 lib/common.odin create mode 100644 lib/components.odin delete mode 100644 lib/constants.h delete mode 100644 lib/log.c delete mode 100644 lib/log.h create mode 100644 lib/log.odin create mode 100644 lib/ols.json delete mode 100644 lib/physics.c delete mode 100644 lib/physics.h delete mode 100644 lib/player.c delete mode 100644 lib/player.h create mode 100644 lib/simulation.odin delete mode 100644 lib/snapshot.c delete mode 100644 lib/snapshot.h create mode 100644 lib/systems.odin delete mode 100644 lib/test.c delete mode 100644 lib/types.h delete mode 100644 lib/vec2.c diff --git a/Djup.Native/src/Djup.Native.csproj b/Djup.Native/src/Djup.Native.csproj index a581a58..f2a149b 100644 --- a/Djup.Native/src/Djup.Native.csproj +++ b/Djup.Native/src/Djup.Native.csproj @@ -9,7 +9,7 @@ - $(MSBuildThisFileDirectory)../../lib/build + $(MSBuildThisFileDirectory)../../lib/output diff --git a/Djup.Native/src/LibDjup.cs b/Djup.Native/src/LibDjup.cs index 4cd99f8..864f330 100644 --- a/Djup.Native/src/LibDjup.cs +++ b/Djup.Native/src/LibDjup.cs @@ -5,50 +5,62 @@ namespace Djup.Native; public static partial class LibDjup { const string LibraryName = "djup"; - const string Prefix = "dp_"; - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(hello_world))] - public static partial void hello_world(); + [LibraryImport(LibraryName, EntryPoint = nameof(vec2_dot))] + public static partial Vec2 vec2_dot(Vec2 a, Vec2 b); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(get_num))] - public static partial int get_num(); + [LibraryImport(LibraryName, EntryPoint = nameof(vec2_length))] + public static partial float vec2_length(Vec2 v); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(get_vec2))] - public static partial Vec2 get_vec2(); + [LibraryImport(LibraryName, EntryPoint = nameof(vec2_normal))] + public static partial Vec2 vec2_normal(Vec2 v); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(vec2_mul))] - public static partial Vec2 vec2_mul(Vec2 v, float scalar); + [LibraryImport(LibraryName, EntryPoint = nameof(sim_log_init))] + public static partial void sim_log_init(IntPtr simulation, [MarshalAs(UnmanagedType.FunctionPtr)] LoggerDelegate logger, [MarshalAs(UnmanagedType.FunctionPtr)] AssertDelegate assert); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(vec2_normal))] - public static partial Vec2 vec2_normal(Vec2 v); + [LibraryImport(LibraryName, EntryPoint = nameof(sim_set_log_level))] + public static partial void sim_set_log_level(IntPtr simulation, LogLevel minLevel); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(vec2_length))] - public static partial float vec2_length(Vec2 v); + [LibraryImport(LibraryName, EntryPoint = nameof(sim_create))] + public static partial IntPtr sim_create(int snapshots, int entities); + + [LibraryImport(LibraryName, EntryPoint = nameof(sim_free))] + public static partial void sim_free(IntPtr simulation); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(log_set_output))] - public static partial void log_set_output([MarshalAs(UnmanagedType.FunctionPtr)] LoggerDelegate logger); + [LibraryImport(LibraryName, EntryPoint = nameof(sim_get_snapshot))] + public unsafe static partial Snapshot* sim_get_snapshot(IntPtr simulation); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(log_set_level))] - public static partial void log_set_level(LogLevel minLevel); + [LibraryImport(LibraryName, EntryPoint = nameof(sim_return_snapshot))] + public unsafe static partial void sim_return_snapshot(IntPtr simulation, Snapshot* snapshot); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(physics_context_create))] - public static partial IntPtr physics_context_create(int snapshots, int players, int balls); + [LibraryImport(LibraryName, EntryPoint = nameof(sim_tick))] + public unsafe static partial void sim_tick(Snapshot* current, Snapshot* next); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(physics_context_free))] - public static partial void physics_context_free(IntPtr context); + [LibraryImport(LibraryName, EntryPoint = nameof(snapshot_create_entity))] + public unsafe static partial EntityId snapshot_create_entity(Snapshot* snapshot); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(physics_context_get_snapshot))] - public static partial IntPtr physics_context_get_snapshot(IntPtr context); + public enum ComponentAddError + { + None, + InvalidArg, + InvalidId, + EntityNotFound + } - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(physics_context_return_snapshot))] - public static partial void physics_context_return_snapshot(IntPtr context, IntPtr snapshot); + [LibraryImport(LibraryName, EntryPoint = nameof(snapshot_add_component))] + public unsafe static partial ComponentAddError snapshot_add_component(Snapshot* snapshot, EntityId entity, Component component, void* init); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(physics_tick))] - public static partial int physics_tick(IntPtr currentSnapshot, double delta, IntPtr nextSnapshot); + public enum ComponentRemoveError + { + None, + InvalidArg, + InvalidId, + EntityNotFound + } - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(snapshot_put_player))] - public static unsafe partial int snapshot_put_player(IntPtr snapshot, int playerId, Player* player); + [LibraryImport(LibraryName, EntryPoint = nameof(snapshot_remove_components))] + public unsafe static partial ComponentRemoveError snapshot_remove_components(Snapshot* snapshot, EntityId entity, Components components); - [LibraryImport(LibraryName, EntryPoint = Prefix + nameof(snapshot_set_player_input))] - public static partial int snapshot_set_player_input(IntPtr snapshot, int playerId, byte input); + [LibraryImport(LibraryName, EntryPoint = nameof(snapshot_reset_entity))] + public unsafe static partial ComponentRemoveError snapshot_reset_entity(Snapshot* snapshot, EntityId entity); } diff --git a/Djup.Native/src/Types.cs b/Djup.Native/src/Types.cs index 3afce83..e9eb824 100644 --- a/Djup.Native/src/Types.cs +++ b/Djup.Native/src/Types.cs @@ -3,11 +3,85 @@ using System.Runtime.InteropServices; namespace Djup.Native; +[StructLayout(LayoutKind.Sequential)] +public struct EntityId +{ + public int Id { get; } + + public static readonly EntityId Invalid = new(-1); + + public EntityId(int id) + { + Id = id; + } + + public bool IsInvalid => Id < 0; + + public override string ToString() => $"#{Id}"; + + public override bool Equals(object? other) + { + if (other is EntityId id) + { + return this == id; + } + return false; + } + + public override int GetHashCode() => Id.GetHashCode(); + + public static bool operator ==(EntityId a, EntityId b) + { + if (a < 0) + return b < 0; + if (b < 0) + return false; + return a.Id == b.Id; + } + public static bool operator !=(EntityId a, EntityId b) => !(a == b); + public static implicit operator int(EntityId entityId) => entityId.Id; + public static implicit operator EntityId(int id) => new(id); +} + +public enum Component : int +{ + Exists, + Ball, + PlayerInput +} + +[Flags] +public enum Components : int +{ + Exists = 1 << Component.Exists, + Ball = 1 << Component.Ball, + PlayerInput = 1 << Component.PlayerInput +} + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct OdinSlice +{ + public void* Data { get; } + public nint Len { get; } + + public Span AsSpan() => new Span(Data, Len.ToInt32()); + public ReadOnlySpan AsReadOnlySpan() => new ReadOnlySpan(Data, Len.ToInt32()); +} + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct OdinString +{ + public sbyte* Data { get; } + public nint Len { get; } + + public override string ToString() => new string(Data, 0, Len.ToInt32()); +} + [StructLayout(LayoutKind.Sequential)] public struct Vec2 { - public float X { get; } - public float Y { get; } + public float X { get; set; } + public float Y { get; set; } public Vec2(float x, float y) { @@ -21,59 +95,32 @@ public struct Vec2 public override string ToString() => $"({X}, {Y})"; } -public enum EntityState : byte -{ - Inactive = 0, - Active, - Removed, - Dead -} - -public enum PlayerInput : byte -{ - Up = 1 << 0, - Down = 1 << 1, - Left = 1 << 2, - Right = 1 << 3 -} - [StructLayout(LayoutKind.Sequential)] -public struct Player +public struct Ball { - public EntityState State { get; } - private byte input; public Vec2 Position { get; set; } public Vec2 Velocity { get; set; } } [StructLayout(LayoutKind.Sequential)] -public struct Bullet +public struct PlayerInput { - public EntityState State { get; set; } - public Vec2 Position { get; set; } - public Vec2 Velocity { get; set; } + public Vec2 Direction { get; set; } } [StructLayout(LayoutKind.Sequential)] -public unsafe struct Snapshot +public struct Snapshot { - private byte active; + private readonly byte active; public uint Tick { get; } - private int playersLength; - private Player* players; - private int bulletsLength; - private Bullet* bullets; - - public ReadOnlySpan Players => - new ReadOnlySpan(players, playersLength); - - public ReadOnlySpan Bullets => - new ReadOnlySpan(bullets, bulletsLength); + public OdinSlice Metadata { get; } + public OdinSlice Balls { get; } + public OdinSlice PlayerInputs { get; } + private IntPtr ctx; } public enum LogLevel { - Trace, Debug, Info, Warn, @@ -81,4 +128,5 @@ public enum LogLevel Fatal } -public delegate void LoggerDelegate(LogLevel level, string source, int line, string message); +public delegate void LoggerDelegate(LogLevel level, OdinString message, OdinString procedure, OdinString file, int line); +public delegate void AssertDelegate(OdinString prefix, OdinString message, OdinString procedure, OdinString file, int line); diff --git a/Djup/src/Program.fs b/Djup/src/Program.fs index 45dea91..c0c7e9f 100644 --- a/Djup/src/Program.fs +++ b/Djup/src/Program.fs @@ -1,5 +1,6 @@ module Djup.Program +open System open FSharp.NativeInterop open Microsoft.Xna.Framework open Microsoft.Xna.Framework.Graphics @@ -27,9 +28,11 @@ let inputMap = |> InputMap.key MoveDown Keys.S type Model = - { PhysicsContext: nativeint - CurrentSnapshot: nativeint - PlayerId: int } + { SimulationContext: nativeint + CurrentSnapshot: Snapshot nativeptr + PlayerId: EntityId + LogFunc: LoggerDelegate + AssertFunc: AssertDelegate } type Msg = | PhysicsTick of float32 @@ -37,82 +40,82 @@ type Msg = | InputChanged of ActionState let init (_: GameContext) : struct (Model * Cmd) = - LibDjup.log_set_output ( - LoggerDelegate(fun level source line message -> - let level = - match level with - | LogLevel.Trace -> "TRACE" - | LogLevel.Debug -> "DEBUG" - | LogLevel.Info -> "INFO " - | LogLevel.Warn -> "WARN " - | LogLevel.Error -> "ERROR" - | LogLevel.Fatal -> "FATAL" - | _ -> sprintf "UNKNOWN(%d)" (int level) - - printfn "libdjup %s %s:%d: %s" level source line message) - ) - - let physics = LibDjup.physics_context_create (2, 1, 1000) - - if physics = 0 then - failwith "Failed to allocate physics context" - - let snapshot = LibDjup.physics_context_get_snapshot physics - - let putPlayer playerId player = - use ptr = fixed &player - LibDjup.snapshot_put_player (snapshot, playerId, ptr) + let ctx = LibDjup.sim_create (2, 1000) - let playerId = 0 + if ctx = 0 then + failwith "Failed to allocate physics context" - match putPlayer playerId (Player()) with - | 0 -> () - | errorCode -> failwithf "Failed to put player (error code %d)" errorCode + let logFunc = + LoggerDelegate( + fun level message procedure file line -> + let level = + match level with + | LogLevel.Debug -> "DEBUG" + | LogLevel.Info -> "INFO " + | LogLevel.Warn -> "WARN " + | LogLevel.Error -> "ERROR" + | LogLevel.Fatal -> "FATAL" + | _ -> sprintf "UNKNOWN(%d)" (int level) + + printfn "libdjup %s %O:%O:%d: %O" level procedure file line message) + let assertFunc = AssertDelegate( + fun prefix message procedure file line -> + printfn "libdjup ASSERT %O:%O:%d: %O %O" procedure file line prefix message + Environment.Exit 1) + + LibDjup.sim_log_init(ctx, logFunc, assertFunc) + + let snapshot = LibDjup.sim_get_snapshot ctx + + let playerId = + match LibDjup.snapshot_create_entity snapshot with + | id when id.IsInvalid -> failwith "Failed to create player" + | id -> id + + let ball = Ball() + use ptr = fixed &ball + LibDjup.snapshot_add_component(snapshot, playerId, Component.Ball, ptr |> NativePtr.toVoidPtr) + let playerInput = PlayerInput() + use ptr = fixed &playerInput + LibDjup.snapshot_add_component(snapshot, playerId, Component.PlayerInput, ptr |> NativePtr.toVoidPtr) let model = - { PhysicsContext = physics + { SimulationContext = ctx CurrentSnapshot = snapshot - PlayerId = playerId } + PlayerId = playerId + LogFunc = logFunc + AssertFunc = assertFunc } model, Cmd.none let update (msg: Msg) (model: Model) : struct (Model * Cmd) = match msg with - | PhysicsTick delta -> - let next = LibDjup.physics_context_get_snapshot model.PhysicsContext - - match LibDjup.physics_tick (model.CurrentSnapshot, double delta, next) with - | 0 -> - LibDjup.physics_context_return_snapshot (model.PhysicsContext, model.CurrentSnapshot) - { model with CurrentSnapshot = next }, Cmd.none - | errorCode -> - printfn "Physics tick failed, code %d" errorCode - LibDjup.physics_context_return_snapshot (model.PhysicsContext, next) - model, Cmd.none + | PhysicsTick _ -> + let next = LibDjup.sim_get_snapshot model.SimulationContext + LibDjup.sim_tick (model.CurrentSnapshot, next) + LibDjup.sim_return_snapshot (model.SimulationContext, model.CurrentSnapshot) + { model with CurrentSnapshot = next }, Cmd.none | InputChanged actionState -> // TODO: Actions get applied for a long time, a small press causes the // player to slide in the pressed direction for a while - let mutable input = 0uy + let mutable input = Vec2() let isHeld action = actionState.Held.Contains action if isHeld MoveUp then - input <- input ||| byte PlayerInput.Up + input.Y <- input.Y - 1.0f if isHeld MoveDown then - input <- input ||| byte PlayerInput.Down + input.Y <- input.Y + 1.0f if isHeld MoveLeft then - input <- input ||| byte PlayerInput.Left + input.X <- input.X - 1.0f if isHeld MoveRight then - input <- input ||| byte PlayerInput.Right - - let ret = - LibDjup.snapshot_set_player_input (model.CurrentSnapshot, model.PlayerId, input) + input.X <- input.X + 1.0f - match ret with - | 0 -> () - | errorCode -> printfn "Failed to set player input (error code %d)" errorCode + let snapshot = NativePtr.toByRef model.CurrentSnapshot + let span = snapshot.PlayerInputs.AsSpan() + span[model.PlayerId].Direction <- input model, Cmd.none | Tick _ -> model, Cmd.none @@ -128,15 +131,18 @@ let view (ctx: GameContext) (model: Model) (buffer: RenderBuffer) = t) ctx - let snapshot: Snapshot = - let ptr = model.CurrentSnapshot |> NativePtr.ofNativeInt - NativePtr.get ptr 0 - - // TODO: Snapshot.Players should be active players - for p in snapshot.Players do - Draw2D.sprite pixel (Rectangle(int p.Position.X, int p.Position.Y, 32, 32)) - |> Draw2D.withColor Color.Red - |> Draw2D.submit buffer + let snapshot = NativePtr.toByRef model.CurrentSnapshot + + let mask = Components.Exists ||| Components.Ball + let metadata = snapshot.Metadata.AsReadOnlySpan() + let balls = snapshot.Balls.AsReadOnlySpan() + for i = 0 to metadata.Length - 1 do + if metadata[i] &&& mask = mask then + let pos = balls[i].Position + printfn "%O: %O" (EntityId i) pos + Draw2D.sprite pixel (Rectangle(int pos.X, int pos.Y, 32, 32)) + |> Draw2D.withColor Color.Red + |> Draw2D.submit buffer [] diff --git a/lib/.gitignore b/lib/.gitignore index 567609b..ea1472e 100644 --- a/lib/.gitignore +++ b/lib/.gitignore @@ -1 +1 @@ -build/ +output/ diff --git a/lib/build.sh b/lib/build.sh index 3f8b5a6..1695dfe 100755 --- a/lib/build.sh +++ b/lib/build.sh @@ -5,11 +5,9 @@ fail() { exit 1 } -CC=cc -CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Og -g -D_POSIX_C_SOURCE=200809L" -LDFLAGS="-fPIC -lm" -SOURCES="test.c vec2.c snapshot.c log.c player.c physics.c" -output="build" +ODIN_FLAGS="-vet -warnings-as-errors -reloc-mode=pic" + +output="output" for arg ; do case "$arg" in @@ -19,6 +17,15 @@ for arg ; do --output=*) output="${arg#*=}" ;; + --release) + release=1 + ;; + --client) + ODIN_FLAGS="$ODIN_FLAGS -define:DJUP_CLIENT=true" + ;; + --server) + ODIN_FLAGS="$ODIN_FLAGS -define:DJUP_SERVER=true" + ;; *) echo "usage" ;; @@ -42,10 +49,20 @@ fi case "$runtime" in linux-x64) sofile="libdjup.so" + target="linux_amd64" ;; *) fail "Unsupported runtime $runtime" esac +if [ ! -z "$release" ]; then + optimize="-o=speed" +else + optimize="-o=none" +fi + echo Building for runtime "$runtime ..." -mkdir -p "$output/$runtime" && $CC $CFLAGS $SOURCES -shared $LDFLAGS -o "$output/$runtime/$sofile" && echo Done +echo ODIN_FLAGS=$ODIN_FLAGS +mkdir -p "$output/$runtime" && \ + odin build . -build-mode=shared -out="$output/$runtime/$sofile" $ODIN_FLAGS $optimize && \ + echo Done diff --git a/lib/common.h b/lib/common.h deleted file mode 100644 index d6d8816..0000000 --- a/lib/common.h +++ /dev/null @@ -1 +0,0 @@ -#define LENGTH(x) (sizeof(x) / sizeof(x[0])) diff --git a/lib/common.odin b/lib/common.odin new file mode 100644 index 0000000..3c6679f --- /dev/null +++ b/lib/common.odin @@ -0,0 +1,24 @@ +package djup + +import "core:math" + +vec2 :: [2]f32 + +vec2_dot :: proc(a, b: vec2) -> vec2 { + return vec2{a.x * b.x, a.y * b.y} +} + +vec2_length :: proc(v: vec2) -> f32 { + if v == (vec2{0, 0}) { + return 0 + } + return math.sqrt(math.pow(v.x, 2) + math.pow(v.y, 2)) +} + +vec2_normal :: proc(v: vec2) -> vec2 { + len := vec2_length(v) + if len == 0 { + return vec2{0, 0} + } + return v * (1 / len) +} diff --git a/lib/components.odin b/lib/components.odin new file mode 100644 index 0000000..56407a3 --- /dev/null +++ b/lib/components.odin @@ -0,0 +1,18 @@ +package djup + +Component :: enum i32 { + Exists, + Ball, + PlayerInput, +} + +Components :: bit_set[Component;i32] + +Ball :: struct { + position: vec2, + velocity: vec2, +} + +PlayerInput :: struct { + direction: vec2, +} diff --git a/lib/constants.h b/lib/constants.h deleted file mode 100644 index 817fa43..0000000 --- a/lib/constants.h +++ /dev/null @@ -1,12 +0,0 @@ -#define WORLD_MAX_PLAYERS 5 -#define WORLD_MAX_BULLETS 5000 - -#define PLAYER_ACCELERATION 150.0f -#define PLAYER_DRAG 2.0f - -enum { - INPUT_UP = 1 << 0, - INPUT_DOWN = 1 << 1, - INPUT_LEFT = 1 << 2, - INPUT_RIGHT = 1 << 3 -}; diff --git a/lib/log.c b/lib/log.c deleted file mode 100644 index c45f735..0000000 --- a/lib/log.c +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include -#include - -#include "log.h" - -/* Message buffer */ -#define MBUF_SIZE 512 - -static void (*log_output)(int level, const char *source, int line, const char *msg) = NULL; -static int min_level = LOGL_INFO; - -void -dp_log_set_output(void (*output)(int, const char *, int, const char *)) -{ - log_output = output; -} - -void -dp_log_set_level(int level) -{ - min_level = level; -} - -void -dp_log_write(int level, const char *source, int line, const char *fmt, ...) -{ - char mbuf[MBUF_SIZE]; - char *msg; - va_list va; - int len; - - if (level < min_level) - return; - if (!log_output) - return; - - msg = mbuf; - va_start(va, fmt); - len = vsnprintf(msg, MBUF_SIZE, fmt, va); - va_end(va); - - if (len >= MBUF_SIZE) - { - msg = (char *)malloc(len + 1); - va_start(va, fmt); - len = vsnprintf(msg, len + 1, fmt, va); - va_end(va); - } - - log_output(level, source, line, msg); - - if (msg != mbuf) - free(msg); -} diff --git a/lib/log.h b/lib/log.h deleted file mode 100644 index e1cdcb8..0000000 --- a/lib/log.h +++ /dev/null @@ -1,23 +0,0 @@ -#include - -enum { LOGL_TRACE, LOGL_DEBUG, LOGL_INFO, LOGL_WARN, LOGL_ERROR, LOGL_FATAL }; - -/* - * log_info("Hello %s!", "World"); - * log_warn("warning"); - */ -#define log_trace(...) dp_log_write(LOGL_TRACE, __FILE__, __LINE__, __VA_ARGS__) -#define log_debug(...) dp_log_write(LOGL_DEBUG, __FILE__, __LINE__, __VA_ARGS__) -#define log_info(...) dp_log_write(LOGL_INFO, __FILE__, __LINE__, __VA_ARGS__) -#define log_warn(...) dp_log_write(LOGL_WARN, __FILE__, __LINE__, __VA_ARGS__) -#define log_error(...) dp_log_write(LOGL_ERROR, __FILE__, __LINE__, __VA_ARGS__) -#define log_fatal(...) dp_log_write(LOGL_FATAL, __FILE__, __LINE__, __VA_ARGS__) - -void dp_log_set_output(void (*)(int level, const char *source, int line, const char *msg)); - -/* - * Sets the minimum log level. Level is inclusive. - */ -void dp_log_set_level(int level); - -void dp_log_write(int level, const char *source, int line, const char *fmt, ...); diff --git a/lib/log.odin b/lib/log.odin new file mode 100644 index 0000000..eb80b5a --- /dev/null +++ b/lib/log.odin @@ -0,0 +1,116 @@ +package djup + +import "base:runtime" +import "core:c/libc" +import "core:fmt" +import "core:log" + +LogLevel :: enum i32 { + Debug = 0, + Info = 1, + Warning = 2, + Error = 3, + Fatal = 4, +} + +LogFunc :: #type proc "c" ( + level: LogLevel, + message: string, + source_procedure: string, + source_file: string, + source_line: i32, +) + +AssertFunc :: #type proc "c" ( + prefix, message: string, + source_procedure: string, + source_file: string, + source_line: i32, +) + +@(export = true) +sim_log_init :: proc "c" (ctx: ^SimulationContext, logf: LogFunc, assertf: AssertFunc) { + if ctx == nil { + return + } + + log_proc :: proc( + data: rawptr, + level: runtime.Logger_Level, + msg: string, + _: runtime.Logger_Options, + loc: runtime.Source_Code_Location, + ) { + f: LogFunc = cast(LogFunc)data + log_level: LogLevel + switch level { + case .Debug: + log_level = .Debug + case .Info: + log_level = .Info + case .Warning: + log_level = .Warning + case .Error: + log_level = .Error + case .Fatal: + log_level = .Fatal + } + f(log_level, msg, loc.procedure, loc.file_path, loc.line) + } + ctx.assert_func = assertf + + assert_proc :: proc(prefix, message: string, loc: runtime.Source_Code_Location) -> ! { + ctx := cast(^SimulationContext)context.user_ptr + if ctx.assert_func == nil { + fmt.eprintfln( + "assertion failure: %s (%s:%d): %s %s", + loc.procedure, + loc.file_path, + loc.line, + prefix, + message, + ) + libc.abort() + } + ctx.assert_func(prefix, message, loc.procedure, loc.file_path, loc.line) + fmt.println("AssertFunc returned") + libc.abort() + } + + if logf != nil { + ctx.ctx.logger = runtime.Logger { + procedure = log_proc, + data = cast(rawptr)logf, + } + } else { + context = ctx.ctx + ctx.ctx.logger = log.nil_logger() + } + + if assertf != nil { + ctx.ctx.assertion_failure_proc = assert_proc + } +} + +@(export = true) +sim_set_log_level :: proc "c" (ctx: ^SimulationContext, level: LogLevel) { + minimum_level: runtime.Logger_Level + switch level { + case .Debug: + minimum_level = .Debug + case .Info: + minimum_level = .Info + case .Warning: + minimum_level = .Warning + case .Error: + minimum_level = .Error + case .Fatal: + minimum_level = .Fatal + case: + context = ctx.ctx + log.errorf("Invalid minimum log level %d", level) + return + } + + ctx.ctx.logger.lowest_level = minimum_level +} diff --git a/lib/ols.json b/lib/ols.json new file mode 100644 index 0000000..89c69d7 --- /dev/null +++ b/lib/ols.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", + "profile": "default", + "profiles": [ + { + "name": "default", + "checker_path": [ "." ], + "defines": { + "ODIN_DEBUG": "true" + } + } + ] +} diff --git a/lib/physics.c b/lib/physics.c deleted file mode 100644 index 0039bd7..0000000 --- a/lib/physics.c +++ /dev/null @@ -1,146 +0,0 @@ -#include -#include -#include - -#include "common.h" -#include "player.h" -#include "snapshot.h" -#include "types.h" - -#include "physics.h" - -struct context * -dp_physics_context_create(int32_t snapshots, int32_t players, int32_t balls) -{ - int32_t i, j; - struct context *ctx = NULL; - - if (!(ctx = malloc(sizeof(*ctx)))) - return NULL; - memset(ctx, 0, sizeof(*ctx)); - - ctx->snapshots_len = snapshots; - if (!(ctx->snapshots = malloc(sizeof(*ctx->snapshots) * snapshots))) - { - free(ctx); - return NULL; - } - memset(ctx->snapshots, 0, sizeof(*ctx->snapshots) * snapshots); - - for (i = 0; i < snapshots; i++) - { - if (dp_snapshot_init(&ctx->snapshots[i], players, balls) != 0) - { - for (j = 0; j < i; j++) - { - dp_snapshot_deinit(&ctx->snapshots[j]); - } - free(ctx); - } - } - - return ctx; -} - -void -dp_physics_context_free(struct context *ctx) -{ - int32_t i; - if (!ctx) - return; - if (ctx->snapshots) - { - for (i = 0; i < ctx->snapshots_len; i++) - { - dp_snapshot_deinit(&ctx->snapshots[i]); - } - free(ctx->snapshots); - } - free(ctx); -} - -struct snapshot * -dp_physics_context_get_snapshot(struct context *ctx) -{ - int32_t i; - struct snapshot *s; - - for (i = 0; i < ctx->snapshots_len; i++) - { - s = &ctx->snapshots[i]; - if (!s->active) - { - s->active = true; - return s; - } - } - return NULL; -} - -void -dp_physics_context_return_snapshot(struct context *ctx, struct snapshot *s) -{ - int32_t i; - - assert(s->active); - - for (i = 0; i < ctx->snapshots_len; i++) - { - if (s == &ctx->snapshots[i]) - { - s->active = false; - } - } - - assert(!s->active); -} - -static void -dp_physics_tick_player(const struct player *cur, double delta, struct player *next) -{ - vec2 acc; - - next->state = cur->state; - if (next->state == STATE_REMOVED) - next->state = STATE_INACTIVE; - if (next->state == STATE_INACTIVE) - return; - - acc = dp_player_calculate_acceleration(cur); - next->vel = dp_vec2_add(cur->vel, dp_vec2_mul(acc, delta)); - next->pos = dp_vec2_add(cur->pos, dp_vec2_mul(next->vel, delta)); -} - -static void -dp_physics_tick_ball(const struct ball *cur, double delta, struct ball *next) -{ - next->state = cur->state; - if (next->state == STATE_REMOVED) - next->state = STATE_INACTIVE; - if (next->state == STATE_INACTIVE) - return; - - next->pos = dp_vec2_add(cur->pos, dp_vec2_mul(cur->vel, delta)); -} - -int -dp_physics_tick(const struct snapshot *cur, double delta, struct snapshot *next) -{ - int32_t i; - - assert(cur->active); - assert(next->active); - - for (i = 0; i < cur->players_len; i++) - { - dp_physics_tick_player(&cur->players[i], delta, &next->players[i]); - } - - for (i = 0; i < cur->balls_len; i++) - { - dp_physics_tick_ball(&cur->balls[i], delta, &next->balls[i]); - } - - next->tick = cur->tick + 1; - return 0; -} diff --git a/lib/physics.h b/lib/physics.h deleted file mode 100644 index e1c53f3..0000000 --- a/lib/physics.h +++ /dev/null @@ -1,17 +0,0 @@ -#include "types.h" - -struct snapshot; - -/* A game world */ -struct context { - int32_t snapshots_len; - struct snapshot *snapshots; -}; - -struct context *dp_physics_context_create(int32_t snapshots, int32_t players, int32_t balls); -void dp_physics_context_free(struct context *); - -struct snapshot *dp_physics_context_get_snapshot(struct context *); -void dp_physics_context_return_snapshot(struct context *, struct snapshot *); - -int dp_physics_tick(const struct snapshot *current, double delta, struct snapshot *next); diff --git a/lib/player.c b/lib/player.c deleted file mode 100644 index 35c3b80..0000000 --- a/lib/player.c +++ /dev/null @@ -1,36 +0,0 @@ -#include "snapshot.h" - -#include "player.h" - -vec2 -dp_player_calculate_acceleration(const struct player *p) -{ - vec2 acc, dec; - int x = 0, y = 0; - - if (p->input & INPUT_UP) - { - y -= 1; - } - if (p->input & INPUT_DOWN) - { - y += 1; - } - - if (p->input & INPUT_LEFT) - { - x -= 1; - } - if (p->input & INPUT_RIGHT) - { - x += 1; - } - - acc = dp_vec2_new((float)x, (float)y); - acc = dp_vec2_normal(acc); - acc = dp_vec2_mul(acc, PLAYER_ACCELERATION); - - dec = dp_vec2_mul(p->vel, -1.0f * PLAYER_DRAG); - - return dp_vec2_add(acc, dec); -} diff --git a/lib/player.h b/lib/player.h deleted file mode 100644 index 2f365bf..0000000 --- a/lib/player.h +++ /dev/null @@ -1,5 +0,0 @@ -#include "types.h" - -struct player; - -vec2 dp_player_calculate_acceleration(const struct player *); diff --git a/lib/simulation.odin b/lib/simulation.odin new file mode 100644 index 0000000..0ac8e89 --- /dev/null +++ b/lib/simulation.odin @@ -0,0 +1,261 @@ +package djup + +import "base:runtime" +import "core:log" + +#assert(#config(DJUP_CLIENT, false) || #config(DJUP_SERVER, false)) +#assert(!(#config(DJUP_CLIENT, false) && #config(DJUP_SERVER, false))) + +SimulationContext :: struct { + snapshots: []Snapshot, + ctx: runtime.Context, + assert_func: AssertFunc, +} + +Metadata :: Components + +entity_id :: distinct i32 + +Snapshot :: struct { + active: bool, + tick: u32, + metadata: []Metadata, + balls: []Ball, + player_inputs: []PlayerInput, + ctx: ^runtime.Context, +} + +@(export = true) +sim_create :: proc "c" (snapshots, entities: i32) -> ^SimulationContext { + if snapshots <= 0 || entities <= 0 { + return nil + } + + context = runtime.default_context() + context.logger = log.create_console_logger() + ctx := new(SimulationContext) + ctx.ctx = context + ctx.ctx.user_ptr = ctx + + snapshots, err := make([]Snapshot, snapshots) + if err != .None { + free(ctx) + return nil + } + ctx.snapshots = snapshots + + error := false + for &s in ctx.snapshots { + s.ctx = &ctx.ctx + if !snapshot_init(&s, entities) { + error = true + } + } + + if error { + for &s in ctx.snapshots { + snapshot_deinit(&s) + } + delete(ctx.snapshots) + free(ctx) + return nil + } + + log.destroy_console_logger(ctx.ctx.logger) + ctx.ctx.logger = log.nil_logger() + + return ctx +} + +@(export = true) +sim_free :: proc "c" (ctx: ^SimulationContext) { + if ctx == nil { + return + } + context = ctx.ctx + + for &s in ctx.snapshots { + snapshot_deinit(&s) + } + delete(ctx.snapshots) + free(ctx) +} + +snapshot_init :: proc(s: ^Snapshot, entities: i32) -> bool { + make_components :: proc($T: typeid, len: i32, error: ^bool) -> []T { + res, err := make([]T, len) + if err != .None { + log.errorf("Failed to allocate component array: %s", err) + error^ = true + return nil + } + return res + } + + error := false + components: Components = ~{} + for c in components { + switch c { + case .Exists: + s.metadata = make_components(Metadata, entities, &error) + case .Ball: + s.balls = make_components(Ball, entities, &error) + case .PlayerInput: + s.player_inputs = make_components(PlayerInput, entities, &error) + } + } + return !error +} + +snapshot_deinit :: proc(s: ^Snapshot) -> bool { + delete_components :: proc($T: typeid, slice: []T, error: ^bool) { + if slice == nil { + return + } + err := delete(slice) + if err != .None && error != nil { + log.errorf("Failed to delete component array: %s", err) + error^ = true + } + } + + error := false + components: Components = ~{} + for c in components { + switch c { + case .Exists: + delete_components(Metadata, s.metadata, &error) + case .Ball: + delete_components(Ball, s.balls, &error) + case .PlayerInput: + delete_components(PlayerInput, s.player_inputs, &error) + } + } + return !error +} + +@(export = true) +sim_get_snapshot :: proc "c" (ctx: ^SimulationContext) -> ^Snapshot { + if ctx == nil { + return nil + } + + for &s in ctx.snapshots { + if s.active { + continue + } + s.active = true + return &s + } + return nil +} + +@(export = true) +sim_return_snapshot :: proc "c" (ctx: ^SimulationContext, s: ^Snapshot) { + if ctx == nil || s == nil { + return + } + + s.active = false +} + +@(export = true) +snapshot_create_entity :: proc "c" (s: ^Snapshot) -> entity_id { + if s == nil { + return -1 + } + i := 0 + for i < len(s.metadata) { + if .Exists not_in s.metadata[i] { + s.metadata[i] = {.Exists} + return entity_id(i) + } + } + return -1 +} + +ComponentAddError :: enum u32 { + None, + InvalidArg, + InvalidId, + EntityNotFound, +} + +@(export = true) +snapshot_add_component :: proc "c" ( + s: ^Snapshot, + id: entity_id, + component: Component, + init: rawptr, +) -> ComponentAddError { + if s == nil { + return .InvalidArg + } + if (id < 0 && id >= entity_id(len(s.metadata))) { + return .InvalidId + } + + if .Exists not_in s.metadata[id] { + return .EntityNotFound + } + + add :: proc "contextless" (s: ^Snapshot, id: entity_id, c: Components, slice: []$T, init: ^T) { + s.metadata[id] += c + if init != nil { + slice[id] = init^ + } + } + + switch component { + case .Exists: + add(s, id, {.Exists}, s.metadata, nil) + case .Ball: + add(s, id, {.Ball}, s.balls, cast(^Ball)init) + case .PlayerInput: + add(s, id, {.PlayerInput}, s.player_inputs, cast(^PlayerInput)init) + } + return .None +} + +ComponentRemoveError :: enum u32 { + None, + InvalidArg, + InvalidId, + EntityNotFound, +} + +@(export = true) +snapshot_remove_components :: proc "c" ( + s: ^Snapshot, + id: entity_id, + components: Components, +) -> ComponentRemoveError { + if s == nil { + return .InvalidArg + } + if (id < 0 && id >= entity_id(len(s.metadata))) { + return .InvalidId + } + + if .Exists not_in s.metadata[id] { + return .EntityNotFound + } + + for c in components { + switch c { + case .Exists: + s.metadata[id] = {} + case .Ball: + s.balls[id] = Ball{} + case .PlayerInput: + s.player_inputs[id] = PlayerInput{} + } + } + + return .None +} + +@(export = true) +snapshot_reset_entity :: proc "c" (s: ^Snapshot, id: entity_id) -> ComponentRemoveError { + return snapshot_remove_components(s, id, ~{}) +} diff --git a/lib/snapshot.c b/lib/snapshot.c deleted file mode 100644 index d7dde50..0000000 --- a/lib/snapshot.c +++ /dev/null @@ -1,142 +0,0 @@ -#include -#include - -#include "common.h" - -#include "snapshot.h" - -int -dp_snapshot_init(struct snapshot *s, int32_t players_len, int32_t balls_len) -{ - if (!s) - return -1; - - if (!(s->players = calloc(players_len, sizeof(*s->players)))) - return -1; - if (!(s->balls = calloc(balls_len, sizeof(*s->balls)))) - { - free(s->players); - return -1; - } - s->players_len = players_len; - s->balls_len = balls_len; - return 0; -} - -void -dp_snapshot_deinit(struct snapshot *s) -{ - if (!s) - return; - if (s->players) - { - free(s->players); - s->players = NULL; - } - if (s->balls) - { - free(s->balls); - s->balls = NULL; - } -} - -int -dp_snapshot_put_player(struct snapshot *s, int32_t id, const struct player *p) -{ - if (!s || !p) - return -1; - if (id >= s->players_len) - return -1; - - s->players[id] = *p; - s->players[id].state = STATE_ACTIVE; - return 0; -} - -struct player * -dp_snapshot_get_player(struct snapshot *s, int32_t id) -{ - struct player *p; - - if (!s) - return NULL; - if (id >= s->players_len) - return NULL; - - p = &s->players[id]; - if (p->state == STATE_INACTIVE) - return NULL; - return p; -} - -int -dp_snapshot_remove_player(struct snapshot *s, int32_t id) -{ - struct player *p; - - if (!s) - return -1; - if (id >= s->players_len) - return -1; - - p = dp_snapshot_get_player(s, id); - memset(p, 0, sizeof(*p)); - p->state = STATE_REMOVED; - return 0; -} - -int -dp_snapshot_set_player_input(struct snapshot *s, int32_t id, uint8_t input) -{ - struct player *p; - - if (!(p = dp_snapshot_get_player(s, id))) - return -1; - p->input = input; - return 0; -} - -int -dp_snapshot_put_ball(struct snapshot *s, int32_t id, const struct ball *b) -{ - if (!s || !b) - return -1; - if (id >= s->balls_len) - return -1; - - s->balls[id] = *b; - s->balls[id].state = STATE_ACTIVE; - return 0; -} - -struct ball * -dp_snapshot_get_ball(struct snapshot *s, int32_t id) -{ - struct ball *b; - - if (!s) - return NULL; - if (id >= s->balls_len) - return NULL; - - b = &s->balls[id]; - if (b->state == STATE_INACTIVE) - return NULL; - return b; -} - -int -dp_snapshot_remove_ball(struct snapshot *s, int32_t id) -{ - struct ball *b; - - if (!s) - return -1; - if (id >= s->balls_len) - return -1; - - b = dp_snapshot_get_ball(s, id); - memset(b, 0, sizeof(*b)); - b->state = STATE_REMOVED; - return 0; -} diff --git a/lib/snapshot.h b/lib/snapshot.h deleted file mode 100644 index 028eac2..0000000 --- a/lib/snapshot.h +++ /dev/null @@ -1,43 +0,0 @@ -#include "types.h" -#include "constants.h" - -enum { - STATE_INACTIVE = 0, - STATE_ACTIVE, - STATE_REMOVED, - STATE_DEAD -}; - -struct player { - uint8_t state; - uint8_t input; - vec2 pos; - vec2 vel; -}; - -struct ball { - uint8_t state; - vec2 pos; - vec2 vel; -}; - -struct snapshot { - char active; - uint32_t tick; - int32_t players_len; - struct player *players; - int32_t balls_len; - struct ball *balls; -}; - -int dp_snapshot_init(struct snapshot *, int32_t players_len, int32_t balls_len); -void dp_snapshot_deinit(struct snapshot *); - -int dp_snapshot_put_player(struct snapshot *, int32_t id, const struct player *); -struct player *dp_snapshot_get_player(struct snapshot *, int32_t id); -int dp_snapshot_remove_player(struct snapshot *, int32_t id); -int dp_snapshot_set_player_input(struct snapshot *, int32_t id, uint8_t input); - -int dp_snapshot_put_ball(struct snapshot *, int32_t id, const struct ball *); -struct ball *dp_snapshot_get_ball(struct snapshot *, int32_t id); -int dp_snapshot_remove_ball(struct snapshot *, int32_t id); diff --git a/lib/systems.odin b/lib/systems.odin new file mode 100644 index 0000000..288bd01 --- /dev/null +++ b/lib/systems.odin @@ -0,0 +1,44 @@ +package djup + +DELTA: f32 : 1.0 / 60.0 + +@(export = true) +sim_tick :: proc "c" (cur, next: ^Snapshot) { + if cur == nil || next == nil { + return + } + context = next.ctx^ + + next.tick = cur.tick + 1 + copy(next.metadata, cur.metadata) + + player_input_system(cur, next) + ball_movement_system(cur, next) +} + +player_input_system :: proc(cur, next: ^Snapshot) { + for &e, id in soa_zip(m = cur.metadata, input = cur.player_inputs, ball = cur.balls) { + if e.m < {.Exists, .PlayerInput, .Ball} { + continue + } + + PLAYER_ACCELERATION :: 1500 + PLAYER_DRAG :: 2 + + acc := + vec2_normal(e.input.direction) * PLAYER_ACCELERATION + e.ball.velocity * -PLAYER_DRAG + e.ball.velocity += acc * DELTA + next.player_inputs[id] = e.input + } +} + +ball_movement_system :: proc(cur, next: ^Snapshot) { + for &e, id in soa_zip(m = cur.metadata, ball = cur.balls) { + if e.m < {.Exists, .Ball} { + continue + } + + next.balls[id].position = e.ball.velocity * DELTA + next.balls[id].velocity = e.ball.velocity + } +} diff --git a/lib/test.c b/lib/test.c deleted file mode 100644 index 70b32fa..0000000 --- a/lib/test.c +++ /dev/null @@ -1,24 +0,0 @@ -#include - -#include "types.h" - -void -dp_hello_world() -{ - printf("Hello World\n"); -} - -int -dp_get_num() -{ - return 1; -} - -vec2 -dp_get_vec2() -{ - vec2 vec; - vec.x = 1.0f; - vec.y = 2.5f; - return vec; -} diff --git a/lib/types.h b/lib/types.h deleted file mode 100644 index bf42576..0000000 --- a/lib/types.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef _TYPES_H -#define _TYPES_H - -#include -#include -#include - -typedef struct { - float x; - float y; -} vec2; - -vec2 dp_vec2_new(float, float); -vec2 dp_vec2_add(vec2, vec2); -vec2 dp_vec2_sub(vec2, vec2); -vec2 dp_vec2_mul(vec2, float scalar); -vec2 dp_vec2_dot(vec2, vec2); -float dp_vec2_length(vec2); -vec2 dp_vec2_normal(vec2); - -#endif diff --git a/lib/vec2.c b/lib/vec2.c deleted file mode 100644 index 55f5078..0000000 --- a/lib/vec2.c +++ /dev/null @@ -1,68 +0,0 @@ -#include -#include - -#include "types.h" - -vec2 -dp_vec2_new(float x, float y) -{ - vec2 new; - - assert(isfinite(x)); - assert(isfinite(y)); - - new.x = x; - new.y = y; - return new; -} - -vec2 -dp_vec2_add(vec2 a, vec2 b) -{ - vec2 new; - new.x = a.x + b.x; - new.y = a.y + b.y; - return new; -} - -vec2 -dp_vec2_sub(vec2 a, vec2 b) -{ - vec2 new; - new.x = a.x - b.x; - new.y = a.y - b.y; - return new; -} - -vec2 -dp_vec2_mul(vec2 vec, float scalar) -{ - vec2 new; - new.x = vec.x * scalar; - new.y = vec.y * scalar; - return new; -} - -vec2 -dp_vec2_dot(vec2 a, vec2 b) -{ - vec2 dot; - dot.x = a.x * b.x; - dot.y = a.y * b.y; - return dot; -} - -float -dp_vec2_length(vec2 v) -{ - return sqrtf(powf(v.x, 2.f) + powf(v.y, 2.f)); -} - -vec2 -dp_vec2_normal(vec2 v) -{ - float len = dp_vec2_length(v); - if (len == 0.0f) - return dp_vec2_new(0.0f, 0.0f); - return dp_vec2_mul(v, 1.0f / len); -} -- cgit v1.2.3