ogl_beamforming

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

Commit: 84f4efe28dfdf8a68c988920499b44f32a9ee21b
Parent: 0b21338b3edef66802aaf3829de640ec08bd55a0
Author: Randy Palamar
Date:   Wed, 18 Feb 2026 14:50:18 -0700

core: make DAS voxel transform a user provided parameter

This allows the user to beamform arbitrary planes and lines which
was not really possible before. For the purposes of UI the min and
max coordinate can be determined from the transform.

see #40; this completes it from the API perspective but the UI
still needs to be fixed. Currently all the retained mode code is
preventing completion.

Diffstat:
Mbeamformer.c | 2+-
Mbeamformer.meta | 5+----
Mbeamformer_core.c | 91+++++++++++++++++++++++--------------------------------------------------------
Mbeamformer_internal.h | 7+++----
Mbeamformer_shared_memory.c | 2+-
Mgenerated/beamformer.meta.c | 15+++------------
Mmath.c | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mshaders/das.glsl | 26+++++++++++++++-----------
Mshaders/render_3d.frag.glsl | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mtests/throughput.c | 19+++++++------------
Mui.c | 369+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mutil.c | 12+++++++++++-
Mutil.h | 7+++++--
13 files changed, 519 insertions(+), 274 deletions(-)

diff --git a/beamformer.c b/beamformer.c @@ -453,7 +453,7 @@ beamformer_init(BeamformerInput *input) "\tf_orig_texture_coordinate = (2 * v_position + 1) / 2;\n" //"\tif (v_position.y == -1) pos.x = clamp(v_position.x, -u_clip_fraction, u_clip_fraction);\n" "\tvec3 tex_coord = (2 * pos + 1) / 2;\n" - "\tf_texture_coordinate = tex_coord.xzy;\n" + "\tf_texture_coordinate = tex_coord;\n" //"\tf_texture_coordinate = u_swizzle? tex_coord.xzy : tex_coord;\n" //"\tf_normal = normalize(mat3(u_model) * v_normal);\n" "\tf_normal = v_normal;\n" diff --git a/beamformer.meta b/beamformer.meta @@ -88,6 +88,7 @@ @Table([name type]) ParametersHead { + [das_voxel_transform M4] [xdc_transform M4] [xdc_element_pitch V2] [raw_data_dimensions UV2] @@ -106,17 +107,13 @@ @Table([name type]) ParametersUI { - [output_min_coordinate V3] - [output_max_coordinate V3] [output_points SV4] [sampling_frequency F32] [demodulation_frequency F32] [speed_of_sound F32] [f_number F32] - [off_axis_pos F32] [interpolation_mode U32] [coherency_weighting U32] - [beamform_plane U32] [decimation_rate U32] } diff --git a/beamformer_core.c b/beamformer_core.c @@ -207,24 +207,14 @@ beamformer_frame_compatible(BeamformerFrame *f, iv3 dim, GLenum gl_kind) return result; } -function iv3 -make_valid_output_points(i32 points[3]) -{ - iv3 result; - result.E[0] = CLAMP(points[0], 1, gl_parameters.max_3d_texture_dim); - result.E[1] = CLAMP(points[1], 1, gl_parameters.max_3d_texture_dim); - result.E[2] = CLAMP(points[2], 1, gl_parameters.max_3d_texture_dim); - return result; -} - function void alloc_beamform_frame(BeamformerFrame *out, iv3 out_dim, GLenum gl_kind, s8 name, Arena arena) { - out->dim = make_valid_output_points(out_dim.E); + out->dim = das_output_dimension(out_dim); /* NOTE: allocate storage for beamformed output data; * this is shared between compute and fragment shaders */ - u32 max_dim = (u32)MAX(out->dim.x, MAX(out->dim.y, out->dim.z)); + u32 max_dim = (u32)Max(out->dim.x, Max(out->dim.y, out->dim.z)); out->mips = (i32)ctz_u32(round_up_power_of_2(max_dim)) + 1; out->gl_kind = gl_kind; @@ -407,42 +397,6 @@ compute_cursor_finished(struct compute_cursor *cursor) return result; } -function m4 -das_voxel_transform_matrix(BeamformerParameters *bp) -{ - v3 extent = v3_abs(v3_sub(bp->output_max_coordinate, bp->output_min_coordinate)); - v3 points = v3_from_iv3(make_valid_output_points(bp->output_points.E)); - - m4 T1 = m4_translation(v3_scale(v3_sub(points, (v3){{1.0f, 1.0f, 1.0f}}), -0.5f)); - m4 T2 = m4_translation(v3_add(bp->output_min_coordinate, v3_scale(extent, 0.5f))); - m4 S = m4_scale(v3_div(extent, points)); - - m4 R; - switch (bp->acquisition_kind) { - case BeamformerAcquisitionKind_FORCES: - case BeamformerAcquisitionKind_UFORCES: - case BeamformerAcquisitionKind_Flash: - { - R = m4_identity(); - S.c[1].E[1] = 0; - T2.c[3].E[1] = 0; - }break; - case BeamformerAcquisitionKind_HERO_PA: - case BeamformerAcquisitionKind_HERCULES: - case BeamformerAcquisitionKind_UHERCULES: - case BeamformerAcquisitionKind_RCA_TPW: - case BeamformerAcquisitionKind_RCA_VLS: - { - R = m4_rotation_about_z(bp->beamform_plane ? 0.0f : 0.25f); - if (!(points.x > 1 && points.y > 1 && points.z > 1)) - T2.c[3].E[1] = bp->off_axis_pos; - }break; - default:{ R = m4_identity(); }break; - } - m4 result = m4_mul(R, m4_mul(T2, m4_mul(S, T1))); - return result; -} - function void plan_compute_pipeline(BeamformerComputePlan *cp, BeamformerParameterBlock *pb) { @@ -651,7 +605,6 @@ plan_compute_pipeline(BeamformerComputePlan *cp, BeamformerParameterBlock *pb) BeamformerShaderDASBakeParameters *db = &sd->bake.DAS; BeamformerDASUBO *du = &cp->das_ubo_data; - du->voxel_transform = das_voxel_transform_matrix(&pb->parameters); du->xdc_element_pitch = pb->parameters.xdc_element_pitch; db->sampling_frequency = sampling_frequency; db->demodulation_frequency = pb->parameters.demodulation_frequency; @@ -668,7 +621,8 @@ plan_compute_pipeline(BeamformerComputePlan *cp, BeamformerParameterBlock *pb) db->transmit_receive_orientation = pb->parameters.transmit_receive_orientation; // NOTE(rnp): old gcc will miscompile an assignment - mem_copy(du->xdc_transform.E, pb->parameters.xdc_transform.E, sizeof(du->xdc_transform)); + mem_copy(du->voxel_transform.E, pb->parameters.das_voxel_transform.E, sizeof(du->voxel_transform)); + mem_copy(du->xdc_transform.E, pb->parameters.xdc_transform.E, sizeof(du->xdc_transform)); if (pb->parameters.single_focus) sd->bake.flags |= BeamformerShaderDASFlags_SingleFocus; if (pb->parameters.single_orientation) sd->bake.flags |= BeamformerShaderDASFlags_SingleOrientation; @@ -679,9 +633,21 @@ plan_compute_pipeline(BeamformerComputePlan *cp, BeamformerParameterBlock *pb) if (id == BeamformerAcquisitionKind_UFORCES || id == BeamformerAcquisitionKind_UHERCULES) sd->bake.flags |= BeamformerShaderDASFlags_Sparse; - sd->layout.x = DAS_LOCAL_SIZE_X; - sd->layout.y = DAS_LOCAL_SIZE_Y; - sd->layout.z = DAS_LOCAL_SIZE_Z; + // TODO(rnp): subgroup size + u32 subgroup_size = gl_parameters.vendor_id == GLVendor_NVIDIA ? 32 : 64; + switch (iv3_dimension(cp->output_points)) { + case 1:{sd->layout = (uv3){{subgroup_size, 1, 1}}; }break; + case 2:{sd->layout = (uv3){{subgroup_size/4, subgroup_size/4, 1}};}break; + case 3:{sd->layout = (uv3){{8, 8, 8}}; }break; + InvalidDefaultCase; + } + + if ((sd->bake.flags & BeamformerShaderDASFlags_Fast) == 0) + sd->layout = (uv3){{DAS_LOCAL_SIZE_X, DAS_LOCAL_SIZE_Y, DAS_LOCAL_SIZE_Z}}; + + sd->dispatch.x = (u32)ceil_f32((f32)cp->output_points.x / sd->layout.x); + sd->dispatch.y = (u32)ceil_f32((f32)cp->output_points.y / sd->layout.y); + sd->dispatch.z = (u32)ceil_f32((f32)cp->output_points.z / sd->layout.z); commit = 1; }break; @@ -826,6 +792,9 @@ beamformer_commit_parameter_block(BeamformerCtx *ctx, BeamformerComputePlan *cp, case BeamformerParameterBlockRegion_ComputePipeline: case BeamformerParameterBlockRegion_Parameters: { + cp->output_points = das_output_dimension(pb->parameters.output_points.xyz); + cp->average_frames = pb->parameters.output_points.E[3]; + plan_compute_pipeline(cp, pb); /* NOTE(rnp): these are both handled by plan_compute_pipeline() */ @@ -855,11 +824,7 @@ beamformer_commit_parameter_block(BeamformerCtx *ctx, BeamformerComputePlan *cp, if (cp->hadamard_order != (i32)cp->acquisition_count) update_hadamard_texture(cp, (i32)cp->acquisition_count, arena); - cp->min_coordinate = pb->parameters.output_min_coordinate; - cp->max_coordinate = pb->parameters.output_max_coordinate; - - cp->output_points = make_valid_output_points(pb->parameters.output_points.E); - cp->average_frames = pb->parameters.output_points.E[3]; + cp->voxel_transform = pb->parameters.das_voxel_transform; GLenum gl_kind = cp->iq_pipeline ? GL_RG32F : GL_R32F; if (cp->average_frames > 1 && !beamformer_frame_compatible(ctx->averaged_frames + 0, cp->output_points, gl_kind)) { @@ -1014,9 +979,7 @@ do_compute_shader(BeamformerCtx *ctx, BeamformerComputePlan *cp, BeamformerFrame /* IMPORTANT(rnp): prevents OS from coalescing and killing our shader */ glFinish(); glProgramUniform1i(program, DAS_FAST_CHANNEL_UNIFORM_LOC, index); - glDispatchCompute((u32)ceil_f32((f32)frame->dim.x / DAS_LOCAL_SIZE_X), - (u32)ceil_f32((f32)frame->dim.y / DAS_LOCAL_SIZE_Y), - (u32)ceil_f32((f32)frame->dim.z / DAS_LOCAL_SIZE_Z)); + glDispatchCompute(dispatch.x, dispatch.y, dispatch.z); glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); } } else { @@ -1070,8 +1033,7 @@ do_compute_shader(BeamformerCtx *ctx, BeamformerComputePlan *cp, BeamformerFrame glProgramUniform1f(program, SUM_PRESCALE_UNIFORM_LOC, 1 / (f32)frame_count); do_sum_shader(cc, in_textures, frame_count, aframe->texture, aframe->dim); - aframe->min_coordinate = frame->min_coordinate; - aframe->max_coordinate = frame->max_coordinate; + aframe->voxel_transform = frame->voxel_transform; aframe->compound_count = frame->compound_count; aframe->acquisition_kind = frame->acquisition_kind; }break; @@ -1226,8 +1188,7 @@ complete_queue(BeamformerCtx *ctx, BeamformWorkQueue *q, Arena *arena, iptr gl_c if (!beamformer_frame_compatible(frame, cp->output_points, gl_kind)) alloc_beamform_frame(frame, cp->output_points, gl_kind, s8("Beamformed_Data"), *arena); - frame->min_coordinate = cp->min_coordinate; - frame->max_coordinate = cp->max_coordinate; + frame->voxel_transform = cp->voxel_transform; frame->acquisition_kind = cp->acquisition_kind; frame->compound_count = cp->acquisition_count; diff --git a/beamformer_internal.h b/beamformer_internal.h @@ -161,8 +161,8 @@ struct BeamformerComputePlan { i32 hadamard_order; b32 iq_pipeline; - v3 min_coordinate; - v3 max_coordinate; + m4 voxel_transform; + iv3 output_points; i32 average_frames; @@ -269,8 +269,7 @@ struct BeamformerFrame { /* NOTE: for use when displaying either prebeamformed frames or on the current frame * when we intend to recompute on the next frame */ - v3 min_coordinate; - v3 max_coordinate; + m4 voxel_transform; // metadata GLenum gl_kind; diff --git a/beamformer_shared_memory.c b/beamformer_shared_memory.c @@ -1,5 +1,5 @@ /* See LICENSE for license details. */ -#define BEAMFORMER_SHARED_MEMORY_VERSION (23UL) +#define BEAMFORMER_SHARED_MEMORY_VERSION (24UL) typedef struct BeamformerFrame BeamformerFrame; diff --git a/generated/beamformer.meta.c b/generated/beamformer.meta.c @@ -174,6 +174,7 @@ typedef union { } BeamformerEmissionParameters; typedef struct { + m4 das_voxel_transform; m4 xdc_transform; v2 xdc_element_pitch; uv2 raw_data_dimensions; @@ -188,23 +189,20 @@ typedef struct { u8 single_orientation; u8 decode_mode; u8 sampling_mode; - v3 output_min_coordinate; - v3 output_max_coordinate; iv4 output_points; f32 sampling_frequency; f32 demodulation_frequency; f32 speed_of_sound; f32 f_number; - f32 off_axis_pos; u32 interpolation_mode; u32 coherency_weighting; - u32 beamform_plane; u32 decimation_rate; BeamformerEmissionKind emission_kind; BeamformerEmissionParameters emission_parameters; } BeamformerParameters; typedef struct { + m4 das_voxel_transform; m4 xdc_transform; v2 xdc_element_pitch; uv2 raw_data_dimensions; @@ -222,21 +220,18 @@ typedef struct { } BeamformerParametersHead; typedef struct { - v3 output_min_coordinate; - v3 output_max_coordinate; iv4 output_points; f32 sampling_frequency; f32 demodulation_frequency; f32 speed_of_sound; f32 f_number; - f32 off_axis_pos; u32 interpolation_mode; u32 coherency_weighting; - u32 beamform_plane; u32 decimation_rate; } BeamformerUIParameters; typedef struct { + m4 das_voxel_transform; m4 xdc_transform; v2 xdc_element_pitch; uv2 raw_data_dimensions; @@ -251,17 +246,13 @@ typedef struct { u8 single_orientation; u8 decode_mode; u8 sampling_mode; - v3 output_min_coordinate; - v3 output_max_coordinate; iv4 output_points; f32 sampling_frequency; f32 demodulation_frequency; f32 speed_of_sound; f32 f_number; - f32 off_axis_pos; u32 interpolation_mode; u32 coherency_weighting; - u32 beamform_plane; u32 decimation_rate; BeamformerEmissionKind emission_kind; BeamformerEmissionParameters emission_parameters; diff --git a/math.c b/math.c @@ -157,6 +157,13 @@ iv3_equal(iv3 a, iv3 b) return result; } +function i32 +iv3_dimension(iv3 points) +{ + i32 result = (points.x > 1) + (points.y > 1) + (points.z > 1); + return result; +} + function v2 clamp_v2_rect(v2 v, Rect r) { @@ -167,6 +174,24 @@ clamp_v2_rect(v2 v, Rect r) } function v2 +v2_from_iv2(iv2 v) +{ + v2 result; + result.E[0] = (f32)v.E[0]; + result.E[1] = (f32)v.E[1]; + return result; +} + +function v2 +v2_abs(v2 a) +{ + v2 result; + result.x = Abs(a.x); + result.y = Abs(a.y); + return result; +} + +function v2 v2_scale(v2 a, f32 scale) { v2 result; @@ -256,9 +281,9 @@ function v3 v3_abs(v3 a) { v3 result; - result.x = ABS(a.x); - result.y = ABS(a.y); - result.z = ABS(a.z); + result.x = Abs(a.x); + result.y = Abs(a.y); + result.z = Abs(a.z); return result; } @@ -459,28 +484,36 @@ m4_scale(v3 scale) } function m4 -m4_rotation_about_z(f32 turns) +m4_rotation_about_axis(v3 axis, f32 turns) { - f32 sa = sin_f32(turns * 2 * PI); - f32 ca = cos_f32(turns * 2 * PI); + assert(f32_equal(v3_magnitude_squared(axis), 1.0f)); + f32 sa = sin_f32(turns * 2 * PI); + f32 ca = cos_f32(turns * 2 * PI); + f32 mca = 1.0f - ca; + + f32 x = axis.x, x2 = x * x; + f32 y = axis.y, y2 = y * y; + f32 z = axis.z, z2 = z * z; + m4 result; - result.c[0] = (v4){{ca, -sa, 0, 0}}; - result.c[1] = (v4){{sa, ca, 0, 0}}; - result.c[2] = (v4){{0, 0, 1, 0}}; - result.c[3] = (v4){{0, 0, 0, 1}}; + result.c[0] = (v4){{ca + mca * x2, mca * x * y - sa * z, mca * x * z + sa * y, 0}}; + result.c[1] = (v4){{mca * x * y + sa * z, ca + mca * y2, mca * y * z - sa * x, 0}}; + result.c[2] = (v4){{mca * x * z - sa * y, mca * y * z + sa * x, ca + mca * z2, 0}}; + result.c[3] = (v4){{0, 0, 0, 1}}; + return result; +} + +function m4 +m4_rotation_about_z(f32 turns) +{ + m4 result = m4_rotation_about_axis((v3){{0, 0, 1.0f}}, turns); return result; } function m4 m4_rotation_about_y(f32 turns) { - f32 sa = sin_f32(turns * 2 * PI); - f32 ca = cos_f32(turns * 2 * PI); - m4 result; - result.c[0] = (v4){{ca, 0, -sa, 0}}; - result.c[1] = (v4){{0, 1, 0, 0}}; - result.c[2] = (v4){{sa, 0, ca, 0}}; - result.c[3] = (v4){{0, 0, 0, 1}}; + m4 result = m4_rotation_about_axis((v3){{0, 1.0f, 0}}, turns); return result; } @@ -579,7 +612,7 @@ obb_raycast(m4 obb_orientation, v3 obb_size, v3 obb_center, ray r) f32 result = 0; f32 t[6] = {0}; for (i32 i = 0; i < 3; i++) { - if (f32_cmp(f.E[i], 0)) { + if (f32_equal(f.E[i], 0)) { if (-e.E[i] - obb_size.E[i] > 0 || -e.E[i] + obb_size.E[i] < 0) result = -1.0f; f.E[i] = F32_EPSILON; @@ -648,7 +681,7 @@ kaiser_low_pass_filter(Arena *arena, f32 cutoff_frequency, f32 sampling_frequenc for (i32 n = 0; n < length; n++) { f32 t = (f32)n - a; - f32 impulse = !f32_cmp(t, 0) ? sin_f32(wc * t) / t : wc; + f32 impulse = !f32_equal(t, 0) ? sin_f32(wc * t) / t : wc; t = t / a; f32 window = (f32)cephes_i0(beta * sqrt_f32(1 - t * t)) / pi_i0_b; result[n] = impulse * window; @@ -687,6 +720,98 @@ baseband_chirp(Arena *arena, f32 min_frequency, f32 max_frequency, f32 sampling_ return result; } +function iv3 +das_output_dimension(iv3 points) +{ + iv3 result; + result.x = Max(points.x, 1); + result.y = Max(points.y, 1); + result.z = Max(points.z, 1); + + switch (iv3_dimension(result)) { + case 1:{ + if (result.y > 1) result.x = result.y; + if (result.z > 1) result.x = result.z; + result.y = result.z = 1; + }break; + + case 2:{ + if (result.x > 1) { + if (result.z > 1) result.y = result.z; + } else { + result.x = result.z; + } + result.z = 1; + }break; + + case 3:{}break; + + InvalidDefaultCase; + } + + return result; +} + +function m4 +das_transform_1d(v3 p1, v3 p2) +{ + v3 extent = v3_sub(p2, p1); + m4 result = { + .c[0] = (v4){{extent.x, extent.y, extent.z, 0.0f}}, + .c[1] = (v4){{0.0f, 0.0f, 0.0f, 0.0f}}, + .c[2] = (v4){{0.0f, 0.0f, 0.0f, 0.0f}}, + .c[3] = (v4){{p1.x, p1.y, p1.z, 1.0f}}, + }; + return result; +} + +function m4 +das_transform_2d_xz(v2 min_coordinate, v2 max_coordinate) +{ + v2 extent = v2_abs(v2_sub(max_coordinate, min_coordinate)); + + // NOTE(rnp): DAS assumes 3D grid with z down -> swap y and z + m4 result; + result.c[0] = (v4){{extent.x, 0.0f, 0.0f, 0.0f}}; + result.c[1] = (v4){{0.0f, 0.0f, extent.y, 0.0f}}; + result.c[2] = (v4){{0.0f, 1.0f, 0.0f, 0.0f}}; + result.c[3] = (v4){{min_coordinate.x, 0.0f, min_coordinate.y, 1.0f}}; + return result; +} + +function m4 +das_transform_3d(v3 min_coordinate, v3 max_coordinate) +{ + v3 extent = v3_abs(v3_sub(max_coordinate, min_coordinate)); + m4 result = m4_mul(m4_translation(min_coordinate), m4_scale(extent)); + return result; +} + +function m4 +das_transform(v3 min_coordinate, v3 max_coordinate, iv3 *points) +{ + m4 result; + + *points = das_output_dimension(*points); + + switch (iv3_dimension(*points)) { + case 1:{result = das_transform_1d( min_coordinate, max_coordinate); }break; + case 2:{result = das_transform_2d_xz(XY(min_coordinate), XY(max_coordinate));}break; + case 3:{result = das_transform_3d( min_coordinate, max_coordinate); }break; + } + + return result; +} + +function v2 +plane_uv(v3 point, v3 U, v3 V) +{ + v2 result; + result.x = v3_dot(U, point) / v3_dot(U, U); + result.y = v3_dot(V, point) / v3_dot(V, V); + return result; +} + function v4 hsv_to_rgb(v4 hsv) { diff --git a/shaders/das.glsl b/shaders/das.glsl @@ -280,38 +280,42 @@ RESULT_TYPE FORCES(const vec3 world_point) void main() { ivec3 out_voxel = ivec3(gl_GlobalInvocationID); + #if !Fast + out_voxel += u_voxel_offset; + #endif + + vec3 image_points = vec3(imageSize(u_out_data_tex)) - 1.0f; if (!all(lessThan(out_voxel, imageSize(u_out_data_tex)))) return; -#if Fast - RESULT_TYPE sum = RESULT_TYPE_CAST(imageLoad(u_out_data_tex, out_voxel)); -#else - RESULT_TYPE sum = RESULT_TYPE(0); - out_voxel += u_voxel_offset; -#endif - - vec3 world_point = (voxel_transform * vec4(out_voxel, 1)).xyz; + vec3 point = vec3(out_voxel) / max(vec3(1.0f), image_points); + vec3 world_point = (voxel_transform * vec4(point, 1)).xyz; + RESULT_TYPE sum; switch (AcquisitionKind) { case AcquisitionKind_FORCES: case AcquisitionKind_UFORCES: { - sum += FORCES(world_point); + sum = FORCES(world_point); }break; case AcquisitionKind_HERCULES: case AcquisitionKind_UHERCULES: case AcquisitionKind_HERO_PA: { - sum += HERCULES(world_point); + sum = HERCULES(world_point); }break; case AcquisitionKind_Flash: case AcquisitionKind_RCA_TPW: case AcquisitionKind_RCA_VLS: { - sum += RCA(world_point); + sum = RCA(world_point); }break; } + #if Fast + sum += RESULT_TYPE_CAST(imageLoad(u_out_data_tex, out_voxel)); + #endif + #if CoherencyWeighting /* TODO(rnp): scale such that brightness remains ~constant */ float denominator = sum[RESULT_LAST_INDEX] + float(sum[RESULT_LAST_INDEX] == 0); diff --git a/shaders/render_3d.frag.glsl b/shaders/render_3d.frag.glsl @@ -20,28 +20,75 @@ float sdf_wire_box_outside(vec3 p, vec3 b, float e) return result; } -void main() +int texture_dimension(ivec3 points) { - float smp = length(texture(u_texture, texture_coordinate).xy); - float threshold_val = pow(10.0f, u_threshold / 20.0f); - smp = clamp(smp, 0.0f, threshold_val); - smp = smp / threshold_val; - smp = pow(smp, u_gamma); + points = ivec3(greaterThan(points, ivec3(1))); + return points.x + points.y + points.z; +} - //float t = test_texture_coordinate.y; - //smp = smp * smoothstep(-0.4, 1.1, t) * u_gain; + +float sample_value(vec3 p) +{ + float result = length(texture(u_texture, p).xy); + float threshold_val = pow(10.0f, u_threshold / 20.0f); + result = clamp(result, 0.0f, threshold_val); + result = result / threshold_val; + result = pow(result, u_gamma); if (u_log_scale) { - smp = 20 * log(smp) / log(10); - smp = clamp(smp, -u_db_cutoff, 0) / -u_db_cutoff; - smp = 1 - smp; + result = 20 * log(result) / log(10); + result = clamp(result, -u_db_cutoff, 0) / -u_db_cutoff; + result = 1 - result; + } + + return result; +} + +float grad(float x) +{ + float h = length(fwidth(texture_coordinate.xy)); + float s1 = sample_value(vec3(x + h, 0, 0)); + float s2 = sample_value(vec3(x - h, 0, 0)); + return (s1 - s2) / (2.0f * h); +} + +void main(void) +{ + int dimension = texture_dimension(textureSize(u_texture, 0)); + + if (dimension == 3) { + // TODO(rnp): add slice offset passed in as a uniform } + float smp = sample_value(texture_coordinate); + //float t = test_texture_coordinate.y; + //smp = smp * smoothstep(-0.4, 1.1, t) * u_gain; + vec3 p = 2.0f * test_texture_coordinate - 1.0f; - float t = clamp(sdf_wire_box_outside(p, vec3(1.0f), u_bb_fraction) / u_bb_fraction, 0, 1); - out_colour = vec4(t * vec3(smp) + (1 - t) * u_bb_colour.xyz, 1); - if (u_solid_bb) out_colour = u_bb_colour; + switch (dimension) { + case 1:{ + + float df = mix(grad(texture_coordinate.x), dFdx(smp), + smoothstep(0.0f, 0.55f, abs(texture_coordinate.x - 0.5f))); + float de = abs(smp - texture_coordinate.y) / sqrt(1.0f + df * df); + + float eps = length(fwidth(texture_coordinate.xy)); + float thickness = 4.f; + + float alpha = smoothstep((0.5f * thickness + 2.0f) * eps, (0.5f * thickness + 0.0f) * eps, de); + out_colour = vec4(u_bb_colour.xyz, alpha); + }break; + + case 2: + case 3: + { + float t = clamp(sdf_wire_box_outside(p, vec3(1.0f), u_bb_fraction) / u_bb_fraction, 0, 1); + + out_colour = vec4(t * vec3(smp) + (1 - t) * u_bb_colour.xyz, 1); + if (u_solid_bb) out_colour = u_bb_colour; + }break; + } //out_colour = vec4(textureQueryLod(u_texture, texture_coordinate).y, 0, 0, 1); //out_colour = vec4(abs(normal), 1); diff --git a/tests/throughput.c b/tests/throughput.c @@ -293,19 +293,14 @@ execute_study(s8 study, Arena arena, Stream path, Options *options) BeamformerParameters bp = {0}; beamformer_parameters_from_zemp_bp_v1(zbp, &bp); + v3 min_coordinate = (v3){{g_lateral_extent.x, g_axial_extent.x, 0}}; + v3 max_coordinate = (v3){{g_lateral_extent.y, g_axial_extent.y, 0}}; + bp.das_voxel_transform = das_transform(min_coordinate, max_coordinate, &g_output_points); + bp.output_points.xyz = g_output_points; bp.output_points.w = 1; - bp.output_min_coordinate.E[0] = g_lateral_extent.x; - bp.output_min_coordinate.E[1] = 0; - bp.output_min_coordinate.E[2] = g_axial_extent.x; - - bp.output_max_coordinate.E[0] = g_lateral_extent.y; - bp.output_max_coordinate.E[1] = 0; - bp.output_max_coordinate.E[2] = g_axial_extent.y; - bp.f_number = g_f_number; - bp.beamform_plane = 0; bp.interpolation_mode = BeamformerInterpolationMode_Cubic; bp.decimation_rate = 1; @@ -316,9 +311,9 @@ execute_study(s8 study, Arena arena, Stream path, Options *options) #if 0 BeamformerFilterParameters kaiser = {0}; - kaiser.Kaiser.beta = 5.65f; - kaiser.Kaiser.cutoff_frequency = 2.0e6f; - kaiser.Kaiser.length = 36; + kaiser.kaiser.beta = 5.65f; + kaiser.kaiser.cutoff_frequency = 2.0e6f; + kaiser.kaiser.length = 36; beamformer_create_filter(BeamformerFilterKind_Kaiser, (f32 *)&kaiser.kaiser, sizeof(kaiser.kaiser), bp.sampling_frequency / 2, 0, 0, 0); diff --git a/ui.c b/ui.c @@ -1,5 +1,9 @@ /* See LICENSE for license details. */ /* TODO(rnp): + * [ ]: bug: draw_view_ruler() needs to use a ray intersection instead of clamping + * [ ]: bug: plane rotation and offset position only work if plane is Z aligned + * [ ]: refactor: ui and views need to store current uv coordinates for expected transform + * [ ]: refactor: there shouldn't need to be if 1d checks all over * [ ]: refactor: ui kind of needs to be mostly thrown away * - want all drawing to be immediate mode * - only layout information should be retained @@ -119,8 +123,8 @@ typedef enum { } RulerState; typedef struct { - v2 start; - v2 end; + v3 start; + v3 end; RulerState state; } Ruler; @@ -321,9 +325,9 @@ struct BeamformerFrameView { BeamformerFrame *frame; BeamformerFrameView *prev, *next; - iv2 texture_dim; - u32 textures[2]; + u32 texture; i32 texture_mipmaps; + iv2 texture_dim; /* NOTE(rnp): any pointers to variables are added to the menu and will * be put onto the freelist if the view is closed. */ @@ -430,6 +434,12 @@ struct BeamformerUI { b32 flush_params; u32 selected_parameter_block; + m4 das_transform; + v3 min_coordinate; + v3 max_coordinate; + f32 off_axis_position; + f32 beamform_plane; + FrameViewRenderContext *frame_view_render_context; BeamformerSharedMemory * shared_memory; @@ -614,7 +624,7 @@ function Texture make_raylib_texture(BeamformerFrameView *v) { Texture result; - result.id = v->textures[0]; + result.id = v->texture; result.width = v->texture_dim.w; result.height = v->texture_dim.h; result.mipmaps = v->texture_mipmaps; @@ -929,30 +939,30 @@ table_end_subtable(Table *table) } function void -resize_frame_view(BeamformerFrameView *view, iv2 dim, b32 depth) +resize_frame_view(BeamformerFrameView *view, iv2 dim) { - glDeleteTextures(countof(view->textures), view->textures); - glCreateTextures(GL_TEXTURE_2D, depth ? countof(view->textures) : countof(view->textures) - 1, view->textures); + glDeleteTextures(1, &view->texture); + glCreateTextures(GL_TEXTURE_2D, 1, &view->texture); view->texture_dim = dim; - view->texture_mipmaps = (i32)ctz_u32((u32)MAX(dim.x, dim.y)) + 1; - glTextureStorage2D(view->textures[0], view->texture_mipmaps, GL_RGBA8, dim.x, dim.y); - if (depth) glTextureStorage2D(view->textures[1], 1, GL_DEPTH_COMPONENT24, dim.x, dim.y); + view->texture_mipmaps = (i32)ctz_u32((u32)Max(dim.x, dim.y)) + 1; + glTextureStorage2D(view->texture, view->texture_mipmaps, GL_RGBA8, dim.x, dim.y); - glGenerateTextureMipmap(view->textures[0]); + glGenerateTextureMipmap(view->texture); /* NOTE(rnp): work around raylib's janky texture sampling */ - v4 border_colour = {0}; - if (view->kind == BeamformerFrameViewKind_Copy) border_colour = (v4){{0, 0, 0, 1}}; - glTextureParameteri(view->textures[0], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); - glTextureParameteri(view->textures[0], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); - glTextureParameterfv(view->textures[0], GL_TEXTURE_BORDER_COLOR, border_colour.E); + v4 border_colour = (v4){{0, 0, 0, 1}}; + if (view->kind != BeamformerFrameViewKind_Copy) border_colour = (v4){0}; + glTextureParameteri(view->texture, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTextureParameteri(view->texture, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + glTextureParameterfv(view->texture, GL_TEXTURE_BORDER_COLOR, border_colour.E); /* TODO(rnp): better choice when depth component is included */ - glTextureParameteri(view->textures[0], GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTextureParameteri(view->textures[0], GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(view->texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTextureParameteri(view->texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST); /* TODO(rnp): add some ID for the specific view here */ - LABEL_GL_OBJECT(GL_TEXTURE, view->textures[0], s8("Frame View Texture")); + s8 label = s8("Frame View Texture"); + glObjectLabel(GL_TEXTURE, view->texture, (i32)label.len, (char *)label.data); } function void @@ -1198,11 +1208,11 @@ add_beamformer_parameters_view(Variable *parent, BeamformerCtx *ctx) VariableGroupKind_Vector, ui->font); { add_beamformer_variable(ui, group, &ui->arena, s8("Min:"), s8("[mm]"), - bp->output_min_coordinate.E + 0, v2_inf, 1e3f, 0.5e-3f, + ui->min_coordinate.E + 0, v2_inf, 1e3f, 0.5e-3f, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); add_beamformer_variable(ui, group, &ui->arena, s8("Max:"), s8("[mm]"), - bp->output_max_coordinate.E + 0, v2_inf, 1e3f, 0.5e-3f, + ui->max_coordinate.E + 0, v2_inf, 1e3f, 0.5e-3f, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); } group = end_variable_group(group); @@ -1211,22 +1221,22 @@ add_beamformer_parameters_view(Variable *parent, BeamformerCtx *ctx) VariableGroupKind_Vector, ui->font); { add_beamformer_variable(ui, group, &ui->arena, s8("Min:"), s8("[mm]"), - bp->output_min_coordinate.E + 2, v2_inf, 1e3f, 0.5e-3f, + ui->min_coordinate.E + 2, v2_inf, 1e3f, 0.5e-3f, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); add_beamformer_variable(ui, group, &ui->arena, s8("Max:"), s8("[mm]"), - bp->output_max_coordinate.E + 2, v2_inf, 1e3f, 0.5e-3f, + ui->max_coordinate.E + 2, v2_inf, 1e3f, 0.5e-3f, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); } group = end_variable_group(group); add_beamformer_variable(ui, group, &ui->arena, s8("Off Axis Position:"), s8("[mm]"), - &bp->off_axis_pos, (v2){{-1e3f, 1e3f}}, 0.25e3f, 0.5e-3f, + &ui->off_axis_position, v2_inf, 1e3f, 0.5e-3f, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); - read_only local_persist s8 beamform_plane_labels[] = {s8_comp("XZ"), s8_comp("YZ")}; - add_variable_cycler(ui, group, &ui->arena, V_CAUSES_COMPUTE, ui->font, s8("Beamform Plane:"), - (u32 *)&bp->beamform_plane, beamform_plane_labels, countof(beamform_plane_labels)); + add_beamformer_variable(ui, group, &ui->arena, s8("Beamform Plane:"), s8(""), + &ui->beamform_plane, (v2){{0, 1.0f}}, 1.0f, 0.025f, + V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); add_beamformer_variable(ui, group, &ui->arena, s8("F#:"), s8(""), &bp->f_number, (v2){.y = 1e3f}, 1, 0.1f, V_INPUT|V_TEXT|V_CAUSES_COMPUTE, ui->font); @@ -1264,8 +1274,10 @@ ui_beamformer_frame_view_convert(BeamformerUI *ui, Arena *arena, Variable *view, bv->threshold.real32 = old? old->threshold.real32 : 55.0f; bv->gamma.scaled_real32.val = old? old->gamma.scaled_real32.val : 1.0f; bv->gamma.scaled_real32.scale = old? old->gamma.scaled_real32.scale : 0.05f; - bv->min_coordinate = (old && old->frame)? old->frame->min_coordinate : (v3){0}; - bv->max_coordinate = (old && old->frame)? old->frame->max_coordinate : (v3){0}; + bv->min_coordinate = (old && old->frame) ? m4_mul_v4(old->frame->voxel_transform, (v4){{0.0f, 0.0f, 0.0f, 1.0f}}).xyz + : (v3){0}; + bv->max_coordinate = (old && old->frame) ? m4_mul_v4(old->frame->voxel_transform, (v4){{1.0f, 1.0f, 1.0f, 1.0f}}).xyz + : (v3){0}; #define X(_t, pretty) s8_comp(pretty), read_only local_persist s8 kind_labels[] = {BEAMFORMER_FRAME_VIEW_KIND_LIST}; @@ -1273,12 +1285,15 @@ ui_beamformer_frame_view_convert(BeamformerUI *ui, Arena *arena, Variable *view, bv->kind_cycler = add_variable_cycler(ui, menu, arena, V_EXTRA_ACTION, ui->small_font, s8("Kind:"), (u32 *)&bv->kind, kind_labels, countof(kind_labels)); + /* TODO(rnp): this is quite dumb. what we actually want is to render directly + * into the view region with the appropriate size for that region (scissor) */ + resize_frame_view(bv, (iv2){{FRAME_VIEW_RENDER_TARGET_SIZE}}); + switch (kind) { case BeamformerFrameViewKind_3DXPlane:{ view->flags |= V_HIDES_CURSOR; - resize_frame_view(bv, (iv2){{FRAME_VIEW_RENDER_TARGET_SIZE}}, 0); - glTextureParameteri(bv->textures[0], GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTextureParameteri(bv->textures[0], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTextureParameteri(bv->texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTextureParameteri(bv->texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); fill_variable(bv->x_plane_shifts + 0, view, s8("XZ Shift"), V_INPUT|V_HIDES_CURSOR, VT_X_PLANE_SHIFT, ui->small_font); fill_variable(bv->x_plane_shifts + 1, view, s8("YZ Shift"), V_INPUT|V_HIDES_CURSOR, @@ -1299,10 +1314,10 @@ ui_beamformer_frame_view_convert(BeamformerUI *ui, Arena *arena, Variable *view, axial->zoom_starting_coord = F32_INFINITY; b32 copy = kind == BeamformerFrameViewKind_Copy; - lateral->min_value = copy ? &bv->min_coordinate.x : &ui->params.output_min_coordinate.x; - lateral->max_value = copy ? &bv->max_coordinate.x : &ui->params.output_max_coordinate.x; - axial->min_value = copy ? &bv->min_coordinate.z : &ui->params.output_min_coordinate.z; - axial->max_value = copy ? &bv->max_coordinate.z : &ui->params.output_max_coordinate.z; + lateral->min_value = copy ? &bv->min_coordinate.x : &ui->min_coordinate.x; + lateral->max_value = copy ? &bv->max_coordinate.x : &ui->max_coordinate.x; + axial->min_value = copy ? &bv->min_coordinate.z : &ui->min_coordinate.z; + axial->max_value = copy ? &bv->max_coordinate.z : &ui->max_coordinate.z; #define X(id, text) add_button(ui, menu, arena, s8(text), UI_BID_ ##id, 0, ui->small_font); FRAME_VIEW_BUTTONS @@ -1487,8 +1502,6 @@ ui_beamformer_frame_view_copy_frame(BeamformerUI *ui, BeamformerFrameView *new, new->frame->texture, GL_TEXTURE_3D, 0, 0, 0, 0, new->frame->dim.x, new->frame->dim.y, new->frame->dim.z); glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT); - /* TODO(rnp): x vs y here */ - resize_frame_view(new, (iv2){{new->frame->dim.x, new->frame->dim.z}}, 1); } function void @@ -1516,11 +1529,11 @@ beamformer_frame_view_plane_size(BeamformerUI *ui, BeamformerFrameView *view) { v3 result; if (view->kind == BeamformerFrameViewKind_3DXPlane) { - result = v3_sub(ui->params.output_max_coordinate, ui->params.output_min_coordinate); + result = v3_sub(ui->max_coordinate, ui->min_coordinate); swap(result.y, result.z); - result.x = MAX(1e-3f, result.x); - result.y = MAX(1e-3f, result.y); - result.z = MAX(1e-3f, result.z); + result.x = Max(1e-3f, result.x); + result.y = Max(1e-3f, result.y); + result.z = Max(1e-3f, result.z); } else { v2 size = v2_sub(XZ(view->max_coordinate), XZ(view->min_coordinate)); result = (v3){.x = size.x, .y = size.y}; @@ -1549,8 +1562,8 @@ normalized_p_in_rect(Rect r, v2 p, b32 invert_y) function v3 x_plane_position(BeamformerUI *ui) { - f32 y_min = ui->params.output_min_coordinate.E[2]; - f32 y_max = ui->params.output_max_coordinate.E[2]; + f32 y_min = ui->min_coordinate.E[2]; + f32 y_max = ui->max_coordinate.E[2]; v3 result = {.y = y_min + (y_max - y_min) / 2}; return result; } @@ -1645,7 +1658,7 @@ render_single_xplane(BeamformerUI *ui, BeamformerFrameView *view, Variable *x_pl XPlaneShift *xp = &x_plane_shift->x_plane_shift; v3 xp_delta = v3_sub(xp->end_point, xp->start_point); - if (!f32_cmp(v3_magnitude(xp_delta), 0)) { + if (!f32_equal(v3_magnitude(xp_delta), 0)) { m4 x_rotation = m4_rotation_about_y(rotation_turns); v3 Z = x_rotation.c[2].xyz; v3 f = v3_scale(Z, v3_dot(Z, v3_sub(xp->end_point, xp->start_point))); @@ -1692,9 +1705,8 @@ function void render_2D_plane(BeamformerUI *ui, BeamformerFrameView *view, u32 program) { m4 view_m = m4_identity(); - v3 size = beamformer_frame_view_plane_size(ui, view); - m4 model = m4_scale(size); - m4 projection = orthographic_projection(0, 1, size.y / 2, size.x / 2); + m4 model = m4_scale((v3){{2.0f, 2.0f, 0.0f}}); + m4 projection = orthographic_projection(0, 1, 1, 1); glProgramUniformMatrix4fv(program, FRAME_VIEW_MODEL_MATRIX_LOC, 1, 0, model.E); glProgramUniformMatrix4fv(program, FRAME_VIEW_VIEW_MATRIX_LOC, 1, 0, view_m.E); @@ -1722,23 +1734,13 @@ view_update(BeamformerUI *ui, BeamformerFrameView *view) u32 index = *view->cycler->cycler.state; view->dirty |= view->frame != ui->latest_plane[index]; view->frame = ui->latest_plane[index]; - if (view->dirty) { - view->min_coordinate = ui->params.output_min_coordinate; - view->max_coordinate = ui->params.output_max_coordinate; + if (view->dirty && view->frame) { + view->min_coordinate = m4_mul_v4(view->frame->voxel_transform, (v4){{0.0f, 0.0f, 0.0f, 1.0f}}).xyz; + view->max_coordinate = m4_mul_v4(view->frame->voxel_transform, (v4){{1.0f, 1.0f, 1.0f, 1.0f}}).xyz; } } /* TODO(rnp): x-z or y-z */ - /* TODO(rnp): add method of setting a target size in frame view */ - iv2 current = view->texture_dim; - iv2 target = {.w = ui->params.output_points.E[0], .h = ui->params.output_points.E[2]}; - if (view->kind != BeamformerFrameViewKind_Copy && - view->kind != BeamformerFrameViewKind_3DXPlane && - !iv2_equal(current, target) && !iv2_equal(target, (iv2){0})) - { - resize_frame_view(view, target, 1); - view->dirty = 1; - } view->dirty |= ui->frame_view_render_context->updated; view->dirty |= view->kind == BeamformerFrameViewKind_3DXPlane; @@ -1753,6 +1755,8 @@ update_frame_views(BeamformerUI *ui, Rect window) b32 fbo_bound = 0; for (BeamformerFrameView *view = ui->views; view; view = view->next) { if (view_update(ui, view)) { + //start_renderdoc_capture(0); + if (!fbo_bound) { fbo_bound = 1; glBindFramebuffer(GL_FRAMEBUFFER, ctx->framebuffers[0]); @@ -1769,25 +1773,26 @@ update_frame_views(BeamformerUI *ui, Rect window) glProgramUniform1f(program, FRAME_VIEW_GAMMA_LOC, view->gamma.scaled_real32.val); glProgramUniform1ui(program, FRAME_VIEW_LOG_SCALE_LOC, view->log_scale->bool32); + glNamedFramebufferRenderbuffer(fb, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ctx->renderbuffers[0]); + glNamedFramebufferRenderbuffer(fb, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, ctx->renderbuffers[1]); + glClearNamedFramebufferfv(fb, GL_COLOR, 0, (f32 []){0, 0, 0, 0}); + glClearNamedFramebufferfv(fb, GL_DEPTH, 0, (f32 []){1}); + if (view->kind == BeamformerFrameViewKind_3DXPlane) { - glNamedFramebufferRenderbuffer(fb, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ctx->renderbuffers[0]); - glNamedFramebufferRenderbuffer(fb, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, ctx->renderbuffers[1]); - glClearNamedFramebufferfv(fb, GL_COLOR, 0, (f32 []){0, 0, 0, 0}); - glClearNamedFramebufferfv(fb, GL_DEPTH, 0, (f32 []){1}); render_3D_xplane(ui, view, program); - /* NOTE(rnp): resolve multisampled scene */ - glNamedFramebufferTexture(ctx->framebuffers[1], GL_COLOR_ATTACHMENT0, view->textures[0], 0); - glBlitNamedFramebuffer(fb, ctx->framebuffers[1], 0, 0, FRAME_VIEW_RENDER_TARGET_SIZE, - 0, 0, FRAME_VIEW_RENDER_TARGET_SIZE, GL_COLOR_BUFFER_BIT, GL_NEAREST); } else { - glNamedFramebufferTexture(fb, GL_COLOR_ATTACHMENT0, view->textures[0], 0); - glNamedFramebufferTexture(fb, GL_DEPTH_ATTACHMENT, view->textures[1], 0); - glClearNamedFramebufferfv(fb, GL_COLOR, 0, (f32 []){0, 0, 0, 0}); - glClearNamedFramebufferfv(fb, GL_DEPTH, 0, (f32 []){1}); render_2D_plane(ui, view, program); } - glGenerateTextureMipmap(view->textures[0]); + + /* NOTE(rnp): resolve multisampled scene */ + glNamedFramebufferTexture(ctx->framebuffers[1], GL_COLOR_ATTACHMENT0, view->texture, 0); + glBlitNamedFramebuffer(fb, ctx->framebuffers[1], 0, 0, FRAME_VIEW_RENDER_TARGET_SIZE, + 0, 0, view->texture_dim.w, view->texture_dim.h, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + glGenerateTextureMipmap(view->texture); view->dirty = 0; + + //end_renderdoc_capture(0); } } if (fbo_bound) { @@ -1959,7 +1964,24 @@ function b32 point_in_rect(v2 p, Rect r) { v2 end = v2_add(r.pos, r.size); - b32 result = BETWEEN(p.x, r.pos.x, end.x) & BETWEEN(p.y, r.pos.y, end.y); + b32 result = Between(p.x, r.pos.x, end.x) & Between(p.y, r.pos.y, end.y); + return result; +} + +function v2 +rect_uv(v2 p, Rect r) +{ + v2 result = v2_div(v2_sub(p, r.pos), r.size); + return result; +} + +function v3 +world_point_from_plane_uv(m4 world, v2 uv) +{ + v3 U = world.c[0].xyz; + v3 V = world.c[1].xyz; + v3 min = world.c[3].xyz; + v3 result = v3_add(v3_add(v3_scale(U, uv.x), v3_scale(V, uv.y)), min); return result; } @@ -2081,12 +2103,13 @@ draw_ruler(BeamformerUI *ui, Arena arena, v2 start_point, v2 end_point, v2 tp = {{(f32)ui->small_font.baseSize / 2.0f, ep.y + RULER_TEXT_PAD}}; TextSpec text_spec = {.font = &ui->small_font, .rotation = 90.0f, .colour = txt_colour, .flags = TF_ROTATED}; Color rl_txt_colour = colour_from_normalized(txt_colour); + for (u32 j = 0; j <= segments; j++) { DrawLineEx(rl_v2(sp), rl_v2(ep), 3, rl_txt_colour); stream_reset(&buf, 0); if (draw_plus && value > 0) stream_append_byte(&buf, '+'); - stream_append_f64(&buf, value, 10); + stream_append_f64(&buf, value, Abs(value_inc) < 1 ? 100 : 10); stream_append_s8(&buf, suffix); draw_text(stream_to_s8(&buf), tp, &text_spec); @@ -2356,18 +2379,31 @@ function void draw_view_ruler(BeamformerFrameView *view, Arena a, Rect view_rect, TextSpec ts) { v2 vr_max_p = v2_add(view_rect.pos, view_rect.size); - v2 start_p = world_point_to_screen_2d(view->ruler.start, XZ(view->min_coordinate), - XZ(view->max_coordinate), view_rect.pos, vr_max_p); - v2 end_p = world_point_to_screen_2d(view->ruler.end, XZ(view->min_coordinate), - XZ(view->max_coordinate), view_rect.pos, vr_max_p); + + v3 U = view->frame->voxel_transform.c[0].xyz; + v3 V = view->frame->voxel_transform.c[1].xyz; + v3 min = view->frame->voxel_transform.c[3].xyz; + + v2 start_uv = plane_uv(v3_sub(view->ruler.start, min), U, V); + v2 end_uv = plane_uv(v3_sub(view->ruler.end, min), U, V); + + v2 start_p = v2_add(view_rect.pos, v2_mul(start_uv, view_rect.size)); + v2 end_p = v2_add(view_rect.pos, v2_mul(end_uv, view_rect.size)); + + b32 start_in_bounds = point_in_rect(start_p, view_rect); + b32 end_in_bounds = point_in_rect(end_p, view_rect); + + // TODO(rnp): this should be a ray intersection not a clamp + start_p = clamp_v2_rect(start_p, view_rect); + end_p = clamp_v2_rect(end_p, view_rect); Color rl_colour = colour_from_normalized(ts.colour); - DrawCircleV(rl_v2(start_p), 3, rl_colour); DrawLineEx(rl_v2(end_p), rl_v2(start_p), 2, rl_colour); - DrawCircleV(rl_v2(end_p), 3, rl_colour); + if (start_in_bounds) DrawCircleV(rl_v2(start_p), 3, rl_colour); + if (end_in_bounds) DrawCircleV(rl_v2(end_p), 3, rl_colour); Stream buf = arena_stream(a); - stream_append_f64(&buf, 1e3 * v2_magnitude(v2_sub(view->ruler.end, view->ruler.start)), 100); + stream_append_f64(&buf, 1e3 * v3_magnitude(v3_sub(view->ruler.end, view->ruler.start)), 100); stream_append_s8(&buf, s8(" mm")); v2 txt_p = start_p; @@ -2474,13 +2510,27 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect displa BeamformerFrameView *view = var->generic; BeamformerFrame *frame = view->frame; + b32 is_1d = iv3_dimension(frame->dim) == 1; + f32 txt_w = measure_text(ui->small_font, s8("-288.8 mm")).w; f32 scale_bar_size = 1.2f * txt_w + RULER_TICK_LENGTH; - v3 min = view->min_coordinate; - v3 max = view->max_coordinate; - v2 requested_dim = v2_sub(XZ(max), XZ(min)); - f32 aspect = requested_dim.w / requested_dim.h; + v3 U = frame->voxel_transform.c[0].xyz; + v3 V = frame->voxel_transform.c[1].xyz; + + v2 min_uv = plane_uv(view->min_coordinate, U, V); + v2 max_uv = plane_uv(view->max_coordinate, U, V); + + v2 output_dim; + output_dim.x = v3_magnitude(U); + output_dim.y = v3_magnitude(V); + + // NOTE(rnp): may be different from UV if recompute in progress or Copy View + v2 requested_dim; + requested_dim.x = v3_magnitude(v3_sub(v3_scale(U, max_uv.x), v3_scale(U, min_uv.x))); + requested_dim.y = v3_magnitude(v3_sub(v3_scale(V, max_uv.y), v3_scale(V, min_uv.y))); + + f32 aspect = is_1d ? 1.0f : output_dim.w / output_dim.h; Rect vr = display_rect; v2 scale_bar_area = {0}; @@ -2511,23 +2561,26 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect displa occupied = v2_add(vr.size, scale_bar_area); vr.pos = v2_add(vr.pos, v2_scale(v2_sub(display_rect.size, occupied), 0.5)); - /* TODO(rnp): make this depend on the requested draw orientation (x-z or y-z or x-y) */ - v2 output_dim = v2_sub(XZ(frame->max_coordinate), XZ(frame->min_coordinate)); - v2 pixels_per_meter = { - .w = (f32)view->texture_dim.w / output_dim.w, - .h = (f32)view->texture_dim.h / output_dim.h, - }; + Rectangle tex_r; + if (is_1d) { + tex_r = (Rectangle){0, 0, view->texture_dim.x, -view->texture_dim.y}; + } else { + v2 pixels_per_meter = { + .w = (f32)view->texture_dim.w / output_dim.w, + .h = (f32)view->texture_dim.h / output_dim.h, + }; - /* NOTE(rnp): math to resize the texture without stretching when the view changes - * but the texture hasn't been (or cannot be) rebeamformed */ - v2 texture_points = v2_mul(pixels_per_meter, requested_dim); - /* TODO(rnp): this also depends on x-y, y-z, x-z */ - v2 texture_start = { - .x = pixels_per_meter.x * 0.5f * (output_dim.x - requested_dim.x), - .y = pixels_per_meter.y * (frame->max_coordinate.z - max.z), - }; + /* NOTE(rnp): math to resize the texture without stretching when the view changes + * but the texture hasn't been (or cannot be) rebeamformed */ + v2 texture_points = v2_mul(pixels_per_meter, requested_dim); + v2 texture_start = { + .x = pixels_per_meter.x * 0.5f * (output_dim.x - requested_dim.x), + .y = pixels_per_meter.y * (output_dim.y - requested_dim.y), + }; + + tex_r = (Rectangle){texture_start.x, texture_start.y, texture_points.x, texture_points.y}; + } - Rectangle tex_r = {texture_start.x, texture_start.y, texture_points.x, texture_points.y}; NPatchInfo tex_np = { tex_r, 0, 0, 0, 0, NPATCH_NINE_PATCH }; DrawTextureNPatch(make_raylib_texture(view), tex_np, rl_rect(vr), (Vector2){0}, 0, WHITE); @@ -2545,10 +2598,16 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect displa start_pos.x += vr.size.x; if (vr.size.h > 0 && view->axial_scale_bar_active->bool32) { - do_scale_bar(ui, a, &view->axial_scale_bar, mouse, - (Rect){.pos = start_pos, .size = vr.size}, - *view->axial_scale_bar.scale_bar.max_value * 1e3f, - *view->axial_scale_bar.scale_bar.min_value * 1e3f, s8(" mm")); + if (is_1d) { + v2 end_pos = start_pos; + u32 tick_count = (u32)(vr.size.y / (1.5f * (f32)ui->small_font.baseSize)); + start_pos.y += vr.size.y; + draw_ruler(ui, a, start_pos, end_pos, 0.0f, 1.0f, 0, 0, tick_count, s8(""), RULER_COLOUR, FG_COLOUR); + } else { + do_scale_bar(ui, a, &view->axial_scale_bar, mouse, (Rect){.pos = start_pos, .size = vr.size}, + *view->axial_scale_bar.scale_bar.max_value * 1e3f, + *view->axial_scale_bar.scale_bar.min_value * 1e3f, s8(" mm")); + } } TextSpec text_spec = {.font = &ui->small_font, .flags = TF_LIMITED|TF_OUTLINED, @@ -2564,8 +2623,17 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect displa v2 world = screen_point_to_world_2d(mouse, vr.pos, v2_add(vr.pos, vr.size), XZ(view->min_coordinate), XZ(view->max_coordinate)); + world = v2_scale(world, 1e3f); + + if (is_1d) world.y = ((vr.pos.y + vr.size.y) - mouse.y) / vr.size.y; + Stream buf = arena_stream(a); - stream_append_v2(&buf, v2_scale(world, 1e3f)); + stream_append_s8(&buf, s8("{")); + stream_append_f64(&buf, world.x, 100); + if (is_1d) stream_append_s8(&buf, s8(" mm")); + stream_append_s8(&buf, s8(", ")); + stream_append_f64(&buf, world.y, 100); + stream_append_s8(&buf, s8("}")); text_spec.limits.size.w -= 4.0f; v2 txt_s = measure_text(*text_spec.font, stream_to_s8(&buf)); @@ -2573,7 +2641,7 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect displa .x = vr.pos.x + vr.size.w - txt_s.w - 4.0f, .y = vr.pos.y + vr.size.h - txt_s.h - 4.0f, }; - txt_p.x = MAX(vr.pos.x, txt_p.x); + txt_p.x = Max(vr.pos.x, txt_p.x); draw_table_width -= draw_text(stream_to_s8(&buf), txt_p, &text_spec).w; text_spec.limits.size.w += 4.0f; } @@ -2588,7 +2656,7 @@ draw_beamformer_frame_view(BeamformerUI *ui, Arena a, Variable *var, Rect displa .x = vr.pos.x + vr.size.w - txt_s.w - 16, .y = vr.pos.y + 4, }; - txt_p.x = MAX(vr.pos.x, txt_p.x); + txt_p.x = Max(vr.pos.x, txt_p.x); draw_text(stream_to_s8(&buf), txt_p, &text_spec); text_spec.font = &ui->small_font; text_spec.limits.size.w += 16; @@ -3559,6 +3627,10 @@ ui_begin_interact(BeamformerUI *ui, v2 mouse, b32 scroll) begin_text_input(&ui->text_input_state, hot.rect, hot.var, mouse); } ui_widget_bring_to_front(&ui->floating_widget_sentinal, hot.var); + + // TODO(rnp): hack. this won't be needed with a proper immediate mode UI + if (ui->interaction.kind == InteractionKind_Text) + hot.var = hot.var->view.child; }break; case VT_UI_VIEW:{ if (scroll) hot.kind = InteractionKind_Scroll; @@ -3592,11 +3664,8 @@ ui_begin_interact(BeamformerUI *ui, v2 mouse, b32 scroll) switch (++bv->ruler.state) { case RulerState_Start:{ hot.kind = InteractionKind_Ruler; - v2 r_max = v2_add(hot.rect.pos, hot.rect.size); - v2 p = screen_point_to_world_2d(mouse, hot.rect.pos, r_max, - XZ(bv->min_coordinate), - XZ(bv->max_coordinate)); - bv->ruler.start = p; + bv->ruler.start = world_point_from_plane_uv(bv->frame->voxel_transform, + rect_uv(mouse, hot.rect)); }break; case RulerState_Hold:{}break; default:{ bv->ruler.state = RulerState_None; }break; @@ -3834,11 +3903,8 @@ ui_interact(BeamformerUI *ui, BeamformerInput *input, Rect window_rect) case InteractionKind_Ruler:{ assert(it->var->type == VT_BEAMFORMER_FRAME_VIEW); BeamformerFrameView *bv = it->var->generic; - v2 r_max = v2_add(it->rect.pos, it->rect.size); v2 mouse = clamp_v2_rect(input_mouse, it->rect); - bv->ruler.end = screen_point_to_world_2d(mouse, it->rect.pos, r_max, - XZ(bv->min_coordinate), - XZ(bv->max_coordinate)); + bv->ruler.end = world_point_from_plane_uv(bv->frame->voxel_transform, rect_uv(mouse, it->rect)); }break; case InteractionKind_Drag:{ if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { @@ -3870,7 +3936,7 @@ ui_interact(BeamformerUI *ui, BeamformerInput *input, Rect window_rect) BeamformerFrameView *bv = it->var->generic; switch (bv->kind) { case BeamformerFrameViewKind_3DXPlane:{ - bv->rotation += dMouse.x / ws.w; + bv->rotation -= dMouse.x / ws.w; if (bv->rotation > 1.0f) bv->rotation -= 1.0f; if (bv->rotation < 0.0f) bv->rotation += 1.0f; }break; @@ -3969,12 +4035,12 @@ ui_init(BeamformerCtx *ctx, Arena store) } function void -validate_ui_parameters(BeamformerUIParameters *p) +validate_ui_parameters(BeamformerUI *ui) { - if (p->output_min_coordinate.x > p->output_max_coordinate.x) - swap(p->output_min_coordinate.x, p->output_max_coordinate.x); - if (p->output_min_coordinate.z > p->output_max_coordinate.z) - swap(p->output_min_coordinate.z, p->output_max_coordinate.z); + if (ui->min_coordinate.x > ui->max_coordinate.x) + swap(ui->min_coordinate.x, ui->max_coordinate.x); + if (ui->min_coordinate.z > ui->max_coordinate.z) + swap(ui->min_coordinate.z, ui->max_coordinate.z); } function void @@ -3992,10 +4058,16 @@ draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformerFrame *frame_to_dr if (ctx->ui_dirty_parameter_blocks & selected_mask) { BeamformerParameterBlock *pb = beamformer_parameter_block_lock(ui->shared_memory, selected_block, 0); if (pb) { - mem_copy(&ui->params, &pb->parameters_ui, sizeof(ui->params)); ui->flush_params = 0; + + mem_copy(&ui->params, &pb->parameters_ui, sizeof(ui->params)); + mem_copy(ui->das_transform.E, pb->parameters.das_voxel_transform.E, sizeof(ui->das_transform)); + atomic_and_u32(&ctx->ui_dirty_parameter_blocks, ~selected_mask); beamformer_parameter_block_unlock(ui->shared_memory, selected_block); + + ui->min_coordinate = m4_mul_v4(ui->das_transform, (v4){{0.0f, 0.0f, 0.0f, 1.0f}}).xyz; + ui->max_coordinate = m4_mul_v4(ui->das_transform, (v4){{1.0f, 1.0f, 1.0f, 1.0f}}).xyz; } } @@ -4005,17 +4077,58 @@ draw_ui(BeamformerCtx *ctx, BeamformerInput *input, BeamformerFrame *frame_to_dr ui_interact(ui, input, window_rect); if (ui->flush_params) { - validate_ui_parameters(&ui->params); + validate_ui_parameters(ui); if (ctx->latest_frame) { BeamformerParameterBlock *pb = beamformer_parameter_block_lock(ui->shared_memory, selected_block, 0); if (pb) { ui->flush_params = 0; + + v3 min_coordinate = ui->min_coordinate; + v3 max_coordinate = ui->max_coordinate; + + iv3 points = ctx->latest_frame->dim; + i32 dimension = iv3_dimension(points); + + // TODO(rnp): this is immediate mode code that should be in the ui building code + swap(min_coordinate.y, min_coordinate.z); + swap(max_coordinate.y, max_coordinate.z); + m4 new_transform = das_transform(min_coordinate, max_coordinate, &points); + switch (dimension) { + case 1:{}break; + + case 2:{ + v3 U = ui->das_transform.c[0].xyz; + v3 V = ui->das_transform.c[1].xyz; + v3 N = v3_normalize(cross(V, U)); + + v3 rotation_axis = v3_normalize(cross(U, N)); + + m4 T = m4_translation(v3_scale(N, ui->off_axis_position)); + m4 R = m4_rotation_about_axis(rotation_axis, ui->beamform_plane); + + new_transform = m4_mul(R, m4_mul(T, new_transform)); + }break; + + case 3:{ + }break; + } + + // TODO(rnp): super janky code because of the retained mode parameters list. + // when this code is run in the correct place we can just decide inline + b32 recompute = 0; + for EachElement(new_transform.E, it) + recompute |= !f32_equal(new_transform.E[it], pb->parameters.das_voxel_transform.E[it]); + recompute |= !memory_equal(&pb->parameters_ui, &ui->params, sizeof(ui->params)); + mem_copy(&pb->parameters_ui, &ui->params, sizeof(ui->params)); + mem_copy(pb->parameters.das_voxel_transform.E, new_transform.E, sizeof(new_transform)); + mark_parameter_block_region_dirty(ui->shared_memory, selected_block, BeamformerParameterBlockRegion_Parameters); beamformer_parameter_block_unlock(ui->shared_memory, selected_block); - beamformer_queue_compute(ctx, frame_to_draw, selected_block); + if (recompute) + beamformer_queue_compute(ctx, frame_to_draw, selected_block); } } } diff --git a/util.c b/util.c @@ -14,6 +14,16 @@ mem_clear(void *restrict p_, u8 c, iz size) return p; } +function b32 +memory_equal(void *restrict left, void *restrict right, uz n) +{ + u8 *a = left, *b = right; + b32 result = 1; + for (; result && n; n--) + result &= *a++ == *b++; + return result; +} + function void mem_copy(void *restrict dest, void *restrict src, uz n) { @@ -493,7 +503,7 @@ stream_append_f64_e(Stream *s, f64 f) stream_append_byte(s, scale >= 0? '+' : '-'); for (u32 i = prec / 10; i > 1; i /= 10) stream_append_byte(s, '0'); - stream_append_u64(s, (u64)ABS(scale)); + stream_append_u64(s, (u64)Abs(scale)); } function void diff --git a/util.h b/util.h @@ -110,7 +110,6 @@ typedef u64 uptr; #define countof(a) (iz)(sizeof(a) / sizeof(*a)) #define ARRAY_COUNT(a) (sizeof(a) / sizeof(*a)) -#define ABS(x) ((x) < 0 ? (-x) : (x)) #define BETWEEN(x, a, b) ((x) >= (a) && (x) <= (b)) #define CLAMP(x, a, b) ((x) < (a) ? (a) : (x) > (b) ? (b) : (x)) #define CLAMP01(x) CLAMP(x, 0, 1) @@ -121,6 +120,9 @@ typedef u64 uptr; #define SIGN(x) ((x) < 0? -1 : 1) #define swap(a, b) do {typeof(a) __tmp = (a); (a) = (b); (b) = __tmp;} while(0) +#define Abs(a) ((a) < 0 ? -(a) : (a)) +#define Between(x, a, b) ((x) >= (a) && (x) <= (b)) +#define Clamp(x, a, b) ((x) < (a) ? (a) : (x) > (b) ? (b) : (x)) #define Min(a, b) ((a) < (b) ? (a) : (b)) #define Max(a, b) ((a) > (b) ? (a) : (b)) @@ -129,7 +131,7 @@ typedef u64 uptr; #define TOLOWER(c) (((c) | 0x20u)) #define TOUPPER(c) (((c) & ~(0x20u))) -#define f32_cmp(x, y) (ABS((x) - (y)) <= F32_EPSILON * MAX(1.0f, MAX(ABS(x), ABS(y)))) +#define f32_equal(x, y) (Abs((x) - (y)) <= F32_EPSILON * Max(1.0f, Max(Abs(x), Abs(y)))) #define DeferLoop(begin, end) for (i32 _i_ = ((begin), 0); !_i_; _i_ += 1, (end)) #define DeferLoopTag(begin, end, tag) for (i32 __##tag = ((begin), 0); !__##tag ; __##tag += 1, (end)) @@ -138,6 +140,7 @@ typedef u64 uptr; #define EachElement(array, it) (u64 it = 0; it < countof(array); it += 1) #define EachEnumValue(type, it) (type it = (type)0; it < type##_Count; it = (type)(it + 1)) #define EachNonZeroEnumValue(type, it) (type it = (type)1; it < type##_Count; it = (type)(it + 1)) +#define EachIndex(count, it) (u64 it = 0; it < count; it += 1) #define spin_wait(c) while ((c)) cpu_yield()