ogl_beamforming

Ultrasound Beamforming Implemented with OpenGL
git clone anongit@rnpnr.xyz:ogl_beamforming.git
Log | Files | Refs | Feed | Submodules | LICENSE

Commit: 0946dec196570b1c0099bab008e313f72df09114
Parent: fa929b045583a71d3b37246300557148144c807e
Author: Randy Palamar
Date:   Sun, 23 Mar 2025 17:53:20 -0600

ui: do frame view rendering per view and only when needed

Diffstat:
Mbeamformer.c | 70+++++++++++-----------------------------------------------------------
Mbeamformer.h | 12++++--------
Mbeamformer_parameters.h | 5+++--
Mstatic.c | 6+-----
Mui.c | 473++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
5 files changed, 341 insertions(+), 225 deletions(-)

diff --git a/beamformer.c b/beamformer.c @@ -93,29 +93,6 @@ alloc_output_image(BeamformerCtx *ctx, uv3 output_dim) alloc_beamform_frame(&ctx->gl, &ctx->averaged_frame, &ctx->averaged_frame_compute_stats, try_dim, 0, s8("Beamformed_Averaged_Data")); - uv3 odim = ctx->averaged_frame.dim; - - UnloadRenderTexture(ctx->fsctx.output); - /* TODO(rnp): sometimes when accepting data on w32 something happens - * and the program will stall in vprintf in TraceLog(...) here. - * for now do this to avoid the problem */ - SetTraceLogLevel(LOG_NONE); - /* TODO: select odim.x vs odim.y */ - ctx->fsctx.output = LoadRenderTexture(odim.x, odim.z); - SetTraceLogLevel(LOG_INFO); - LABEL_GL_OBJECT(GL_FRAMEBUFFER, ctx->fsctx.output.id, s8("Rendered_View")); - GenTextureMipmaps(&ctx->fsctx.output.texture); - //SetTextureFilter(ctx->fsctx.output.texture, TEXTURE_FILTER_ANISOTROPIC_8X); - //SetTextureFilter(ctx->fsctx.output.texture, TEXTURE_FILTER_TRILINEAR); - SetTextureFilter(ctx->fsctx.output.texture, TEXTURE_FILTER_BILINEAR); - - /* NOTE(rnp): work around raylib's janky texture sampling */ - i32 id = ctx->fsctx.output.texture.id; - glTextureParameteri(id, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); - glTextureParameteri(id, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); - - f32 border_color[] = {0, 0, 0, 1}; - glTextureParameterfv(id, GL_TEXTURE_BORDER_COLOR, border_color); } } @@ -574,9 +551,8 @@ reload_compute_shader(BeamformerCtx *ctx, s8 path, ComputeShaderReloadContext *c LABEL_GL_OBJECT(GL_PROGRAM, cs->programs[csr->shader], csr->label); result = 1; } + glDeleteShader(shader_id); } - - glDeleteShader(shader_id); } else { Stream buf = arena_stream(&tmp); stream_append_s8(&buf, s8("failed to load: ")); @@ -800,7 +776,6 @@ DEBUG_EXPORT BEAMFORMER_FRAME_STEP_FN(beamformer_frame_step) if (frame->in_flight && frame->ready_to_present) { frame->in_flight = 0; ctx->display_frame_index = frame - bfi.frames; - ctx->fsctx.gen_mipmaps = 1; } } @@ -809,43 +784,20 @@ DEBUG_EXPORT BEAMFORMER_FRAME_STEP_FN(beamformer_frame_step) ctx->os.wake_thread(ctx->os.compute_worker.sync_handle); } - /* NOTE: draw output image texture using render fragment shader */ - /* TODO(rnp): each beamform frame should have its own rendered view */ - BeamformFrame *frame_to_draw = 0; - ComputeShaderStats *frame_compute_stats = 0; - BeginTextureMode(ctx->fsctx.output); - ClearBackground(PINK); - BeginShaderMode(ctx->fsctx.shader); - FragmentShaderCtx *fs = &ctx->fsctx; - glUseProgram(fs->shader.id); - u32 out_texture = 0; - if (bp->output_points.w > 1) { - frame_to_draw = &ctx->averaged_frame; - frame_compute_stats = &ctx->averaged_frame_compute_stats; - out_texture = ctx->averaged_frame.texture; - } else { - frame_to_draw = ctx->beamform_frames + ctx->display_frame_index; - frame_compute_stats = ctx->beamform_frame_compute_stats + ctx->display_frame_index; - out_texture = frame_to_draw->ready_to_present ? frame_to_draw->texture : 0; - } - glBindTextureUnit(0, out_texture); - glUniform1f(fs->db_cutoff_id, fs->db); - glUniform1f(fs->threshold_id, fs->threshold); - DrawTexture(fs->output.texture, 0, 0, WHITE); - EndShaderMode(); - EndTextureMode(); - - /* NOTE: regenerate mipmaps only when the output has actually changed */ - if (ctx->fsctx.gen_mipmaps) { - /* NOTE: shut up raylib's reporting on mipmap gen */ - SetTraceLogLevel(LOG_NONE); - GenTextureMipmaps(&ctx->fsctx.output.texture); - SetTraceLogLevel(LOG_INFO); - ctx->fsctx.gen_mipmaps = 0; + BeamformFrame *frame_to_draw; + ComputeShaderStats *frame_compute_stats; + if (bp->output_points.w > 1) { + frame_to_draw = &ctx->averaged_frame; + frame_compute_stats = &ctx->averaged_frame_compute_stats; + } else { + frame_to_draw = ctx->beamform_frames + ctx->display_frame_index; + frame_compute_stats = ctx->beamform_frame_compute_stats + ctx->display_frame_index; } draw_ui(ctx, input, frame_to_draw, frame_compute_stats); + ctx->fsctx.updated = 0; + if (WindowShouldClose()) ctx->should_exit = 1; } diff --git a/beamformer.h b/beamformer.h @@ -104,14 +104,10 @@ typedef struct { } ComputeShaderCtx; typedef struct { - Shader shader; - RenderTexture2D output; - /* TODO: cleanup: X macro? */ - i32 db_cutoff_id; - i32 threshold_id; - f32 db; - f32 threshold; - b32 gen_mipmaps; + Shader shader; + b32 updated; + i32 db_cutoff_id; + i32 threshold_id; } FragmentShaderCtx; typedef enum { diff --git a/beamformer_parameters.h b/beamformer_parameters.h @@ -43,6 +43,7 @@ typedef enum { typedef struct { v4 output_min_coordinate; /* [m] Back-Top-Left corner of output region (w ignored) */ v4 output_max_coordinate; /* [m] Front-Bottom-Right corner of output region (w ignored)*/ + uv4 output_points; /* Width * Height * Depth * (Frame Average Count) */ f32 sampling_frequency; /* [Hz] */ f32 center_frequency; /* [Hz] */ f32 speed_of_sound; /* [m/s] */ @@ -58,7 +59,6 @@ typedef struct { f32 transmit_angles[256]; /* [radians] Transmit Angles for each transmit of a RCA imaging scheme*/ f32 xdc_transform[16]; /* IMPORTANT: column major order */ uv4 dec_data_dim; /* Samples * Channels * Acquisitions; last element ignored */ - uv4 output_points; /* Width * Height * Depth * (Frame Average Count) */ f32 xdc_element_pitch[2]; /* [m] Transducer Element Pitch {row, col} */ uv2 rf_raw_dim; /* Raw Data Dimensions */ i32 transmit_mode; /* Method/Orientation of Transmit */ @@ -70,6 +70,7 @@ typedef struct { /* UI Parameters */ v4 output_min_coordinate; /* [m] Back-Top-Left corner of output region (w ignored) */ v4 output_max_coordinate; /* [m] Front-Bottom-Right corner of output region (w ignored)*/ + uv4 output_points; /* Width * Height * Depth * (Frame Average Count) */ f32 sampling_frequency; /* [Hz] */ f32 center_frequency; /* [Hz] */ f32 speed_of_sound; /* [m/s] */ @@ -96,7 +97,6 @@ layout(std140, binding = 0) uniform parameters {\n\ vec4 transmit_angles[64]; /* [radians] Transmit Angles for each transmit of a RCA imaging scheme*/\n\ mat4 xdc_transform; /* IMPORTANT: column major order */\n\ uvec4 dec_data_dim; /* Samples * Channels * Acquisitions; last element ignored */\n\ - uvec4 output_points; /* Width * Height * Depth * (Frame Average Count) */\n\ vec2 xdc_element_pitch; /* [m] Transducer Element Pitch {row, col} */\n\ uvec2 rf_raw_dim; /* Raw Data Dimensions */\n\ int transmit_mode; /* Method/Orientation of Transmit */\n\ @@ -105,6 +105,7 @@ layout(std140, binding = 0) uniform parameters {\n\ float time_offset; /* pulse length correction time [s] */\n\ vec4 output_min_coord; /* [m] Top left corner of output region */\n\ vec4 output_max_coord; /* [m] Bottom right corner of output region */\n\ + uvec4 output_points; /* Width * Height * Depth * (Frame Average Count) */\n\ float sampling_frequency; /* [Hz] */\n\ float center_frequency; /* [Hz] */\n\ float speed_of_sound; /* [m/s] */\n\ diff --git a/static.c b/static.c @@ -195,7 +195,7 @@ static FILE_WATCH_CALLBACK_FN(reload_render_shader) ctx->shader = updated_fs; ctx->db_cutoff_id = GetShaderLocation(updated_fs, "u_db_cutoff"); ctx->threshold_id = GetShaderLocation(updated_fs, "u_threshold"); - ctx->gen_mipmaps = 1; + ctx->updated = 1; } return 1; @@ -295,9 +295,6 @@ setup_beamformer(BeamformerCtx *ctx, Arena *memory) ctx->beamform_work_queue = push_struct(memory, BeamformWorkQueue); - ctx->fsctx.db = -50.0f; - ctx->fsctx.threshold = 40.0f; - ctx->params = os_open_shared_memory_area(OS_SMEM_NAME, sizeof(*ctx->params)); /* TODO: properly handle this? */ ASSERT(ctx->params); @@ -349,7 +346,6 @@ setup_beamformer(BeamformerCtx *ctx, Arena *memory) s8 render = s8(static_path_join("shaders", "render.glsl")); reload_render_shader(&ctx->os, render, (iptr)&ctx->fsctx, *memory); os_add_file_watch(&ctx->os, memory, render, reload_render_shader, (iptr)&ctx->fsctx); - ctx->fsctx.gen_mipmaps = 0; ctx->ready_for_rf = 1; } diff --git a/ui.c b/ui.c @@ -6,6 +6,13 @@ * [ ]: global menu * [ ]: allow views to collapse to just their title bar * [ ]: enforce a mininum region size or allow regions themselves to scroll + * [ ]: move remaining fragment shader stuff into ui + * [ ]: refactor: draw_left_aligned_list() + * [ ]: refactor: draw_variable_table() + * [ ]: refactor: add_variable_no_link() + * [ ]: fix leaking in ui_variable_free for UIView (when close, menu are populated) + * [ ]: refactor: draw_text with flags (OUTLINED|ROTATED|LIMITED|etc..) + * [ ]: refactor: draw_text_limited should clamp to rect and measure text itself */ #define BG_COLOUR (v4){.r = 0.15, .g = 0.12, .b = 0.13, .a = 1.0} @@ -133,11 +140,11 @@ enum ui_view_flags { typedef struct { /* NOTE(rnp): superset of group, group must come first */ VariableGroup group; - Variable *close; - Variable *menu; - f32 needed_height; - f32 offset; - u32 flags; + Variable *close; + Variable *menu; + f32 needed_height; + f32 offset; + u32 flags; } UIView; typedef struct { @@ -160,7 +167,7 @@ enum variable_flags { V_TEXT = 1 << 1, V_BUTTON = 1 << 2, V_CAUSES_COMPUTE = 1 << 29, - V_GEN_MIPMAPS = 1 << 30, + V_UPDATE_VIEW = 1 << 30, }; struct Variable { @@ -211,11 +218,21 @@ typedef enum { typedef struct { ScaleBar lateral_scale_bar; ScaleBar axial_scale_bar; + + v4 min_coordinate; + v4 max_coordinate; + + Variable threshold; + Variable dynamic_range; + /* TODO(rnp): hack, this needs to be removed to support multiple views */ FragmentShaderCtx *ctx; - void *store; - void *next; + BeamformFrame *frame; + void *next; + + RenderTexture2D rendered_view; BeamformerFrameViewType type; + b32 needs_update; } BeamformerFrameView; typedef struct { @@ -236,18 +253,23 @@ typedef struct { Variable *scratch_variable; Variable scratch_variables[2]; + BeamformerFrameView *views; + BeamformerFrameView *view_freelist; + InteractionState interaction; InputState text_input_state; v2_sll *scale_bar_savepoint_freelist; - BeamformFrame *latest_frame; - ComputeShaderStats *latest_compute_stats; v2 ruler_start_p; v2 ruler_stop_p; u32 ruler_state; + b32 latest_frame_changed; + BeamformFrame *latest_frame; + ComputeShaderStats *latest_compute_stats; + BeamformerUIParameters params; b32 flush_params; @@ -313,6 +335,7 @@ stream_append_variable(Stream *s, Variable *var) } } +/* TODO(rnp): this function leaks for UIView */ static void ui_variable_free(BeamformerUI *ui, Variable *var) { @@ -322,6 +345,12 @@ ui_variable_free(BeamformerUI *ui, Variable *var) if (var->type == VT_GROUP || var->type == VT_UI_VIEW) { var = var->u.group.first; } else { + if (var->type == VT_BEAMFORMER_FRAME_VIEW) { + /* TODO(rnp): instead there should be a way of linking these up */ + BeamformerFrameView *bv = var->u.generic; + SLLPush(bv, ui->view_freelist); + } + Variable *next = SLLPush(var, ui->variable_freelist); if (next) { var = next; @@ -346,25 +375,32 @@ ui_view_free(BeamformerUI *ui, Variable *view) } static Variable * -add_variable(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, u32 flags, VariableType type, Font font) +fill_variable(Variable *var, Variable *group, s8 name, u32 flags, VariableType type, Font font) { - Variable *result = SLLPop(ui->variable_freelist); - if (result) zero_struct(result); - else result = push_struct(arena, Variable); - result->flags = flags; - result->type = type; - result->name = name; - result->parent = group; - result->name_width = measure_text(font, name).x; + var->flags = flags; + var->type = type; + var->name = name; + var->parent = group; + var->name_width = measure_text(font, name).x; if (group && (group->type == VT_GROUP || group->type == VT_UI_VIEW)) { - if (group->u.group.last) group->u.group.last = group->u.group.last->next = result; - else group->u.group.last = group->u.group.first = result; + if (group->u.group.last) group->u.group.last = group->u.group.last->next = var; + else group->u.group.last = group->u.group.first = var; - group->u.group.max_name_width = MAX(group->u.group.max_name_width, result->name_width); + group->u.group.max_name_width = MAX(group->u.group.max_name_width, var->name_width); } - return result; + return var; +} + +static Variable * +add_variable(BeamformerUI *ui, Variable *group, Arena *arena, s8 name, u32 flags, + VariableType type, Font font) +{ + Variable *result = SLLPop(ui->variable_freelist); + if (result) zero_struct(result); + else result = push_struct(arena, Variable); + return fill_variable(result, group, name, flags, type, font); } static Variable * @@ -492,14 +528,6 @@ add_beamformer_parameters_view(Variable *parent, BeamformerCtx *ctx) add_beamformer_variable_f32(ui, result, &ui->arena, s8("F#:"), s8(""), &bp->f_number, (v2){.y = 1e3}, 1, 0.1, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - add_beamformer_variable_f32(ui, result, &ui->arena, s8("Dynamic Range:"), s8("[dB]"), - &ctx->fsctx.db, (v2){.x = -120}, 1, 1, - V_INPUT|V_TEXT|V_GEN_MIPMAPS, ui->font); - - add_beamformer_variable_f32(ui, result, &ui->arena, s8("Threshold:"), s8(""), - &ctx->fsctx.threshold, (v2){.y = 240}, 1, 1, - V_INPUT|V_TEXT|V_GEN_MIPMAPS, ui->font); - return result; } @@ -508,10 +536,18 @@ add_beamformer_frame_view(BeamformerUI *ui, Variable *parent, Arena *arena, Beam { /* TODO(rnp): this can be closable once we have a way of opening new views */ Variable *result = add_ui_view(ui, parent, arena, s8(""), UI_VIEW_CUSTOM_TEXT, 0); - add_variable(ui, result, &ui->arena, s8(""), 0, VT_BEAMFORMER_FRAME_VIEW, ui->small_font); - - BeamformerFrameView *bv = result->u.group.first->u.generic = push_struct(arena, typeof(*bv)); - bv->type = type; + Variable *var = add_variable(ui, result, &ui->arena, s8(""), 0, VT_BEAMFORMER_FRAME_VIEW, + ui->small_font); + + BeamformerFrameView *bv = var->u.generic = push_struct(arena, typeof(*bv)); + bv->type = type; + SLLPush(bv, ui->views); + fill_variable(&bv->dynamic_range, var, s8("Dynamic Range:"), V_INPUT|V_TEXT|V_UPDATE_VIEW, + VT_F32, ui->small_font); + fill_variable(&bv->threshold, var, s8("Threshold:"), V_INPUT|V_TEXT|V_UPDATE_VIEW, + VT_F32, ui->small_font); + bv->dynamic_range.u.f32 = -50.0f; + bv->threshold.u.f32 = 40.0f; return result; } @@ -539,15 +575,93 @@ add_compute_stats_view(BeamformerUI *ui, Variable *parent, Arena *arena, Variabl return result; } -static BeamformFrame * -beamformer_frame_view_frame(BeamformerFrameView *bv) +static void +resize_frame_view(BeamformerFrameView *view, uv2 dim) { - BeamformFrame *result; - switch (bv->type) { - case FVT_LATEST: result = *(BeamformFrame **)bv->store; break; - default: result = bv->store; break; + UnloadRenderTexture(view->rendered_view); + /* TODO(rnp): sometimes when accepting data on w32 something happens + * and the program will stall in vprintf in TraceLog(...) here. + * for now do this to avoid the problem */ + //SetTraceLogLevel(LOG_NONE); + view->rendered_view = LoadRenderTexture(dim.x, dim.y); + //SetTraceLogLevel(LOG_INFO); + + /* TODO(rnp): add some ID for the specific view here */ + LABEL_GL_OBJECT(GL_FRAMEBUFFER, view->rendered_view.id, s8("Frame View")); + LABEL_GL_OBJECT(GL_TEXTURE, view->rendered_view.texture.id, s8("Frame View Texture")); + GenTextureMipmaps(&view->rendered_view.texture); + + //SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_ANISOTROPIC_8X); + //SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_TRILINEAR); + //SetTextureFilter(view->rendered_view.texture, TEXTURE_FILTER_BILINEAR); + + /* NOTE(rnp): work around raylib's janky texture sampling */ + i32 id = view->rendered_view.texture.id; + glTextureParameteri(id, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTextureParameteri(id, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + glTextureParameterfv(id, GL_TEXTURE_BORDER_COLOR, (f32 []){0, 0, 0, 1}); +} + +static b32 +view_update(BeamformerUI *ui, BeamformerFrameView *view) +{ + b32 needs_resize = 0; + uv2 current = {.w = view->rendered_view.texture.width, .h = view->rendered_view.texture.height}; + uv2 target; + + switch (view->type) { + case FVT_LATEST: { + /* TODO(rnp): x-z or y-z */ + target = (uv2){.w = ui->params.output_points.x, .h = ui->params.output_points.z}; + needs_resize = !uv2_equal(current, target) && !uv2_equal(target, (uv2){0}); + view->needs_update |= view->frame != ui->latest_frame; + view->frame = ui->latest_frame; + } break; + default: { + /* TODO(rnp): add method of setting a target size in frame view */ + target = (uv2){.w = ui->params.output_points.x, .h = ui->params.output_points.z}; + needs_resize = !uv2_equal(current, target) && !uv2_equal(target, (uv2){0}); + } break; } - return result; + + if (needs_resize && view->frame) { + resize_frame_view(view, target); + view->needs_update = 1; + } + + return (view->ctx->updated || view->needs_update) && view->frame; +} + +static void +update_frame_views(BeamformerUI *ui) +{ + for (BeamformerFrameView *view = ui->views; view; view = view->next) { + if (view_update(ui, view)) { + BeamformFrame *frame = view->frame; + BeginTextureMode(view->rendered_view); + ClearBackground(PINK); + BeginShaderMode(view->ctx->shader); + glUseProgram(view->ctx->shader.id); + glBindTextureUnit(0, frame->texture); + glUniform1f(view->ctx->db_cutoff_id, view->dynamic_range.u.f32); + glUniform1f(view->ctx->threshold_id, view->threshold.u.f32); + DrawTexture(view->rendered_view.texture, 0, 0, WHITE); + EndShaderMode(); + EndTextureMode(); + SetTraceLogLevel(LOG_NONE); + GenTextureMipmaps(&view->rendered_view.texture); + SetTraceLogLevel(LOG_INFO); + view->needs_update = 0; + } + } +} + +static b32 +frame_view_ready_to_present(BeamformerFrameView *view) +{ + uv2 render_size = {.w = view->rendered_view.texture.width, + .h = view->rendered_view.texture.height}; + return !uv2_equal((uv2){0}, render_size); } static Color @@ -653,30 +767,6 @@ draw_text(Font font, s8 text, v2 pos, Color colour) return result; } -static v2 -draw_text_limited(Font font, s8 text, v2 pos, Color colour, f32 space, f32 text_width) -{ - v2 result = {.y = font.baseSize}; - if (text_width < space) { - result = draw_text(font, text, pos, colour); - } else { - f32 elipsis_width = measure_text(font, s8("...")).x; - if (elipsis_width < space) { - /* TODO(rnp): there must be a better way */ - while (text.len) { - text.len--; - f32 width = measure_text(font, text).x; - if (width + elipsis_width < space) - break; - } - result.x += draw_text(font, text, pos, colour).x; - pos.x += result.x; - result.x += draw_text(font, s8("..."), pos, colour).x; - } - } - return result; -} - /* NOTE(rnp): expensive but of the available options in raylib this gives the best results */ static v2 draw_outlined_text(Font font, s8 text, v2 pos, f32 outline_width, Color colour, Color outline) @@ -705,6 +795,38 @@ draw_text_r(Font font, s8 text, v2 pos, f32 rotation, Color colour) return result; } +static v2 +draw_text_limited(Font font, s8 text, v2 pos, Color colour, f32 space, f32 text_width, b32 outlined) +{ + v2 result = {.y = font.baseSize}; + if (text_width < space) { + if (outlined) result = draw_outlined_text(font, text, pos, 1, colour, BLACK); + else result = draw_text(font, text, pos, colour); + } else { + f32 elipsis_width = measure_text(font, s8("...")).x; + if (elipsis_width < space) { + /* TODO(rnp): there must be a better way */ + while (text.len) { + text.len--; + f32 width = measure_text(font, text).x; + if (width + elipsis_width < space) + break; + + } + if (outlined) { + result.x += draw_outlined_text(font, text, pos, 1, colour, BLACK).x; + pos.x += result.x; + result.x += draw_outlined_text(font, s8("..."), pos, 1, colour, BLACK).x; + } else { + result.x += draw_text(font, text, pos, colour).x; + pos.x += result.x; + result.x += draw_text(font, s8("..."), pos, colour).x; + } + } + } + return result; +} + static Rect extend_rect_centered(Rect r, v2 delta) { @@ -738,17 +860,6 @@ scale_rect_centered(Rect r, v2 scale) return r; } -static v2 -center_text_in_rect(Rect r, s8 text, Font font) -{ - v2 ts = measure_text(font, text); - v2 delta = { .w = r.size.w - ts.w, .h = r.size.h - ts.h }; - return (v2) { - .x = r.pos.x + 0.5 * delta.w, - .y = r.pos.y + 0.5 * delta.h, - }; -} - static b32 hover_rect(v2 mouse, Rect rect, f32 *hover_t) { @@ -812,7 +923,7 @@ draw_title_bar(BeamformerUI *ui, Arena arena, Variable *ui_view, Rect r, v2 mous v2 title_pos = title_rect.pos; title_pos.y += 0.5 * TITLE_BAR_PAD; draw_text_limited(ui->small_font, title, title_pos, NORMALIZED_FG_COLOUR, - title_rect.size.w, measure_text(ui->small_font, title).w); + title_rect.size.w, measure_text(ui->small_font, title).w, 0); return result; } @@ -917,13 +1028,42 @@ do_scale_bar(BeamformerUI *ui, Stream *buf, ScaleBar *sb, ScaleBarDirection dire colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, sb->hover_t))); } +static v2 +draw_beamformer_variable(BeamformerUI *ui, Arena arena, Variable *var, v2 at, v2 mouse, + f32 *hover_t, f32 space, b32 outlined, v4 base_colour, Font *font) +{ + Stream buf = arena_stream(&arena); + stream_append_variable(&buf, var); + + Color colour = colour_from_normalized(base_colour); + v2 text_size = measure_text(*font, stream_to_s8(&buf)); + if (var->flags & V_INPUT) { + Rect text_rect = {.pos = at, .size = text_size}; + text_rect = extend_rect_centered(text_rect, (v2){.x = 8}); + + if (hover_rect(mouse, text_rect, hover_t)) { + if (var->flags & V_TEXT) { + InputState *is = &ui->text_input_state; + is->hot_rect = text_rect; + is->hot_font = font; + } + ui->interaction.hot = var; + ui->interaction.hot_type = IT_NONE; + } + + colour = colour_from_normalized(lerp_v4(base_colour, HOVERED_COLOUR, *hover_t)); + } + return draw_text_limited(*font, stream_to_s8(&buf), at, colour, space, text_size.x, outlined); +} + static void -draw_beamformer_frame_view(BeamformerUI *ui, Arena a, BeamformerFrameView *view, Rect display_rect, v2 mouse) +draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect display_rect, v2 mouse) { + ASSERT(var->type == VT_BEAMFORMER_FRAME_VIEW); BeamformerUIParameters *bp = &ui->params; InteractionState *is = &ui->interaction; - BeamformFrame *frame = beamformer_frame_view_frame(view); - Stream buf = arena_stream(&a); + BeamformerFrameView *view = var->u.generic; + BeamformFrame *frame = view->frame; v2 txt_s = measure_text(ui->small_font, s8("-288.8 mm")); @@ -953,7 +1093,7 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, BeamformerFrameView *view, vr.pos.x += (display_rect.size.w - (vr.size.w + pad)) / 2; vr.pos.y += (display_rect.size.h - (vr.size.h + pad)) / 2; - Texture *output = &view->ctx->output.texture; + Texture *output = &view->rendered_view.texture; v2 pixels_per_meter = { .w = (f32)output->width / output_dim.w, .h = (f32)output->height / output_dim.h, @@ -974,6 +1114,8 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, BeamformerFrameView *view, start_pos.y += vr.size.y; if (vr.size.w > 0) { + Arena tmp = a; + Stream buf = arena_stream(&tmp); do_scale_bar(ui, &buf, &view->lateral_scale_bar, SB_LATERAL, mouse, (Rect){.pos = start_pos, .size = vr.size}, bp->output_min_coordinate.x * 1e3, @@ -984,6 +1126,8 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, BeamformerFrameView *view, start_pos.x += vr.size.x; if (vr.size.h > 0) { + Arena tmp = a; + Stream buf = arena_stream(&tmp); do_scale_bar(ui, &buf, &view->axial_scale_bar, SB_AXIAL, mouse, (Rect){.pos = start_pos, .size = vr.size}, bp->output_max_coordinate.z * 1e3, @@ -994,27 +1138,19 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, BeamformerFrameView *view, pixels_to_mm.x /= vr.size.x * 1e-3; pixels_to_mm.y /= vr.size.y * 1e-3; + b32 drew_coordinates = 0; + f32 remaining_width = vr.size.w; if (CheckCollisionPointRec(mouse.rl, vr.rl)) { - Variable *var = zero_struct(ui->scratch_variable); - var->type = VT_BEAMFORMER_VARIABLE; - var->flags = V_GEN_MIPMAPS; - - BeamformerVariable *bv = &var->u.beamformer_variable; - bv->store = &view->ctx->threshold; - bv->store_type = VT_F32; - bv->params.limits = (v2){.y = 240}; - bv->params.display_scale = 1; - bv->params.scroll_scale = 1; - - is->hot_type = IT_DISPLAY; - is->hot = var; + is->hot_type = IT_DISPLAY; + is->hot = var; v2 relative_mouse = sub_v2(mouse, vr.pos); v2 mm = mul_v2(relative_mouse, pixels_to_mm); mm.x += 1e3 * bp->output_min_coordinate.x; mm.y += 1e3 * bp->output_min_coordinate.z; - stream_reset(&buf, 0); + Arena tmp = a; + Stream buf = arena_stream(&tmp); stream_append_v2(&buf, mm); v2 txt_s = measure_text(ui->small_font, stream_to_s8(&buf)); if (vr.size.w + vr.pos.x - txt_s.w - 4 > display_rect.pos.x && txt_s.h < display_rect.size.h - 4) { @@ -1022,13 +1158,16 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, BeamformerFrameView *view, .x = vr.pos.x + vr.size.w - txt_s.w - 4, .y = vr.pos.y + vr.size.h - txt_s.h - 4, }; - draw_outlined_text(ui->small_font, stream_to_s8(&buf), txt_p, 1, - colour_from_normalized(RULER_COLOUR), BLACK); + f32 width = draw_outlined_text(ui->small_font, stream_to_s8(&buf), txt_p, 1, + colour_from_normalized(RULER_COLOUR), BLACK).w; + remaining_width -= width; + drew_coordinates = 1; } } { - stream_reset(&buf, 0); + Arena tmp = a; + Stream buf = arena_stream(&tmp); s8 shader = push_das_shader_id(&buf, frame->das_shader_id, frame->compound_count); v2 txt_s = measure_text(ui->font, shader); /* TODO(rnp): we want this 16 to be proportional to vr size */ @@ -1058,7 +1197,8 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, BeamformerFrameView *view, DrawLineEx(end_p.rl, ui->ruler_start_p.rl, 2, colour); DrawCircleV(end_p.rl, 3, colour); - stream_reset(&buf, 0); + Arena tmp = a; + Stream buf = arena_stream(&tmp); stream_append_f64(&buf, magnitude_v2(mm_delta), 100); stream_append_s8(&buf, s8(" mm")); @@ -1068,6 +1208,44 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, BeamformerFrameView *view, if (pixel_delta.x < 0) txt_p.x -= txt_s.x; draw_outlined_text(ui->small_font, stream_to_s8(&buf), txt_p, 1, colour, BLACK); } + + if (remaining_width > view->dynamic_range.name_width || !drew_coordinates) { + Color colour = colour_from_normalized(RULER_COLOUR); + f32 max_prefix_width = MAX(view->threshold.name_width, view->dynamic_range.name_width); + + v2 end = add_v2(vr.pos, vr.size); + f32 start_y = MAX(end.y - 4 - 2 * ui->small_font.baseSize, vr.pos.y); + end.y -= ui->small_font.baseSize; + v2 at = {.x = vr.pos.x + 4, .y = start_y}; + + at.y += draw_text_limited(ui->small_font, view->dynamic_range.name, at, colour, + end.x - at.x, view->dynamic_range.name_width, 1).y; + if (at.y < end.y) { + draw_text_limited(ui->small_font, view->threshold.name, at, colour, + end.x - at.x, view->threshold.name_width, 1); + } + + at.y = start_y; + at.x += max_prefix_width + 8; + + v2 size = draw_beamformer_variable(ui, a, &view->dynamic_range, at, mouse, + &view->dynamic_range.hover_t, end.x - at.x, 1, + RULER_COLOUR, &ui->small_font); + + f32 max_center_width = size.w; + at.y += size.h; + + if (at.y < end.y) { + size = draw_beamformer_variable(ui, a, &view->threshold, at, mouse, + &view->threshold.hover_t, end.x - at.x, 1, + RULER_COLOUR, &ui->small_font); + max_center_width = MAX(size.w, max_center_width); + } + at.y = start_y; + at.x += max_center_width + 8; + f32 width = measure_text(ui->small_font, s8(" [dB]")).w; + draw_text_limited(ui->small_font, s8(" [dB]"), at, colour, end.x - at.x, width, 1); + } } static v2 @@ -1123,7 +1301,7 @@ draw_compute_stats_view(BeamformerCtx *ctx, Arena arena, ComputeShaderStats *sta for (u32 i = 0; i < stages; i++) { u32 index = ctx->params->compute_stages[i]; draw_text_limited(ui->font, labels[index], at, NORMALIZED_FG_COLOUR, r.size.w, - label_widths[index]); + label_widths[index], 0); stream_reset(&buf, 0); stream_append_f64_e(&buf, stats->times[index]); @@ -1131,7 +1309,7 @@ draw_compute_stats_view(BeamformerCtx *ctx, Arena arena, ComputeShaderStats *sta v2 txt_fs = measure_text(ui->font, stream_to_s8(&buf)); v2 rpos = {.x = r.pos.x + max_label_width + LISTING_ITEM_PAD, .y = at.y}; at.y += draw_text_limited(ui->font, stream_to_s8(&buf), rpos, NORMALIZED_FG_COLOUR, - r.size.w - LISTING_ITEM_PAD - max_label_width, txt_fs.w).h; + r.size.w - LISTING_ITEM_PAD - max_label_width, txt_fs.w, 0).h; compute_time_sum += stats->times[index]; } @@ -1141,42 +1319,16 @@ draw_compute_stats_view(BeamformerCtx *ctx, Arena arena, ComputeShaderStats *sta stream_append_s8(&buf, s8(" [s]")); v2 txt_fs = measure_text(ui->font, stream_to_s8(&buf)); v2 rpos = {.x = at.x + max_label_width + LISTING_ITEM_PAD, .y = at.y}; - draw_text_limited(ui->font, compute_total, at, NORMALIZED_FG_COLOUR, r.size.w, compute_total_width); + draw_text_limited(ui->font, compute_total, at, NORMALIZED_FG_COLOUR, r.size.w, + compute_total_width, 0); draw_text_limited(ui->font, stream_to_s8(&buf), rpos, NORMALIZED_FG_COLOUR, - r.size.w - LISTING_ITEM_PAD - max_label_width, txt_fs.w); + r.size.w - LISTING_ITEM_PAD - max_label_width, txt_fs.w, 0); at.y += ui->font.baseSize; v2 result = {.x = r.size.w, .y = at.y - r.pos.y}; return result; } -static v2 -draw_beamformer_variable(BeamformerUI *ui, Arena arena, Variable *var, v2 at, v2 mouse, - f32 *hover_t, f32 space) -{ - Stream buf = arena_stream(&arena); - stream_append_variable(&buf, var); - - Color colour = NORMALIZED_FG_COLOUR; - v2 text_size = measure_text(ui->font, stream_to_s8(&buf)); - if (var->flags & V_INPUT) { - Rect text_rect = {.pos = at, .size = text_size}; - text_rect = extend_rect_centered(text_rect, (v2){.x = 8}); - - if (hover_rect(mouse, text_rect, hover_t)) { - if (var->flags & V_TEXT) { - InputState *is = &ui->text_input_state; - is->hot_rect = text_rect; - is->hot_font = &ui->font; - } - ui->interaction.hot = var; - } - - colour = colour_from_normalized(lerp_v4(FG_COLOUR, HOVERED_COLOUR, *hover_t)); - } - return draw_text_limited(ui->font, stream_to_s8(&buf), at, colour, space, text_size.x); -} - static void draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse) { @@ -1208,10 +1360,11 @@ draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse) suffix = bv->suffix; advance = draw_text_limited(ui->font, var->name, at, NORMALIZED_FG_COLOUR, - r.size.w + r.pos.x - at.x, var->name_width).y; + r.size.w + r.pos.x - at.x, var->name_width, 0).y; at.x += x_off + LISTING_ITEM_PAD; at.x += draw_beamformer_variable(ui, ui->arena, var, at, mouse, - &var->hover_t, r.size.w + r.pos.x - at.x).x; + &var->hover_t, r.size.w + r.pos.x - at.x, + 0, FG_COLOUR, &ui->font).x; while (var) { if (var->next) { @@ -1228,7 +1381,8 @@ draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse) VariableGroup *g = &var->u.group; advance = draw_beamformer_variable(ui, ui->arena, var, at, mouse, &var->hover_t, - r.size.w + r.pos.x - at.x).y; + r.size.w + r.pos.x - at.x, 0, FG_COLOUR, + &ui->font).y; at.x += x_off + LISTING_ITEM_PAD; if (g->expanded) { cut_rect_horizontal(r, LISTING_INDENT, 0, &r); @@ -1247,21 +1401,22 @@ draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse) case VG_V4: { at.x += draw_text_limited(ui->font, s8("{"), at, NORMALIZED_FG_COLOUR, r.size.w + r.pos.x - at.x, - measure_text(ui->font, s8("{")).x).x; + measure_text(ui->font, s8("{")).x, 0).x; while (v) { at.x += draw_beamformer_variable(ui, ui->arena, v, at, mouse, &v->hover_t, - r.size.w + r.pos.x - at.x).x; + r.size.w + r.pos.x - at.x, 0, FG_COLOUR, + &ui->font).x; v = v->next; if (v) at.x += draw_text_limited(ui->font, s8(", "), at, NORMALIZED_FG_COLOUR, r.size.w + r.pos.x - at.x, - measure_text(ui->font, s8(", ")).x).x; + measure_text(ui->font, s8(", ")).x, 0).x; } at.x += draw_text_limited(ui->font, s8("}"), at, NORMALIZED_FG_COLOUR, r.size.w + r.pos.x - at.x, - measure_text(ui->font, s8("}")).x).x; + measure_text(ui->font, s8("}")).x, 0).x; } break; } @@ -1270,8 +1425,8 @@ draw_ui_view(BeamformerUI *ui, Variable *ui_view, Rect r, v2 mouse) } break; case VT_BEAMFORMER_FRAME_VIEW: { BeamformerFrameView *bv = var->u.generic; - if (beamformer_frame_view_frame(bv)->ready_to_present) - draw_beamformer_frame_view(ui, ui->arena, bv, r, mouse); + if (frame_view_ready_to_present(bv)) + draw_beamformer_frame_view(ui, ui->arena, var, r, mouse); var = var->next; } break; case VT_COMPUTE_PROGRESS_BAR: { @@ -1312,11 +1467,11 @@ draw_active_text_box(BeamformerUI *ui, Variable *var) Rect box = is->rect; s8 text = {.len = is->buf_len, .data = is->buf}; - v2 text_size = measure_text(ui->font, text); + v2 text_size = measure_text(*is->font, text); v2 text_position = {.x = box.pos.x, .y = box.pos.y + (box.size.h - text_size.h) / 2}; f32 cursor_width = (is->cursor == is->buf_len) ? 16 : 4; - f32 cursor_offset = measure_text(ui->font, (s8){.data = text.data, .len = is->cursor}).w; + f32 cursor_offset = measure_text(*is->font, (s8){.data = text.data, .len = is->cursor}).w; cursor_offset += text_position.x; box.size.w = MAX(box.size.w, text_size.w + cursor_width); @@ -1599,13 +1754,20 @@ display_interaction_end(BeamformerUI *ui) } static void -display_interaction(BeamformerUI *ui, v2 mouse) +display_interaction(BeamformerUI *ui, v2 mouse, f32 scroll) { b32 mouse_left_pressed = IsMouseButtonPressed(MOUSE_BUTTON_LEFT); b32 mouse_right_pressed = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT); b32 is_hot = ui->interaction.hot_type == IT_DISPLAY; b32 is_active = ui->interaction.type == IT_DISPLAY; + if (scroll) { + ASSERT(ui->interaction.active->type == VT_BEAMFORMER_FRAME_VIEW); + BeamformerFrameView *bv = ui->interaction.active->u.generic; + bv->threshold.u.f32 += scroll; + bv->needs_update = 1; + } + if (mouse_left_pressed && is_active) { ui->ruler_state++; switch (ui->ruler_state) { @@ -1761,7 +1923,7 @@ ui_end_interact(BeamformerCtx *ctx, v2 mouse) default: INVALID_CODE_PATH; } } break; - case IT_DISPLAY: display_interaction_end(ui); /* FALLTHROUGH */ + case IT_DISPLAY: display_interaction_end(ui); break; case IT_SCROLL: scroll_interaction(is->active, GetMouseWheelMoveV().y); break; case IT_TEXT: end_text_input(&ui->text_input_state, is->active); break; case IT_SCALE_BAR: break; @@ -1771,8 +1933,11 @@ ui_end_interact(BeamformerCtx *ctx, v2 mouse) if (is->active->flags & V_CAUSES_COMPUTE) ui->flush_params = 1; - if (is->active->flags & V_GEN_MIPMAPS && ctx->fsctx.output.texture.id) - ctx->fsctx.gen_mipmaps = 1; + if (is->active->flags & V_UPDATE_VIEW) { + Variable *frame_view = is->active->parent; + ASSERT(frame_view && frame_view->type == VT_BEAMFORMER_FRAME_VIEW); + ((BeamformerFrameView *)frame_view->u.generic)->needs_update = 1; + } is->type = IT_NONE; is->active = 0; @@ -1798,7 +1963,7 @@ ui_interact(BeamformerCtx *ctx, BeamformerInput *input) switch (is->type) { case IT_NONE: break; case IT_NOP: break; - case IT_DISPLAY: display_interaction(ui, input->mouse); break; + case IT_DISPLAY: display_interaction(ui, input->mouse, GetMouseWheelMoveV().y); break; case IT_SCROLL: ui_end_interact(ctx, input->mouse); break; case IT_SET: ui_end_interact(ctx, input->mouse); break; case IT_TEXT: update_text_input(&ui->text_input_state, is->active); break; @@ -1855,10 +2020,14 @@ ui_init(BeamformerCtx *ctx, Arena store) BeamformerUI *ui = ctx->ui; - /* NOTE: unload old fonts from the GPU */ + /* NOTE(rnp): unload old data from GPU */ if (ui) { UnloadFont(ui->font); UnloadFont(ui->small_font); + + for (BeamformerFrameView *view = ui->views; view; view = view->next) + if (view->rendered_view.id) + UnloadRenderTexture(view->rendered_view); } ui = ctx->ui = push_struct(&store, typeof(*ui)); @@ -1869,7 +2038,6 @@ ui_init(BeamformerCtx *ctx, Arena store) ui->font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 28, 0, 0); ui->small_font = LoadFontEx("assets/IBMPlexSans-Bold.ttf", 20, 0, 0); - ui->scratch_variable = ui->scratch_variables + 0; Variable *split = ui->regions = add_ui_split(ui, 0, &ui->arena, s8("UI Root"), 0.4, RSD_HORIZONTAL, ui->font); @@ -1878,7 +2046,6 @@ ui_init(BeamformerCtx *ctx, Arena store) split->u.region_split.right = add_beamformer_frame_view(ui, split, &ui->arena, FVT_LATEST); BeamformerFrameView *bv = split->u.region_split.right->u.group.first->u.generic; - bv->store = &ui->latest_frame; bv->lateral_scale_bar.min_value = &ui->params.output_min_coordinate.x; bv->lateral_scale_bar.max_value = &ui->params.output_max_coordinate.x; bv->axial_scale_bar.min_value = &ui->params.output_min_coordinate.z; @@ -1927,7 +2094,8 @@ draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformFrame *frame_to_draw ComputeShaderStats *latest_compute_stats) { BeamformerUI *ui = ctx->ui; - ui->latest_frame = frame_to_draw; + + if (frame_to_draw->ready_to_present) ui->latest_frame = frame_to_draw; ui->latest_compute_stats = latest_compute_stats; /* TODO(rnp): there should be a better way of detecting this */ @@ -1951,6 +2119,9 @@ draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformFrame *frame_to_draw } } + /* NOTE(rnp): can't render to a different framebuffer in the middle of BeginDrawing()... */ + update_frame_views(ui); + BeginDrawing(); ClearBackground(colour_from_normalized(BG_COLOUR));