implement user-notifications

This commit is contained in:
jacob 2026-01-13 20:01:50 -06:00
parent 891efffc27
commit 1502645570
5 changed files with 232 additions and 63 deletions

View File

@ -262,7 +262,10 @@ i32 SignF64(f64 v);
u64 PowU64(u64 v, u8 exp);
#define PowF32(v, exp) powf((v), (exp))
#define PowF64(v, exp) pow((v), (exp))
#define SqrtF32(v) sqrtf(v)
#define SqrtF64(v) sqrt(v)
#define LnF32(v) log(v)
#define Log2F32(v) log2f(v)

View File

@ -39,7 +39,7 @@
#define SIM_TILES_PER_UNIT_SQRT (4)
#define SIM_TILES_PER_CHUNK_SQRT (16)
// #define SIM_TICKS_PER_SECOND 100
#define SIM_MAX_PING 5.0
#define SIM_TICKS_PER_SECOND 64
// Like USER_INTERP_RATIO, but applies to snapshots received by the local sim from the
// master sim (how far back in time should the client render the server's state)

View File

@ -36,7 +36,7 @@ void S_TickForever(WaveLaneCtx *lane)
// FIXME: Header
Rng user_cmd_tick_range = RNG(0, SIM_TICKS_PER_SECOND * 1.0);
Rng user_cmd_tick_range = RNG(SIM_TICKS_PER_SECOND * -SIM_MAX_PING, SIM_TICKS_PER_SECOND * SIM_MAX_PING);
P_CmdList queued_user_cmds = Zi;
P_CmdNode *first_free_user_cmd_node = 0;
@ -89,7 +89,7 @@ void S_TickForever(WaveLaneCtx *lane)
P_CmdList user_cmds = Zi;
{
i64 user_cmds_min_tick = world_frame->tick - user_cmd_tick_range.min;
i64 user_cmds_min_tick = world_frame->tick + user_cmd_tick_range.min;
i64 user_cmds_max_tick = world_frame->tick + user_cmd_tick_range.max;
// Prune queued user cmds
{
@ -97,7 +97,7 @@ void S_TickForever(WaveLaneCtx *lane)
{
P_CmdNode *next = cmd_node->next;
b32 prune = 0;
if (cmd_node->cmd.tick < user_cmds_min_tick || cmd_node->cmd.tick > user_cmds_max_tick)
if (cmd_node->cmd.predicted && (cmd_node->cmd.tick < user_cmds_min_tick || cmd_node->cmd.tick > user_cmds_max_tick))
{
prune = 1;
}
@ -114,7 +114,7 @@ void S_TickForever(WaveLaneCtx *lane)
{
for (P_CmdNode *src_cmd_node = input->cmds.first; src_cmd_node; src_cmd_node = src_cmd_node->next)
{
if (src_cmd_node->cmd.tick >= user_cmds_min_tick && src_cmd_node->cmd.tick <= user_cmds_max_tick)
if (!src_cmd_node->cmd.predicted || (src_cmd_node->cmd.tick >= user_cmds_min_tick && src_cmd_node->cmd.tick <= user_cmds_max_tick))
{
P_CmdNode *dst_cmd_node = first_free_user_cmd_node;
if (dst_cmd_node)
@ -132,25 +132,26 @@ void S_TickForever(WaveLaneCtx *lane)
}
}
}
// Copy queued commands for current tick
// Pop frame user cmds from queue
{
for (P_CmdNode *src_cmd_node = queued_user_cmds.first; src_cmd_node; src_cmd_node = src_cmd_node->next)
for (P_CmdNode *src_cmd_node = queued_user_cmds.first; src_cmd_node;)
{
i64 cmd_tick = src_cmd_node->cmd.tick;
if (!src_cmd_node->cmd.predicted)
P_CmdNode *next = src_cmd_node->next;
if (!src_cmd_node->cmd.predicted || src_cmd_node->cmd.tick == world_frame->tick)
{
// We can execute unpredicted cmds this frame, since they don't
// need to be buffered to match predicted behavior
cmd_tick = world_frame->tick;
}
if (cmd_tick == world_frame->tick)
{
P_CmdNode *dst_cmd_node = PushStruct(frame_arena, P_CmdNode);
dst_cmd_node->cmd = src_cmd_node->cmd;
dst_cmd_node->cmd.tick = cmd_tick;
DllQueuePush(user_cmds.first, user_cmds.last, dst_cmd_node);
++user_cmds.count;
}
{
DllQueueRemove(queued_user_cmds.first, queued_user_cmds.last, src_cmd_node);
SllStackPush(first_free_user_cmd_node, src_cmd_node);
--queued_user_cmds.count;
}
}
src_cmd_node = next;
}
}
}

View File

@ -279,11 +279,14 @@ V_WidgetTheme V_GetWidgetTheme(void)
{
V_WidgetTheme theme = Zi;
theme.text_font = GC_FontKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("font/seguisb.ttf")));
theme.ui_font = GC_FontKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("font/seguisb.ttf")));
// theme.chat_font = GC_FontKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("font/fixedsys.ttf")));
theme.chat_font = GC_FontKeyFromResource(ResourceKeyFromStore(&P_Resources, Lit("font/seguisb.ttf")));
theme.icon_font = UI_BuiltinIconFont();
// theme.font_size = 14;
theme.font_size = TweakFloat("Font size", 14, 8, 24, .precision = 0);
theme.ui_font_size = TweakFloat("UI font size", 14, 8, 24, .precision = 0);
theme.chat_font_size = TweakFloat("Chat font size", 16, 8, 24, .precision = 0);
theme.h1 = 2.00;
theme.h2 = 1.50;
theme.h3 = 1.25;
@ -322,11 +325,29 @@ V_WidgetTheme V_GetWidgetTheme(void)
void V_PushWidgetThemeStyles(V_WidgetTheme theme)
{
UI_Push(Font, theme.text_font);
UI_Push(FontSize, theme.font_size);
UI_Push(Font, theme.ui_font);
UI_Push(FontSize, theme.ui_font_size);
UI_Push(TextColor, theme.col.text);
}
////////////////////////////////////////////////////////////
//~ Notification helpers
void V_PushNotif(String msg)
{
Arena *perm = PermArena();
V_Notif *notif = PushStruct(perm, V_Notif);
notif->time_ns = TimeNs();
notif->datetime = LocalDateTime();
notif->msg = PushString(perm, msg);
if (V.last_notif)
{
notif->seq = V.last_notif->seq + 1;
}
SllQueuePushFront(V.first_notif, V.last_notif, notif);
LogInfoF("Notif: %F", FmtString(notif->msg));
}
////////////////////////////////////////////////////////////
//~ Vis tick
@ -938,6 +959,116 @@ void V_TickForever(WaveLaneCtx *lane)
}
}
//////////////////////////////
//- Build notifications
{
i64 duration_ns = NsFromSeconds(10);
i64 min_notif_time_ns = frame->time_ns - duration_ns;
i64 max_notifs = 10;
// Collect recent notifications
i64 draw_notifs_count = 0;
V_Notif **draw_notifs = PushStructs(frame->arena, V_Notif *, max_notifs);
{
i64 notif_idx = 0;
for (V_Notif *notif = V.first_notif; notif; notif = notif->next)
{
if (notif_idx < max_notifs && notif->time_ns > min_notif_time_ns)
{
draw_notifs[draw_notifs_count] = notif;
draw_notifs_count += 1;
}
else
{
break;
}
notif_idx += 1;
}
}
// Build notification UI
{
UI_Key notifs_key = UI_KeyF("notifs");
UI_BoxReport notifs_rep = UI_ReportsFromKey(notifs_key).draw;
f32 width = 600;
Vec2 pos = Zi;
pos.x = 10;
pos.y = frame->ui_dims.y * 0.5 - DimsFromRng2(notifs_rep.screen_rect).y * 0.5;
// Vec4 bg = VEC4(0, 0, 0, 0.25);
Vec4 bg = VEC4(0, 0, 0, 0);
UI_SetNext(Width, UI_PIX(width, 0));
// UI_SetNext(Height, UI_PIX(height, 0));
// UI_SetNext(Width, UI_GROW(0.9, 0));
// UI_SetNext(Width, UI_SHRINK(0, 0));
UI_SetNext(Height, UI_SHRINK(0, 0));
UI_SetNext(FloatingPos, pos);
UI_SetNext(BackgroundColor, bg);
UI_SetNext(Rounding, 0);
UI_SetNext(Flags, UI_BoxFlag_Floating);
UI_PushCP(UI_BuildColumnEx(notifs_key));
{
UI_Push(ChildAlignment, UI_Region_Left);
UI_Push(Font, theme.chat_font);
UI_Push(FontSize, theme.chat_font_size);
for (i64 notif_idx = draw_notifs_count - 1; notif_idx >= 0; --notif_idx)
{
V_Notif *notif = draw_notifs[notif_idx];
f32 opacity = 1.0;
f32 fade_curve = 0.8;
{
i64 remaining_ns = notif->time_ns - min_notif_time_ns;
opacity = PowF64(((f64)remaining_ns / (f64)duration_ns), fade_curve);
}
UI_SetNext(Tint, 0);
UI_SetNext(Width, UI_SHRINK(0, 0));
UI_SetNext(Height, UI_SHRINK(0, 0));
UI_PushCP(UI_BuildRow());
{
UI_Push(Tint, VEC4(1, 1, 1, opacity));
UI_Push(Width, UI_SHRINK(0, 0));
UI_Push(Height, UI_SHRINK(0, 0));
{
String msg = StringF(
frame->arena,
"[%F:%F:%F] ",
FmtUint(notif->datetime.hour, .z = 2),
FmtUint(notif->datetime.minute, .z = 2),
FmtUint(notif->datetime.second, .z = 2)
);
UI_SetNext(TextColor, VEC4(0.5, 0.5, 0.5, 1));
UI_SetNext(Text, msg);
UI_SetNext(Flags, UI_BoxFlag_DrawText);
UI_BuildRow();
}
{
String msg = notif->msg;
UI_SetNext(Text, msg);
UI_SetNext(Flags, UI_BoxFlag_DrawText);
UI_BuildRow();
}
}
UI_PopCP(UI_TopCP());
if (notif_idx != 0)
{
UI_BuildSpacer(UI_PIX(5, 0), Axis_Y);
}
}
}
UI_PopCP(UI_TopCP());
}
}
//////////////////////////////
//- Init test layout
@ -2137,13 +2268,13 @@ void V_TickForever(WaveLaneCtx *lane)
UI_BuildLabelF("Sim world tick: %F", FmtSint(sim_world->last_frame->tick));
UI_BuildLabelF("Sim world entities: %F", FmtSint(sim_world->last_frame->ents_count));
UI_BuildSpacer(UI_PIX(padding, 1), Axis_Y);
UI_BuildLabelF("Predict world seed: 0x%F", FmtHex(predict_world->seed));
UI_BuildLabelF("Predict world tick: %F", FmtSint(predict_world->last_frame->tick));
UI_BuildLabelF("Predict world entities: %F", FmtSint(predict_world->last_frame->ents_count));
UI_BuildLabelF("Predicted world seed: 0x%F", FmtHex(predict_world->seed));
UI_BuildLabelF("Predicted world tick: %F", FmtSint(predict_world->last_frame->tick));
UI_BuildLabelF("Predicted world entities: %F", FmtSint(predict_world->last_frame->ents_count));
UI_BuildSpacer(UI_PIX(padding, 1), Axis_Y);
UI_BuildLabelF("Blend world seed: 0x%F", FmtHex(blend_world->seed));
UI_BuildLabelF("Blend world tick: %F", FmtSint(blend_world->last_frame->tick));
UI_BuildLabelF("Blend world entities: %F", FmtSint(blend_world->last_frame->ents_count));
UI_BuildLabelF("Blended world seed: 0x%F", FmtHex(blend_world->seed));
UI_BuildLabelF("Blended world tick: %F", FmtSint(blend_world->last_frame->tick));
UI_BuildLabelF("Blended world entities: %F", FmtSint(blend_world->last_frame->ents_count));
}
UI_BuildSpacer(UI_PIX(padding, 1), Axis_Y);
{
@ -2165,8 +2296,8 @@ void V_TickForever(WaveLaneCtx *lane)
UI_Pop(FontSize);
}
UI_BuildLabelF(" Arenas: %F", FmtSint(GetGstat(NumArenas)));
UI_BuildLabelF(" Arena memory committed: %F MiB", FmtFloat((f64)GetGstat(ArenaMemoryCommitted) / 1024 / 1024));
UI_BuildLabelF(" Arena memory reserved: %F TiB", FmtFloat((f64)GetGstat(ArenaMemoryReserved) / 1024 / 1024 / 1024 / 1024));
UI_BuildLabelF(" Arena memory committed: %F MiB", FmtFloat((f64)GetGstat(ArenaMemoryCommitted) / 1024 / 1024, .p = 3));
UI_BuildLabelF(" Arena memory reserved: %F TiB", FmtFloat((f64)GetGstat(ArenaMemoryReserved) / 1024 / 1024 / 1024 / 1024, .p = 3));
}
UI_BuildSpacer(UI_PIX(padding, 1), Axis_Y);
{
@ -2176,8 +2307,8 @@ void V_TickForever(WaveLaneCtx *lane)
UI_Pop(FontSize);
}
UI_BuildLabelF(" Arenas: %F", FmtUint(gpu_stats.arenas_count));
UI_BuildLabelF(" Device memory usage: %F MiB", FmtFloat((f64)gpu_stats.device_committed / 1024 / 1024));
UI_BuildLabelF(" Host memory usage: %F MiB", FmtFloat((f64)gpu_stats.host_committed / 1024 / 1024));
UI_BuildLabelF(" Device memory usage: %F MiB", FmtFloat((f64)gpu_stats.device_committed / 1024 / 1024, .p = 3));
UI_BuildLabelF(" Host memory usage: %F MiB", FmtFloat((f64)gpu_stats.host_committed / 1024 / 1024, .p = 3));
UI_BuildLabelF(" Non-reuse tally: %F", FmtUint(gpu_stats.cumulative_nonreuse_count));
}
UI_BuildSpacer(UI_PIX(padding, 1), Axis_Y);
@ -2228,7 +2359,6 @@ void V_TickForever(WaveLaneCtx *lane)
}
f32 fade_curve = 0.5;
i64 now_ns = TimeNs();
{
UI_Push(FloatingPos, VEC2(0, 0));
UI_SetNext(Flags, UI_BoxFlag_Floating);
@ -2262,7 +2392,7 @@ void V_TickForever(WaveLaneCtx *lane)
for (u64 i = logs.count; i-- > 0 && display_count < max && !done;)
{
LogEvent ev = logs.logs[i];
if (ev.time_ns > (now_ns - max_time_ns))
if (ev.time_ns > (frame->time_ns - max_time_ns))
{
if (ev.level <= console_level)
{
@ -2281,7 +2411,7 @@ void V_TickForever(WaveLaneCtx *lane)
{
LogEvent log = display_logs[i];
f32 opacity = 0.75;
f32 lin = 1.0 - ClampF64((f64)(now_ns - log.time_ns) / (f64)fade_time_ns, 0, 1);
f32 lin = 1.0 - ClampF64((f64)(frame->time_ns - log.time_ns) / (f64)fade_time_ns, 0, 1);
opacity *= PowF32(lin, fade_curve);
String text = log.msg;
if (!minimized)
@ -2482,6 +2612,11 @@ void V_TickForever(WaveLaneCtx *lane)
// {
// should_clear_particles = 1;
// } break;
case V_CmdKind_test:
{
V_PushNotif(Lit("Hello!!!"));
} break;
}
}
@ -2544,6 +2679,12 @@ void V_TickForever(WaveLaneCtx *lane)
{
P_Msg *msg = &msg_node->msg;
//- Chat
// if (msg->kind == P_MsgKind_Chat)
// {
// }
//- Tiles
if (msg->kind == P_MsgKind_Tiles && sim_world->tiles_hash != msg->tiles_hash)
{
@ -2633,8 +2774,8 @@ void V_TickForever(WaveLaneCtx *lane)
//- Submit sim commands
// FIXME: Real ping
f64 ping = 0.250;
// f64 ping = 0;
// f64 ping = 0.250;
f64 ping = 0;
i64 ping_ns = NsFromSeconds(ping);
frame->predict_to = sim_world->last_frame->tick + MaxF64(CeilF64(ping * SIM_TICKS_PER_SECOND), 1.0);
@ -2682,37 +2823,40 @@ void V_TickForever(WaveLaneCtx *lane)
predict_world->tiles_hash = sim_world->tiles_hash;
CopyStructs(predict_world->tiles, sim_world->tiles, P_TilesCount);
}
predict_world->seed = sim_world->seed;
// P_ClearFrames(predict_world, I64Min, I64Max);
// predict_frame = P_PushFrame(predict_world, sim_world->last_frame, sim_world->last_frame->tick);
P_ClearFrames(predict_world, I64Min, I64Max);
predict_frame = P_PushFrame(predict_world, sim_world->last_frame, sim_world->last_frame->tick);
// We want to keep previously predicted ticks to preserve constraints
P_ClearFrames(predict_world, I64Min, sim_world->last_frame->tick - 2);
// // We want to keep previously predicted ticks to preserve constraints
// P_ClearFrames(predict_world, I64Min, sim_world->last_frame->tick - 2);
i64 step_count = frame->predict_to - sim_world->last_frame->tick;
P_Frame *base_predict_frame = P_PushFrame(predict_world, sim_world->last_frame, sim_world->last_frame->tick);
for (i64 step_idx = 0; step_idx < step_count; ++step_idx)
{
P_Frame *step_frame = P_PushFrame(predict_world, predict_world->last_frame, predict_world->last_frame->tick + 1);
// i64 step_count = frame->predict_to - sim_world->last_frame->tick;
// P_Frame *base_predict_frame = P_PushFrame(predict_world, sim_world->last_frame, sim_world->last_frame->tick);
// for (i64 step_idx = 0; step_idx < step_count; ++step_idx)
// {
// P_Frame *step_frame = P_PushFrame(predict_world, predict_world->last_frame, predict_world->last_frame->tick + 1);
// FIXME: Cmds
P_CmdList step_cmds = Zi;
for (P_CmdNode *src_cmd_node = V.sim_cmds.first; src_cmd_node; src_cmd_node = src_cmd_node->next)
{
if (src_cmd_node->cmd.predicted && src_cmd_node->cmd.tick == step_frame->tick)
{
P_CmdNode *dst_cmd_node = PushStruct(frame->arena, P_CmdNode);
DllQueuePush(step_cmds.first, step_cmds.last, dst_cmd_node);
++step_cmds.count;
}
}
// // FIXME: Cmds
// P_CmdList step_cmds = Zi;
// for (P_CmdNode *src_cmd_node = V.sim_cmds.first; src_cmd_node; src_cmd_node = src_cmd_node->next)
// {
// if (src_cmd_node->cmd.predicted && src_cmd_node->cmd.tick == step_frame->tick)
// {
// P_CmdNode *dst_cmd_node = PushStruct(frame->arena, P_CmdNode);
// DllQueuePush(step_cmds.first, step_cmds.last, dst_cmd_node);
// ++step_cmds.count;
// }
// }
P_StepFrame(step_frame, step_cmds);
}
// P_StepFrame(step_frame, step_cmds);
// }
predict_frame = predict_world->last_frame;
// TODO: Extract information that occurred between first & last prediction, like bullet hits etc?
}
@ -2735,6 +2879,7 @@ void V_TickForever(WaveLaneCtx *lane)
CopyStructs(blend_world->tiles, predict_world->tiles, P_TilesCount);
tiles_dirty = 1;
}
blend_world->seed = predict_world->seed;
P_ClearFrames(blend_world, I64Min, I64Max);
blend_frame = P_PushFrame(blend_world, predict_world->last_frame, predict_world->last_frame->tick);

View File

@ -18,6 +18,7 @@
X(delete, Delete entity at cursor, V_CmdDescFlag_None, V_HOTKEY( Button_M2 ), ) \
X(reset_world, Reset world, V_CmdDescFlag_None, V_HOTKEY( Button_R, .ctrl = 1, .alt = 1 ), ) \
X(clear_particles, Clear particles, V_CmdDescFlag_None, V_HOTKEY( Button_C ), ) \
X(test, Test, V_CmdDescFlag_None, V_HOTKEY( Button_T, .ctrl = 1 ), ) \
/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
////////////////////////////////////////////////////////////
@ -25,10 +26,13 @@
Struct(V_WidgetTheme)
{
GC_FontKey text_font;
GC_FontKey ui_font;
GC_FontKey chat_font;
GC_FontKey icon_font;
f32 font_size;
f32 ui_font_size;
f32 chat_font_size;
f32 h1;
f32 h2;
f32 h3;
@ -133,6 +137,18 @@ Global Readonly V_CmdDesc V_cmd_descs[V_CmdKind_COUNT] = {
#undef X
};
////////////////////////////////////////////////////////////
//~ Notification types
Struct(V_Notif)
{
V_Notif *next;
DateTime datetime;
i64 time_ns;
i64 seq;
String msg;
};
////////////////////////////////////////////////////////////
//~ Palette types
@ -289,6 +305,10 @@ Struct(V_Ctx)
V_Panel *root_panel;
V_Window *dragging_window;
// Notifications
V_Notif *first_notif;
V_Notif *last_notif;
// Sim commands
P_CmdList sim_cmds;
P_CmdNode *first_free_sim_cmd_node;