From 7c5d82e124cccd220d7414fb6df500e3191e79cc Mon Sep 17 00:00:00 2001 From: markmental Date: Fri, 27 Mar 2026 23:19:42 -0400 Subject: [PATCH] Add hw acceleration, improve video performance --- src/app.c | 30 +++++-- src/app.h | 2 + src/app.o | Bin 10392 -> 10904 bytes src/channel.c | 15 ++++ src/channel.h | 2 + src/channel.o | Bin 5648 -> 6016 bytes src/frame_queue.h | 2 +- src/player.c | 22 ++--- src/player.h | 8 +- src/player.o | Bin 21792 -> 21808 bytes src/ui.c | 220 ++++++++++++++++++++++++++++++++++++---------- src/ui.h | 44 +++++++++- src/ui.o | Bin 10680 -> 15504 bytes 13 files changed, 273 insertions(+), 72 deletions(-) diff --git a/src/app.c b/src/app.c index 1d8357a..6ac3aef 100644 --- a/src/app.c +++ b/src/app.c @@ -74,9 +74,11 @@ static int update_video_texture(App *app) { FrameData frame = {0}; double sync_clock; double lead_tolerance; + Uint64 now_ticks; - sync_clock = player_get_sync_clock(&app->player, time(NULL)); - lead_tolerance = app->startup_handoff_active ? 0.220 : 0.035; + now_ticks = SDL_GetTicks64(); + sync_clock = player_get_sync_clock(&app->player, now_ticks); + lead_tolerance = app->startup_handoff_active ? 0.240 : 0.060; if (!player_consume_synced_frame(&app->player, &frame, sync_clock, lead_tolerance)) { if (!app->startup_handoff_active || !player_consume_latest_frame(&app->player, &frame)) { @@ -172,7 +174,6 @@ int app_init(App *app) { app->running = 1; app->mode = MODE_FULLSCREEN; app->last_blackout_state = 1; - app->app_start_time = time(NULL); configure_runtime_environment(); log_runtime_environment("startup-before-sdl"); @@ -188,6 +189,9 @@ int app_init(App *app) { return -1; } + app->app_start_time = time(NULL); + app->app_start_ticks = SDL_GetTicks64(); + if (TTF_Init() != 0) { fprintf(stderr, "SDL_ttf init failed: %s\n", TTF_GetError()); return -1; @@ -216,11 +220,16 @@ int app_init(App *app) { return -1; } - if (player_init(&app->player, &app->channels, app->app_start_time) != 0) { + if (player_init(&app->player, &app->channels, app->app_start_ticks) != 0) { fprintf(stderr, "Unable to initialize player.\n"); return -1; } + if (ui_cache_init(&app->ui_cache, app->renderer, &app->fonts, &app->channels) != 0) { + fprintf(stderr, "Unable to initialize UI cache.\n"); + return -1; + } + if (app->channels.count > 0) { begin_startup_handoff(app); player_tune(&app->player, 0); @@ -234,6 +243,8 @@ void app_run(App *app) { while (app->running) { int in_blackout; + Uint64 now_ticks; + time_t now_wall; while (SDL_PollEvent(&event)) { handle_event(app, &event); @@ -245,6 +256,8 @@ void app_run(App *app) { } in_blackout = player_is_in_blackout(&app->player); + now_ticks = SDL_GetTicks64(); + now_wall = time(NULL); if (app->last_blackout_state && !in_blackout) { app->startup_handoff_active = 1; app->startup_handoff_until = SDL_GetTicks() + 400; @@ -253,7 +266,7 @@ void app_run(App *app) { app->last_blackout_state = in_blackout; if (app->channels.count == 0) { - ui_render_no_media(app->renderer, &app->fonts); + ui_render_no_media(app->renderer, &app->ui_cache); } else if (app->mode == MODE_GUIDE) { if (!in_blackout) { player_resume_audio(&app->player); @@ -263,10 +276,12 @@ void app_run(App *app) { app->texture_width, app->texture_height, &app->fonts, + &app->ui_cache, &app->channels, app->player.current_index, - app->app_start_time, - time(NULL)); + app->app_start_ticks, + now_ticks, + now_wall); } else { if (!in_blackout) { player_resume_audio(&app->player); @@ -289,6 +304,7 @@ void app_destroy(App *app) { player_destroy(&app->player); channel_list_destroy(&app->channels); destroy_video_texture(app); + ui_cache_destroy(&app->ui_cache); ui_destroy_fonts(&app->fonts); if (app->renderer) { SDL_DestroyRenderer(app->renderer); diff --git a/src/app.h b/src/app.h index edfcdb9..1a70c95 100644 --- a/src/app.h +++ b/src/app.h @@ -20,9 +20,11 @@ typedef struct App { int last_blackout_state; Uint32 startup_handoff_until; time_t app_start_time; + Uint64 app_start_ticks; ChannelList channels; Player player; UiFonts fonts; + UiCache ui_cache; } App; int app_init(App *app); diff --git a/src/app.o b/src/app.o index d0b92bbb184bd94f588d12afa5356355032e086f..b89b50093f19c96a73c19646f94943cc9dfc1349 100644 GIT binary patch literal 10904 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 literal 10392 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 diff --git a/src/channel.c b/src/channel.c index c4ef3a1..10a9916 100644 --- a/src/channel.c +++ b/src/channel.c @@ -189,3 +189,18 @@ double channel_live_position(const Channel *channel, time_t app_start_time, time return fmod(elapsed, channel->duration_seconds); } + +double channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks) { + double elapsed; + + if (!channel || channel->duration_seconds <= 0.0) { + return 0.0; + } + + if (now_ticks < app_start_ticks) { + now_ticks = app_start_ticks; + } + + elapsed = (double) (now_ticks - app_start_ticks) / 1000.0; + return fmod(elapsed, channel->duration_seconds); +} diff --git a/src/channel.h b/src/channel.h index 28fb1d4..fcb6354 100644 --- a/src/channel.h +++ b/src/channel.h @@ -1,6 +1,7 @@ #ifndef CHANNEL_H #define CHANNEL_H +#include #include #include @@ -21,5 +22,6 @@ typedef struct ChannelList { int channel_list_load(ChannelList *list, const char *media_dir); void channel_list_destroy(ChannelList *list); double channel_live_position(const Channel *channel, time_t app_start_time, time_t now); +double channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks); #endif diff --git a/src/channel.o b/src/channel.o index 542bcfa9664d94239bb548f5a0ee1ceed4beec79..619064d8d3d4ad3c9879a26035bccbbc447ea6e6 100644 GIT binary patch delta 1123 zcmY*YT}TvB6ux)cU!B>R*`KbXF1a$nXzC<{(u1XyF_40kMD`Ei(ho&yf<%&~MS>sKzMBze$O5Q<4(=VOvF*Kxw^{gP+yOqtT2S7u47tA#QN8kJ6^B&9*~ zrlgZDL@l|G`HE9ZI<toXM5jc*5904GJUgbN+Km4Nh}Z37EW!6&B-)deg=lS-j7gEUqu~DquRmR zi7vN{3kqrQou?2QW$Q72k>jA>d&pk|LxIS|Lr)(LBjiS2OO##OAnGHyi{ovM4>&%7 z8#JHb{&S8mIlkt&$ni7BZybMf^g~+?!8*qpc$8z{2JSaAR9i4Y0fdpv?+&37YZI3M z=Mvn-cwdzNz5QjS7hZo8|ZVQ=M*iKn^67{-Kf&Z*1ot!K`4w4(j6CJ^zc)y+xQ z5d0O0N)L?wtY_?_jU`Yn5}XJ1q5=J)fDgmCNZ`Jx;R2Wx4T>uqBYx&La_sgKTnNK{ zLuFT;?RwLZY0G97LXv9o;Rn@6V5?-P5u0*lgYu$8)O;Ho-DK}GA`y5Y3HSi4QSXhU zL76OoR~A&&R`b|a6NPe_h{ZNmg?BQ6v$96_G9a@ZP13ku7Q}J3S!!(2U_~ZslzQ0{ zEo@*T3Q;v18?pY6Ol_k=;F==Te6VG8o4V9Z18VH(^mYB8F=XXg7ah74f&(zD^eSl@ zGh=%8TLmSsD2`Za#vuBY1wjtx`4D{u%+gR9tac>b^ef*CJwZL|CW1On!l&SFSCNOx zcwKEscWn2-`Oq#$lZO{1LV8XUBlF zLk9=5b#ahvqR?)2)2)gf3OWWWI7vj$JuiV558UsZd%y3TbN|{&_e8g@cHK9}@*4KE zrCj^rsq_8Dlr5lo5e~du|6ZXj;7xntE}ZF5v>9J8ZsHEDyybYC@dM*0#?Or37zd2M z7zO=D5LnYP!v}Yik(;3j;2nzfuEFlpK~5_&L!h!f7_@E7A|Ao z4H20J4AUs}MoBsH9v00o4k#M_O)XC?q)9f!s+r&xeu#Ut<#6N z9q)P$dv=Jgw(;Dl#q*hl&oZJ2i%u9cdbY=XZ z3JVmC5A-p^R&dgtWQQ4c0oVRxua90-ijLq3J=eNxlq{M_ldBE}?gMmYc1u4-TBhjd z%%JT>`mVuqWd}X4sU{j$3IYAo`Z#R(b&=qze^$S(rOE<&ep78}_~M^YH`0VgP}es` x`~mSHT0upB<)j%c^n#|}nM!9{qZ^ZrX?MYK{X#Ki)=>?cdgP|FHSBxy{{XU)-hTi9 diff --git a/src/frame_queue.h b/src/frame_queue.h index cfde523..d4cdf1e 100644 --- a/src/frame_queue.h +++ b/src/frame_queue.h @@ -3,7 +3,7 @@ #include -#define FRAME_QUEUE_CAPACITY 8 +#define FRAME_QUEUE_CAPACITY 12 typedef struct FrameData { unsigned char *buffer; diff --git a/src/player.c b/src/player.c index ed30848..a57f849 100644 --- a/src/player.c +++ b/src/player.c @@ -272,7 +272,7 @@ static int queue_audio_frame(Player *player, : audio_frame->best_effort_timestamp * av_q2d(audio_time_base); frame_duration = (double) sample_count / (double) out_rate; - queued_limit = (int) (player_audio_bytes_per_second(player) * 0.12); + queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18); while (!should_stop(player) && (int) SDL_GetQueuedAudioSize(player->audio_device) > queued_limit) { SDL_Delay(2); } @@ -420,7 +420,7 @@ static int decode_thread_main(void *userdata) { goto cleanup; } - seek_seconds = channel_live_position(channel, player->app_start_time, time(NULL)); + seek_seconds = channel_live_position_precise(channel, player->app_start_ticks, SDL_GetTicks64()); if (channel->duration_seconds > 0.0) { int64_t seek_target = (int64_t) (seek_seconds / av_q2d(time_base)); avformat_seek_file(format_context, video_stream_index, INT64_MIN, seek_target, INT64_MAX, 0); @@ -583,7 +583,7 @@ static void player_stop_thread(Player *player) { player->thread = NULL; } -int player_init(Player *player, const ChannelList *channels, time_t app_start_time) { +int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks) { if (!player || !channels) { return -1; } @@ -591,7 +591,7 @@ int player_init(Player *player, const ChannelList *channels, time_t app_start_ti memset(player, 0, sizeof(*player)); player->channels = channels; player->current_index = -1; - player->app_start_time = app_start_time; + player->app_start_ticks = app_start_ticks; player->audio_mutex = SDL_CreateMutex(); player->clock_mutex = SDL_CreateMutex(); player->error_mutex = SDL_CreateMutex(); @@ -692,7 +692,7 @@ int player_consume_synced_frame(Player *player, FrameData *out, double clock_sec return 0; } - late_tolerance = SDL_GetTicks() < player->catchup_until ? 0.180 : 0.090; + late_tolerance = SDL_GetTicks() < player->catchup_until ? 0.220 : 0.130; while (frame_queue_peek_first(&player->frame_queue, &candidate)) { if (candidate.pts_seconds < clock_seconds - late_tolerance) { @@ -728,15 +728,15 @@ int player_consume_synced_frame(Player *player, FrameData *out, double clock_sec return 0; } -double player_live_position(const Player *player, time_t now) { +double player_live_position(const Player *player, Uint64 now_ticks) { if (!player || !player->channels || player->current_index < 0 || player->current_index >= player->channels->count) { return 0.0; } - return channel_live_position(&player->channels->items[player->current_index], player->app_start_time, now); + return channel_live_position_precise(&player->channels->items[player->current_index], player->app_start_ticks, now_ticks); } -double player_get_sync_clock(Player *player, time_t now) { +double player_get_sync_clock(Player *player, Uint64 now_ticks) { double clock_seconds; double queued_seconds = 0.0; double bytes_per_second; @@ -747,12 +747,12 @@ double player_get_sync_clock(Player *player, time_t now) { } if (!player->has_audio_stream || player->audio_device == 0) { - return player_live_position(player, now); + return player_live_position(player, now_ticks); } bytes_per_second = player_audio_bytes_per_second(player); if (bytes_per_second <= 0.0) { - return player_live_position(player, now); + return player_live_position(player, now_ticks); } SDL_LockMutex(player->clock_mutex); @@ -761,7 +761,7 @@ double player_get_sync_clock(Player *player, time_t now) { SDL_UnlockMutex(player->clock_mutex); if (!audio_started) { - return player_live_position(player, now); + return player_live_position(player, now_ticks); } queued_seconds = (double) SDL_GetQueuedAudioSize(player->audio_device) / bytes_per_second; diff --git a/src/player.h b/src/player.h index 78b8ece..0568cf6 100644 --- a/src/player.h +++ b/src/player.h @@ -13,7 +13,7 @@ typedef struct Player { SDL_atomic_t stop_requested; const ChannelList *channels; int current_index; - time_t app_start_time; + Uint64 app_start_ticks; Uint32 tuning_blackout_until; SDL_AudioDeviceID audio_device; SDL_AudioSpec audio_spec; @@ -29,13 +29,13 @@ typedef struct Player { char last_error[256]; } Player; -int player_init(Player *player, const ChannelList *channels, time_t app_start_time); +int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks); void player_destroy(Player *player); int player_tune(Player *player, int channel_index); int player_consume_latest_frame(Player *player, FrameData *out); int player_consume_synced_frame(Player *player, FrameData *out, double clock_seconds, double lead_tolerance); -double player_live_position(const Player *player, time_t now); -double player_get_sync_clock(Player *player, time_t now); +double player_live_position(const Player *player, Uint64 now_ticks); +double player_get_sync_clock(Player *player, Uint64 now_ticks); void player_set_catchup_until(Player *player, Uint32 tick); int player_is_in_blackout(const Player *player); void player_resume_audio(Player *player); diff --git a/src/player.o b/src/player.o index c9f8d2a286ceba69b4cad285c103e6f5f87eed0b..fd2c547ac638ebc75c9a06d61effa1334d6d0419 100644 GIT binary patch delta 2880 zcmZ`*Yitx%6y9NIyKS-4_jF6Q4_O{9NNGU2ZN*Y-VOA?rslk8|2$b+JQl*$`#G<>~ zG7bx^SEB|)R7i+`rgaTs3S~uLOF|U*Q=&$JC{WbUK#D}r^*d)~OH1@7`_0^QzI*O@ z-8=i?Ua51h)ILe->?kbElWjK5A8DGa>4R5n7@GeNEfB+Rf$oEH#|!zA-0@b$Ioa_BMky8v>v_n1U-zN zPFb9{r;BByJE2X9`5bsG#+|?xJ;njIEbOrp;%|)xInbIfi_%zk%Lnlk#;dMoX8A+P zmQ9FEGrn-OV^?FYtL%#6TaH9kmklVs#iO9p|77lO`<1-Sb8?i-jH#g3ia#zW^U-jF24_tC9L#PsRQ zL}wPP4JC<_01v{geX{sHF-zJeV#e%5t#^-!ky62=#7<>%olDOICVxAlNG1Oa-d{HWk)1*WGqLq zfVtffjQ*{I6!#$)8niK`zSIec zSazKY*o?E0=7IFdv_mD3R_Q($pVUy(R20eFkC_^@EE#|YEO}ZMmFc-y`)oS(u%12- zcqTnF3C#ZZe^C3tAy>OB2GcjF5OEu?Lnm0(vasmAjB&t28Fj#<%!yQY-hukf z1CLI~VqRu$S=7k$e_BSOZFEmYN7RIxRP4+wl17V@nGe?C68O4bzM<{Ybm1n0dTt4DGO}QaRZry)ip|Ve?v!aa}W0ATc-l?k8kWtAn6QuoCodAah z)xw3~@WorC1&+{}?4b)bx{A(9$xU+>_R@@ob%oN=7|Xh$+x1mei5Ifc#(_(OA(zO3 z5dwIzC(Ah>31OSw%eS<`+X3U?P==O#^Nn@W4cmlYS1H#*VK4Ow4JE}(&4C9xBGoY- zMRYsz<4Ic^*n|6axg(<FZIS;cQI{)TqGEr zd4FhQ{F48uJU-qISXwQMA9M2yuqIm1VSfg!o%oyW%&Vl&)zBXg{aUSlsU)PK#Yh~; zKn8~V>y(3Sd1aEC(6_5`RzkS4@R{vL2Pd7X+>Ke^u28Tq-xDvD2Mdbcm878PDpI90 zj6XB}!T2|0ida5Hl_oOIWSq@d$GDR56~?z2KW6M?Jj~e3c#&~{F~WGi=z2hvrZRdM zmoctoe3|hL#vP1%8G9JRj6X15WV|ZQOjV_u{2nWo7ps!WI7yV?y_DZ;80#5V6FN5G zzeOtkgPqT!?AdHv>9)YonX`it5rG-Bn2p&cBh7r+zGLkrOWST~FI#p-i2~%-!B|Y_ zsEjpt+?-_&^W38nfaGK&VmewmSK>NO>U6yN#-?X%!22)D9M zp{ah&igg=i&Jd@kXWExGzSyv0&4z~HrRf8*lq>4oYH*;mS#}(T!!+o+nu8zy^YHji zyk4O?#$qEHq^-s;sx1{=6{@r%c(P)BhIA!ZR@Wj)3xZ!giN|}QtiDa^5ntDr#@iOq MVw-KRc)c#~UlH6ICIA2c delta 2880 zcmZ`*4NOy46z+RipnyF7OOY0c4;B2uMvVpL5C-}>6-C`NGntwV2hkzM;z;I(SzGY! zSuMLUnwb!dON?R++6_ryQ2!_<+ahL(?k7$cH)U!hI@CpD>%JLzuSAxch5cd zoOADe9mlwX$2jj4?qF+PUKX#@N$#Lyfh3RI)Aahfyz?4>p>!4pi#MUcqfbkPDBogs+yD)?wbvXo{Gjo=X}s^jg+Bh8QkPWFv68r z5liVVhp{`@ungwbz~9l!AZ_LtLBQDv9Bfj+>EhWgPULvsNv?+H!1%a{0%+}Wb3b-U zZwJq=N2joL<2pc_s6PdGUT=%D2Fa#f>9p}IGIEmP(Kw)cnzMN}C(_pRr2#jxU2G(l z+b`_=81^P9UyEK?S6Lty-WFVIAynms!-8uyEG5a0iF@a`kX5eJEpb0`RM7IEf-Mx1 za)Yp$!du$kd=>Yg5lV%`<}MhA)p`&N@a$n^KDcX%GAE8!g6;y(7Dt(4a9MXyaQ*&_ z`D0V2zCa4VcSZ?gLO!iMu4gAmv_y<7}T!)?ngQ`!b zW6kMcEgS_}2|NfiU*Xx^*mTaz^l@#F>#ySUToN!burt&=dV0Hf);&HGR=Ya>HLirE z#=pbeWcBeWAaTd%m|n4!^E~T|&jHSj_?)~~a0Xs*YLI7B1+!gqz&a+gV*)yMFK1x_ zal3=ihIWnMzI#L{&{iQg4V0Uz--m>tgu;#?W#0*_z&SCY2ykseX%VO@+?OGf5tzIl z@%eG_xWwytCggI_Rw{J5#7f{|4cjV!C-J_Y6W*Yx!Svm;p%63@D@dva<9m}z0k0%Y ziiK$pAnPu%k!R7#c91Sl?y;gzvP-0s>UYe5B&q8WQvxkXhQR}ptmausiUsDrn}Rhg zrz`{j60~g6-kVa>VbcE8iGV$+ReUWn+JoM4f%wzPaMf?Z6TQw1?SGZgVqYL{eYLxZw! z3UmSIOh8N;c_+!v=TVoPco(X2g`r%#|9}OVU22>cDpb8j>IQL}(TD{Zl8iEe+8>OQ zz@ZUi-U{H*#hWjA`tVHl;sqO;g=fY3NF4>7IKrk~p?EYVkS=_?ZYLGnlAb&fxJU@Q zgbs`mfEP#7&6N-kvgy3_M9O>WQM$qrT66B4khKGqqZXmR8-)A*fD`Kkn~>j9#)4`8RcM(C@t!3@(;$Ssw+QJ2b2x0N0GQTKg1<7uM@M$<0>sUw6R+Qig@a*2I*Hhb$qaraXG{ew$;d#PIH7>Fp*qrL zz)sLWND9LMly`<7uBA68G;x_U_p+lvX+bk?e`9WOS@b8_lCsCfvp+J+rlAxL@HKEk zX;F`p;JSpPeHbY`oo8VrbhcFTY=yaKE(j%gwn)xA5E2eFf+FqK)S899+~24M-|a>5$I&j`JQZG`QForISNuMyrOOkgLc8@WuvxrCL3 zs|i0KtS6KS_Y)o`{D$x%;U&VWY;cAVo-efdH=&+!1x7B7SpjqD|182X!nYAk8{yj` z3BJLur(^VVx?Ol%K>LU|GNG54hcsq=hRR4*JKeXW{ZZ3;HSI0U&M;YsVmT0DGoopF zq`KmcOw~YzW~YpuoM|+z*0jTBRUD^jx3C+aZPB#rEUNf(O?$wXXzk`R9JL=->)xwb zUsqSXY3-)kZPjb*H*c+V)NZcxJ()Ee0T+>lt+yFZ*%~8Eec&)Q_=MAy!4JN9s135# zVx!3n3t>Yoe3^x{h3rhRk*oIoRJ=KryXv!7HF4a0-^Jz7?PT_qE!=T7xN@#xcqvZS L>5j41s;vJ2H=-1C diff --git a/src/ui.c b/src/ui.c index 654be97..2aa6c32 100644 --- a/src/ui.c +++ b/src/ui.c @@ -12,6 +12,32 @@ static const char *FONT_CANDIDATES[] = { }; static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color); +static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, const char *text, SDL_Color color, int *width, int *height); + +static void text_texture_destroy(UiTextTexture *text_texture) { + if (!text_texture) { + return; + } + + if (text_texture->texture) { + SDL_DestroyTexture(text_texture->texture); + } + memset(text_texture, 0, sizeof(*text_texture)); +} + +static int text_texture_init(UiTextTexture *text_texture, + SDL_Renderer *renderer, + TTF_Font *font, + const char *text, + SDL_Color color) { + if (!text_texture) { + return -1; + } + + memset(text_texture, 0, sizeof(*text_texture)); + text_texture->texture = text_to_texture(renderer, font, text, color, &text_texture->width, &text_texture->height); + return text_texture->texture ? 0 : -1; +} static void set_draw_color(SDL_Renderer *renderer, SDL_Color color) { SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); @@ -41,23 +67,18 @@ static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, cons return texture; } -static void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, int x, int y, SDL_Color color) { - SDL_Texture *texture; +static void draw_cached_text(SDL_Renderer *renderer, const UiTextTexture *text_texture, int x, int y) { SDL_Rect dst; - int width = 0; - int height = 0; - texture = text_to_texture(renderer, font, text, color, &width, &height); - if (!texture) { + if (!text_texture || !text_texture->texture) { return; } dst.x = x; dst.y = y; - dst.w = width; - dst.h = height; - SDL_RenderCopy(renderer, texture, NULL, &dst); - SDL_DestroyTexture(texture); + dst.w = text_texture->width; + dst.h = text_texture->height; + SDL_RenderCopy(renderer, text_texture->texture, NULL, &dst); } static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) { @@ -106,22 +127,20 @@ static void format_clock_label(char *buffer, size_t buffer_size, time_t now, int strftime(buffer, buffer_size, "%I:%M %p", &local_time); } -static void draw_timeline_header(SDL_Renderer *renderer, const UiFonts *fonts, SDL_Rect rect, time_t now) { - char label[32]; +static void draw_timeline_header_cached(SDL_Renderer *renderer, const UiCache *cache, SDL_Rect rect) { int segments = 4; - draw_text(renderer, fonts->medium, "FRI 6/30", rect.x + 12, rect.y + 8, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->timeline_day, rect.x + 12, rect.y + 8); for (int i = 0; i < segments; ++i) { int x = rect.x + (rect.w * i) / segments; - format_clock_label(label, sizeof(label), now, (int) ((TIMELINE_VISIBLE_SECONDS / 60.0 / segments) * i)); - draw_text(renderer, fonts->small, label, x + 6, rect.y + 10, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->timeline_labels[i], x + 6, rect.y + 10); set_draw_color(renderer, COLOR_SLATE); SDL_RenderDrawLine(renderer, x, rect.y + rect.h - 4, x, rect.y + rect.h + 5 * 76); } } -static void draw_footer_legend(SDL_Renderer *renderer, const UiFonts *fonts, int window_width, int window_height) { +static void draw_footer_legend(SDL_Renderer *renderer, const UiCache *cache, int window_width, int window_height) { SDL_Rect footer = {0, window_height - 54, window_width, 54}; SDL_Rect chip = {window_width / 2 - 170, window_height - 40, 24, 24}; @@ -130,26 +149,26 @@ static void draw_footer_legend(SDL_Renderer *renderer, const UiFonts *fonts, int SDL_RenderDrawLine(renderer, footer.x, footer.y, footer.x + footer.w, footer.y); fill_rect(renderer, &chip, COLOR_HIGHLIGHT_YELLOW); - draw_text(renderer, fonts->medium, "A", chip.x + 7, chip.y + 2, COLOR_TEXT_DARK); - draw_text(renderer, fonts->medium, "Time", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->footer_a, chip.x + 7, chip.y + 2); + draw_cached_text(renderer, &cache->footer_time, chip.x + 34, chip.y + 1); chip.x += 132; fill_rect(renderer, &chip, COLOR_DEEP_BLUE); - draw_text(renderer, fonts->medium, "B", chip.x + 7, chip.y + 2, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->medium, "Theme", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->footer_b, chip.x + 7, chip.y + 2); + draw_cached_text(renderer, &cache->footer_theme, chip.x + 34, chip.y + 1); chip.x += 136; fill_rect(renderer, &chip, COLOR_HINT_RED); - draw_text(renderer, fonts->medium, "C", chip.x + 7, chip.y + 2, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->medium, "Title", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK); + draw_cached_text(renderer, &cache->footer_c, chip.x + 7, chip.y + 2); + draw_cached_text(renderer, &cache->footer_title, chip.x + 34, chip.y + 1); } -void ui_render_no_media(SDL_Renderer *renderer, const UiFonts *fonts) { +void ui_render_no_media(SDL_Renderer *renderer, const UiCache *cache) { SDL_Rect full = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}; fill_rect(renderer, &full, COLOR_NAVY_DARK); - draw_text(renderer, fonts->large, "Passport-C Media Player", 58, 80, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->medium, "No channels found in ./media", 58, 136, COLOR_HIGHLIGHT_YELLOW); - draw_text(renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", 58, 176, COLOR_PALE_BLUE); + draw_cached_text(renderer, &cache->no_media_title, 58, 80); + draw_cached_text(renderer, &cache->no_media_body, 58, 136); + draw_cached_text(renderer, &cache->no_media_hint, 58, 176); } void ui_render_fullscreen(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height) { @@ -163,10 +182,12 @@ void ui_render_guide(SDL_Renderer *renderer, int texture_width, int texture_height, const UiFonts *fonts, + UiCache *cache, const ChannelList *channels, int active_channel, - time_t app_start_time, - time_t now) { + Uint64 app_start_ticks, + Uint64 now_ticks, + time_t now_wall) { SDL_Rect full = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}; SDL_Rect header_card = {0, 0, 390, 168}; SDL_Rect preview = {820, 0, 460, 210}; @@ -189,13 +210,22 @@ void ui_render_guide(SDL_Renderer *renderer, const Channel *current = &channels->items[active_channel]; snprintf(detail, sizeof(detail), "%.0f min - %s", current->duration_seconds / 60.0, current->file_name); fill_rect(renderer, &(SDL_Rect){0, 0, 390, 38}, COLOR_HIGHLIGHT_YELLOW); - draw_text(renderer, fonts->large, current->name, 26, 44, COLOR_TEXT_DARK); - draw_text(renderer, fonts->medium, current->program_title, 20, 92, COLOR_TEXT_DARK); - draw_text(renderer, fonts->small, detail, 20, 124, COLOR_TEXT_DARK); - draw_text(renderer, fonts->medium, "Nick * 56", preview.x + preview.w - 126, preview.y + preview.h - 34, COLOR_TEXT_LIGHT); + draw_cached_text(renderer, &cache->channels[active_channel].name_medium, 26, 52); + draw_cached_text(renderer, &cache->channels[active_channel].program_medium_dark, 20, 92); + draw_cached_text(renderer, &cache->channels[active_channel].detail_small_dark, 20, 124); + draw_cached_text(renderer, &cache->preview_badge, preview.x + preview.w - 126, preview.y + preview.h - 34); } - draw_timeline_header(renderer, fonts, header_row, now); + if (cache->timeline_label_slot != now_wall / 60) { + char label[32]; + for (int i = 0; i < 4; ++i) { + format_clock_label(label, sizeof(label), now_wall, (int) ((TIMELINE_VISIBLE_SECONDS / 60.0 / 4) * i)); + text_texture_destroy(&cache->timeline_labels[i]); + text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_DARK); + } + cache->timeline_label_slot = now_wall / 60; + } + draw_timeline_header_cached(renderer, cache, header_row); if (start_index < 0) { start_index = 0; @@ -223,34 +253,132 @@ void ui_render_guide(SDL_Renderer *renderer, { const Channel *channel = &channels->items[channel_index]; - char number[16]; - double live_position = channel_live_position(channel, app_start_time, now); + double live_position = channel_live_position_precise(channel, app_start_ticks, now_ticks); double pixels_per_second = timeline_w / TIMELINE_VISIBLE_SECONDS; int block_x = timeline_x - (int) (live_position * pixels_per_second); int block_w = (int) (channel->duration_seconds * pixels_per_second); SDL_Rect block = {block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8}; - snprintf(number, sizeof(number), "%d", channel->number); - draw_text(renderer, fonts->medium, channel->name, 20, row_rect.y + 12, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->medium, number, 176, row_rect.y + 12, COLOR_TEXT_LIGHT); - draw_text(renderer, fonts->small, channel->file_name, 20, row_rect.y + 38, COLOR_PALE_BLUE); + draw_cached_text(renderer, &cache->channels[channel_index].name_medium, 20, row_rect.y + 12); + draw_cached_text(renderer, &cache->channels[channel_index].number_medium, 176, row_rect.y + 12); + draw_cached_text(renderer, &cache->channels[channel_index].file_small, 20, row_rect.y + 38); fill_rect(renderer, &timeline_rect, COLOR_NAVY_DARK); SDL_RenderSetClipRect(renderer, &clip); fill_rect(renderer, &block, is_selected ? COLOR_HIGHLIGHT_YELLOW : COLOR_DEEP_BLUE); stroke_rect(renderer, &block, COLOR_PALE_BLUE); - draw_text(renderer, - is_selected ? fonts->medium : fonts->small, - channel->program_title, - block.x + 10, - block.y + 10, - is_selected ? COLOR_TEXT_DARK : COLOR_TEXT_LIGHT); + draw_cached_text(renderer, + is_selected ? &cache->channels[channel_index].program_medium_dark + : &cache->channels[channel_index].program_small_light, + block.x + 10, + block.y + 10); SDL_RenderSetClipRect(renderer, NULL); stroke_rect(renderer, &timeline_rect, COLOR_SLATE); } } - draw_footer_legend(renderer, fonts, WINDOW_WIDTH, WINDOW_HEIGHT); + draw_footer_legend(renderer, cache, WINDOW_WIDTH, WINDOW_HEIGHT); +} + +int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, const ChannelList *channels) { + char buffer[256]; + + if (!cache || !renderer || !fonts) { + return -1; + } + + memset(cache, 0, sizeof(*cache)); + cache->renderer = renderer; + cache->timeline_label_slot = -1; + cache->timeline_labels = calloc(4, sizeof(UiTextTexture)); + if (!cache->timeline_labels) { + ui_cache_destroy(cache); + return -1; + } + + if (text_texture_init(&cache->no_media_title, renderer, fonts->large, "Passport-C Media Player", COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->no_media_body, renderer, fonts->medium, "No channels found in ./media", COLOR_HIGHLIGHT_YELLOW) != 0 || + text_texture_init(&cache->no_media_hint, renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", COLOR_PALE_BLUE) != 0 || + text_texture_init(&cache->timeline_day, renderer, fonts->medium, "FRI 6/30", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->footer_a, renderer, fonts->medium, "A", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->footer_time, renderer, fonts->medium, "Time", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->footer_b, renderer, fonts->medium, "B", COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->footer_theme, renderer, fonts->medium, "Theme", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->footer_c, renderer, fonts->medium, "C", COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->footer_title, renderer, fonts->medium, "Title", COLOR_TEXT_DARK) != 0 || + text_texture_init(&cache->preview_badge, renderer, fonts->medium, "Nick * 56", COLOR_TEXT_LIGHT) != 0) { + ui_cache_destroy(cache); + return -1; + } + + if (channels && channels->count > 0) { + cache->channels = calloc((size_t) channels->count, sizeof(UiChannelCache)); + if (!cache->channels) { + ui_cache_destroy(cache); + return -1; + } + cache->channel_count = channels->count; + + for (int i = 0; i < channels->count; ++i) { + const Channel *channel = &channels->items[i]; + snprintf(buffer, sizeof(buffer), "%d", channel->number); + if (text_texture_init(&cache->channels[i].name_medium, renderer, fonts->medium, channel->name, COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->channels[i].number_medium, renderer, fonts->medium, buffer, COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->channels[i].file_small, renderer, fonts->small, channel->file_name, COLOR_PALE_BLUE) != 0 || + text_texture_init(&cache->channels[i].program_small_light, renderer, fonts->small, channel->program_title, COLOR_TEXT_LIGHT) != 0 || + text_texture_init(&cache->channels[i].program_medium_dark, renderer, fonts->medium, channel->program_title, COLOR_TEXT_DARK) != 0) { + ui_cache_destroy(cache); + return -1; + } + + snprintf(buffer, sizeof(buffer), "%.0f min - %s", channel->duration_seconds / 60.0, channel->file_name); + if (text_texture_init(&cache->channels[i].detail_small_dark, renderer, fonts->small, buffer, COLOR_TEXT_DARK) != 0) { + ui_cache_destroy(cache); + return -1; + } + } + } + + return 0; +} + +void ui_cache_destroy(UiCache *cache) { + if (!cache) { + return; + } + + text_texture_destroy(&cache->no_media_title); + text_texture_destroy(&cache->no_media_body); + text_texture_destroy(&cache->no_media_hint); + text_texture_destroy(&cache->timeline_day); + text_texture_destroy(&cache->footer_a); + text_texture_destroy(&cache->footer_time); + text_texture_destroy(&cache->footer_b); + text_texture_destroy(&cache->footer_theme); + text_texture_destroy(&cache->footer_c); + text_texture_destroy(&cache->footer_title); + text_texture_destroy(&cache->preview_badge); + + if (cache->timeline_labels) { + for (int i = 0; i < 4; ++i) { + text_texture_destroy(&cache->timeline_labels[i]); + } + free(cache->timeline_labels); + } + + if (cache->channels) { + for (int i = 0; i < cache->channel_count; ++i) { + text_texture_destroy(&cache->channels[i].name_medium); + text_texture_destroy(&cache->channels[i].number_medium); + text_texture_destroy(&cache->channels[i].file_small); + text_texture_destroy(&cache->channels[i].program_small_light); + text_texture_destroy(&cache->channels[i].program_medium_dark); + text_texture_destroy(&cache->channels[i].detail_small_dark); + } + free(cache->channels); + } + + memset(cache, 0, sizeof(*cache)); } int ui_load_fonts(UiFonts *fonts) { diff --git a/src/ui.h b/src/ui.h index c251352..523b4fa 100644 --- a/src/ui.h +++ b/src/ui.h @@ -14,18 +14,56 @@ typedef struct UiFonts { TTF_Font *large; } UiFonts; +typedef struct UiTextTexture { + SDL_Texture *texture; + int width; + int height; +} UiTextTexture; + +typedef struct UiChannelCache { + UiTextTexture name_medium; + UiTextTexture number_medium; + UiTextTexture file_small; + UiTextTexture program_small_light; + UiTextTexture program_medium_dark; + UiTextTexture detail_small_dark; +} UiChannelCache; + +typedef struct UiCache { + SDL_Renderer *renderer; + UiTextTexture no_media_title; + UiTextTexture no_media_body; + UiTextTexture no_media_hint; + UiTextTexture timeline_day; + UiTextTexture footer_a; + UiTextTexture footer_time; + UiTextTexture footer_b; + UiTextTexture footer_theme; + UiTextTexture footer_c; + UiTextTexture footer_title; + UiTextTexture preview_badge; + UiTextTexture *timeline_labels; + time_t timeline_label_slot; + UiChannelCache *channels; + int channel_count; +} UiCache; + void ui_render_fullscreen(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height); void ui_render_guide(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height, const UiFonts *fonts, + UiCache *cache, const ChannelList *channels, int active_channel, - time_t app_start_time, - time_t now); -void ui_render_no_media(SDL_Renderer *renderer, const UiFonts *fonts); + Uint64 app_start_ticks, + Uint64 now_ticks, + time_t now_wall); +void ui_render_no_media(SDL_Renderer *renderer, const UiCache *cache); int ui_load_fonts(UiFonts *fonts); void ui_destroy_fonts(UiFonts *fonts); +int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, const ChannelList *channels); +void ui_cache_destroy(UiCache *cache); #endif diff --git a/src/ui.o b/src/ui.o index de0ec258ca607caf47331b41c16bf5873abfd092..8df4c7d6a94c62e1d229217e2c02228c2958b86d 100644 GIT binary patch literal 15504 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 literal 10680 zcmb_i4Rln;b$*gou>5pac8#qzjd_k2AAzIQF2+={-C7T=#1meD3X+r5KVp&I!p5sz zWq(|VIWA-+;@4#fPMjPkX&Z9-=hMHHCwft zwVO2KVWZdR={4O zP5WKbewjg0!Z0Dn!La=wMD_S?q3T~c?U$q84F~ZYk9y;i-o-8-ljs_`obg4y zNtch*6d)Wt6}B&X7l(Xxj3I1aVYQh%q(AbQ@u=~b@%TaS!jPu*t~2eQ!_0=T?ec}~ zu+Poxy#&)Pa)p;hPI-6z8R(H?-d%r$I_e!dT8w(%{bxM?2D_dbnD$#hxEIJnru|>UaZ387;gQGS5v?pAyv(~4x!fV*ibnn3g~J#HuGrnn z1x)+WSOI`>XJd_zsGv{R+=_mNy-0f|ii z&9!3D%xB$HTF&`N{ zUG^bv3yshG#Z1`#PIEN{8FyQL*f#`{BD0m^xm$MRrmKlwFKIjE3llIT0q^2r-!^zc zEP2M)4Hq$jk2-b{oq+bAK|TJ8%DMh}PKu`BWr>-EjBiL162P^%eZyVLZw62k2<}NX3BmWuq{m-%KUzzfL zXA&D@crxS0cKpiiZcPj2_xdOb!A%<4gGFO(-c6q@=3nwX1G+Px!20_RRZ1#^CT zq1_En1?;W9W*W2W=P>J~pXK2S zr5dniqw27cE4sWR%ZtS#Mh*ujMvC6fBV>H5iG`@ouW|1u+Y3XkmXqGDlexERTTXbp z4w(H@VLRi~!|-wI_df!M89UH@6J_()$S=%my9ovnAg}EaM1wi1K_jidz1Q~~Djpu> zhUbZ=vChSv@&c$r`z+uu5fDB3&t}Jj_6ibUmG6iRY8kOY5Z_F={Vo$3B}a?} z@Velw`xR8EdAwmCyaaN*jxfXfxq^uWLi|lF#>tYHz_uaB7P?))=m>dTL>-;5El0iK z$-;7J6MgqmSJJjAFLXI)D0SR}f?h z8)spWJVpyD`TjxWTjt|Ay|eK35ayvMyleWaU=;qQxPW|l8JTM+?u2GsEhrS5IgS;p z2RkFm(Xa{uCiHz1`dGr-e-BzEAzcc6>@u5BU*g1!&qD?6Up`^X=;C1dpL_|7++Or*nZR zMy8ZwlpIFcn~ro!=^`=<=IpZ5s;8g5#3G7p<;ngA_D_}BtEM-Aotpr?!OLT`$ZUPL z^ZC+Q@(Zn@i^|N_)ckVRoPJ+gBkpSK1Qw<|^M5mCGjxBvAFp#vC)myHhv?QJ@5V+S z?tu#9EH8EQXm_npC+IFI;lN%p;(l0Yx1nX=VgpO zN=%eIr~jHUEyI_|+lrE{rTzl?G1IMd4Ff|>$d{=J`-ViW!T9dn*{6|W-VjrteiQ;{ zCy{H!`qDYy#_pZ@2dP~vJpUBFgA}T+OwpakJkkbFA|fx%?;gd)rpRNuWFO$*guqz;!R|;pJ(Nsk8r$?P zYakZUyW^29R!Uo))cZF@5(z7w)}zU6VnB~2bbr$z@o0z$o2Ge%*1;GUrB#Fw!un8GO=W0X;Zk=zQI5~onQWqy*q`BCd&@& z@E1TV!zoYY70{=6lh@#1B510J|1Lp)i}av>IjSF%9?%q}^zgE}mDSspx1RY@BmRy) zsI6>kyH9TjSsP-JgdSYxU+NDu23r=3rbfG)O4S$FYE_%-sy;D)wtF{pKLG4ESdXUt z71b*|b-Supc%zKJlY`b1q)Mzf#@qsozzz%A*%t zt2_-mYgc&!J7#rynsc*PdCt3Po^;RkG#hM+n0sl8<~)d#??W^{lumkec&Hd{N?RBAMmy1sk@WR*6%1U8gf#9ziGd^c;% zkz}b3uoi;bQSKFZImS5y{XC=1V9$dLpTp+Cn&K4_xUPa|vo+J^Xng!~=W5oh)z*|D z*6LyJxtiO7|1P6zwa+^6^^Bf-gFQSw)@l(4f1J^CSnQ}>DS@w7L9{nBx>g%-;7<#j zk5ld(7Wfloh_(Je;DZi)x4_d5oW2Ao8D$;#9}1kWTe%> ze$B%j!}24Vw*sF@zm|>p`?Y~o&}$jJ?xojqdIec`ko5-{Ly&O<8B37!{3hdUX73hyFJ+pSa@91gg-5vTTmwk{OzYfD*?46aU_Guf2YkxCA(%ci1{ev5T> zq%2FNxsJBv&=%GmveKDUa*GgV&^jx_nL_YRTQZ(ZF_>c=v3Pu~)t`a?`cllCzC^N* z-yO?PG#ihn`!RAt;oF#nd$oA7KN8R2(%6^MFm#lfihde197|Z5xT5yOV^3OrL&APIlK9ICuG;fMfjjMaUBOj*!Xgh+x2nfd@cGKPSp}yQB<b%Y=xT^m{ z1y}iR6IV>9{uTvS?TIURBkYoPJ}q$a8~qF*@f{WLaRvWXh5v%0pT3i&o-b1nqd4vS zj)JT6+NPpDMVj=f4$P<)0_6;ABrg(ch-v>bUC_T-7tE;HsWy1y26F zMbSU5;0qM|2Z|n5{}}~W=XFWJRsP$=6_e(ru6t0yRsIJQT-E){#6sJ9RE4a#UD!8h@SHacz*&}fB1N{ym?L492pH%RFQuL_%?k$D? zc7^|%!mrxn7WY@0mummF1y1%XRP-EB_?Ig9R0aHv3itwX4<`MTLgajZOW{+!&D?b%1KQZwZz|a z;51))WLYm765l82=N-8Gu9|S*^1JG)1DD@bCK-ZK(l6Fyz=6x}63w69GMI^MK%GhP zdXsF?@1cJ7HR2zLWFne>LprVbQ%NF-J?O`Gf=c2mfypoiw9x5i_v;c{e>y{-9zqBd zMEaA1gBGs7{zNik`B$#)ZA@n({Z9y8)~3E_DuO%t|4-4OhFmerp{`ZX-xF~pyJb8` znsN%oYO4f9-AqXcB_Q%P-C1ZH8D}znXwMNYu1RR|a?vra#Kp2nlI48T`M;H?y+Jx@ zFRMk4|C9=%EytI2tH7M{Lo{KOJ0v3~OY@g7{k}wPr~S_hM>hyLS?RJN>NTim^8cng zIm2ebN9ItTar%EFFdCokKx&x~;}5AI+H(D6O}_`tBp;y5C5l{UPL;=(c6Yn`=Ma!~OWtpzF_Zt_6#jof zFp~c%^+