From 9919ac59bb5136f3c128c036adc3b286907bbd97 Mon Sep 17 00:00:00 2001 From: Ryan Fleury Date: Mon, 20 May 2024 10:58:45 -0700 Subject: [PATCH] move module image info parse / cache management to ctrl thread, directly tie to module lifetime as ctrl thread sees it; reduce load from each unwind --- src/ctrl/ctrl_core.c | 438 ++++++++++++++++++++++++++---------------- src/ctrl/ctrl_core.h | 49 +++++ src/df/core/df_core.c | 2 +- src/df/gfx/df_gfx.c | 13 +- 4 files changed, 330 insertions(+), 172 deletions(-) diff --git a/src/ctrl/ctrl_core.c b/src/ctrl/ctrl_core.c index fb1cb63e..4bbe61d6 100644 --- a/src/ctrl/ctrl_core.c +++ b/src/ctrl/ctrl_core.c @@ -845,6 +845,15 @@ ctrl_init(void) ctrl_state->thread_reg_cache.stripes[idx].arena = arena_alloc(); ctrl_state->thread_reg_cache.stripes[idx].rw_mutex = os_rw_mutex_alloc(); } + ctrl_state->module_image_info_cache.slots_count = 1024; + ctrl_state->module_image_info_cache.slots = push_array(arena, CTRL_ModuleImageInfoCacheSlot, ctrl_state->module_image_info_cache.slots_count); + ctrl_state->module_image_info_cache.stripes_count = os_logical_core_count(); + ctrl_state->module_image_info_cache.stripes = push_array(arena, CTRL_ModuleImageInfoCacheStripe, ctrl_state->module_image_info_cache.stripes_count); + for(U64 idx = 0; idx < ctrl_state->module_image_info_cache.stripes_count; idx += 1) + { + ctrl_state->module_image_info_cache.stripes[idx].arena = arena_alloc(); + ctrl_state->module_image_info_cache.stripes[idx].rw_mutex = os_rw_mutex_alloc(); + } ctrl_state->u2c_ring_size = KB(64); ctrl_state->u2c_ring_base = push_array_no_zero(arena, U8, ctrl_state->u2c_ring_size); ctrl_state->u2c_ring_mutex = os_mutex_alloc(); @@ -1453,6 +1462,78 @@ ctrl_thread_write_reg_block(CTRL_MachineID machine_id, DMN_Handle thread, void * return good; } +//////////////////////////////// +//~ rjf: Module Image Info Functions + +//- rjf: cache lookups + +internal PE_IntelPdata * +ctrl_intel_pdata_from_module_voff(Arena *arena, CTRL_MachineID machine_id, DMN_Handle module_handle, U64 voff) +{ + PE_IntelPdata *first_pdata = 0; + { + U64 hash = ctrl_hash_from_machine_id_handle(machine_id, module_handle); + U64 slot_idx = hash%ctrl_state->module_image_info_cache.slots_count; + U64 stripe_idx = slot_idx%ctrl_state->module_image_info_cache.stripes_count; + CTRL_ModuleImageInfoCacheSlot *slot = &ctrl_state->module_image_info_cache.slots[slot_idx]; + CTRL_ModuleImageInfoCacheStripe *stripe = &ctrl_state->module_image_info_cache.stripes[stripe_idx]; + OS_MutexScopeR(stripe->rw_mutex) for(CTRL_ModuleImageInfoCacheNode *n = slot->first; n != 0; n = n->next) + { + if(n->machine_id == machine_id && dmn_handle_match(n->module, module_handle)) + { + PE_IntelPdata *pdatas = n->pdatas; + U64 pdatas_count = n->pdatas_count; + if(n->pdatas_count != 0 && voff >= n->pdatas[0].voff_first) + { + // NOTE(rjf): + // + // binary search: + // find max index s.t. pdata_array[index].voff_first <= voff + // we assume (i < j) -> (pdata_array[i].voff_first < pdata_array[j].voff_first) + U64 index = pdatas_count; + U64 min = 0; + U64 opl = pdatas_count; + for(;;) + { + U64 mid = (min + opl)/2; + PE_IntelPdata *pdata = pdatas + mid; + if(voff < pdata->voff_first) + { + opl = mid; + } + else if(pdata->voff_first < voff) + { + min = mid; + } + else + { + index = mid; + break; + } + if(min + 1 >= opl) + { + index = min; + break; + } + } + + // rjf: if we are in range fill result + { + PE_IntelPdata *pdata = pdatas + index; + if(pdata->voff_first <= voff && voff < pdata->voff_one_past_last) + { + first_pdata = push_array(arena, PE_IntelPdata, 1); + MemoryCopyStruct(first_pdata, pdata); + } + } + } + break; + } + } + } + return first_pdata; +} + //////////////////////////////// //~ rjf: Unwinding Functions @@ -1521,169 +1602,9 @@ ctrl_unwind_step__pe_x64(CTRL_EntityStore *store, CTRL_MachineID machine_id, DMN U64 rip_voff = regs->rip.u64 - module->vaddr_range.min; ////////////////////////////// - //- rjf: unpack relevant PE info + //- rjf: rip_voff -> first pdata // - PE_IntelPdata *pdatas = 0; - U64 pdatas_count = 0; - ProfScope("unpack relevant PE info") if(module != &ctrl_entity_nil) - { - B32 is_valid = 1; - - //- rjf: read DOS header - PE_DosHeader dos_header = {0}; - if(is_valid) - { - if(!ctrl_read_cached_process_memory_struct(machine_id, process->handle, module->vaddr_range.min, &is_stale, &dos_header, endt_us) || - is_stale || - dos_header.magic != PE_DOS_MAGIC) - { - is_valid = 0; - is_good = 0; - } - } - - //- rjf: read PE magic - U32 pe_magic = 0; - if(is_valid) - { - if(!ctrl_read_cached_process_memory_struct(machine_id, process->handle, module->vaddr_range.min + dos_header.coff_file_offset, &is_stale, &pe_magic, endt_us) || - is_stale || - pe_magic != PE_MAGIC) - { - is_valid = 0; - is_good = 0; - } - } - - //- rjf: read COFF header - U64 coff_header_off = dos_header.coff_file_offset + sizeof(pe_magic); - COFF_Header coff_header = {0}; - if(is_valid) - { - if(!ctrl_read_cached_process_memory_struct(machine_id, process->handle, module->vaddr_range.min + coff_header_off, &is_stale, &coff_header, endt_us) || - is_stale) - { - is_valid = 0; - is_good = 0; - } - } - - //- rjf: unpack range of optional extension header - U32 opt_ext_size = coff_header.optional_header_size; - Rng1U64 opt_ext_off_range = r1u64(coff_header_off + sizeof(coff_header), - coff_header_off + sizeof(coff_header) + opt_ext_size); - - //- rjf: read optional header - U16 optional_magic = 0; - U64 image_base = 0; - U64 entry_point = 0; - U32 data_dir_count = 0; - U64 virt_section_align = 0; - U64 file_section_align = 0; - Rng1U64 *data_dir_franges = 0; - if(opt_ext_size > 0) - { - // rjf: read magic number - U16 opt_ext_magic = 0; - ctrl_read_cached_process_memory_struct(machine_id, process->handle, module->vaddr_range.min + opt_ext_off_range.min, &is_stale, &opt_ext_magic, endt_us); - - // rjf: read info - U32 reported_data_dir_offset = 0; - U32 reported_data_dir_count = 0; - switch(opt_ext_magic) - { - case PE_PE32_MAGIC: - { - PE_OptionalHeader32 pe_optional = {0}; - ctrl_read_cached_process_memory_struct(machine_id, process->handle, module->vaddr_range.min + opt_ext_off_range.min, &is_stale, &pe_optional, endt_us); - image_base = pe_optional.image_base; - entry_point = pe_optional.entry_point_va; - virt_section_align = pe_optional.section_alignment; - file_section_align = pe_optional.file_alignment; - reported_data_dir_offset = sizeof(pe_optional); - reported_data_dir_count = pe_optional.data_dir_count; - }break; - case PE_PE32PLUS_MAGIC: - { - PE_OptionalHeader32Plus pe_optional = {0}; - ctrl_read_cached_process_memory_struct(machine_id, process->handle, module->vaddr_range.min + opt_ext_off_range.min, &is_stale, &pe_optional, endt_us); - image_base = pe_optional.image_base; - entry_point = pe_optional.entry_point_va; - virt_section_align = pe_optional.section_alignment; - file_section_align = pe_optional.file_alignment; - reported_data_dir_offset = sizeof(pe_optional); - reported_data_dir_count = pe_optional.data_dir_count; - }break; - } - - // rjf: find number of data directories - U32 data_dir_max = (opt_ext_size - reported_data_dir_offset) / sizeof(PE_DataDirectory); - data_dir_count = ClampTop(reported_data_dir_count, data_dir_max); - - // rjf: grab pdatas from exceptions section - if(data_dir_count > PE_DataDirectoryIndex_EXCEPTIONS) - { - PE_DataDirectory dir = {0}; - ctrl_read_cached_process_memory_struct(machine_id, process->handle, module->vaddr_range.min + opt_ext_off_range.min + reported_data_dir_offset + sizeof(PE_DataDirectory)*PE_DataDirectoryIndex_EXCEPTIONS, &is_stale, &dir, endt_us); - Rng1U64 pdatas_voff_range = r1u64((U64)dir.virt_off, (U64)dir.virt_off + (U64)dir.virt_size); - pdatas_count = dim_1u64(pdatas_voff_range)/sizeof(PE_IntelPdata); - pdatas = push_array(scratch.arena, PE_IntelPdata, pdatas_count); - ctrl_read_cached_process_memory(machine_id, process->handle, r1u64(module->vaddr_range.min + pdatas_voff_range.min, module->vaddr_range.min + pdatas_voff_range.max), &is_stale, pdatas, endt_us); - } - } - } - - ////////////////////////////// - //- rjf: find current rip's pdata entry - // - PE_IntelPdata *first_pdata = 0; - if(pdatas_count != 0) ProfScope("find current RIP's pdata entry") - { - U64 first_pdata_voff = 0; - if(rip_voff >= pdatas[0].voff_first) - { - // NOTE(rjf): - // - // binary search: - // find max index s.t. pdata_array[index].voff_first <= voff - // we assume (i < j) -> (pdata_array[i].voff_first < pdata_array[j].voff_first) - U64 index = pdatas_count; - U64 min = 0; - U64 opl = pdatas_count; - for(;;) - { - U64 mid = (min + opl)/2; - PE_IntelPdata *pdata = pdatas + mid; - if(rip_voff < pdata->voff_first) - { - opl = mid; - } - else if(pdata->voff_first < rip_voff) - { - min = mid; - } - else - { - index = mid; - break; - } - if(min + 1 >= opl) - { - index = min; - break; - } - } - - // rjf: if we are in range fill result - { - PE_IntelPdata *pdata = pdatas + index; - if(pdata->voff_first <= rip_voff && rip_voff < pdata->voff_one_past_last) - { - first_pdata = pdata; - } - } - } - } + PE_IntelPdata *first_pdata = ctrl_intel_pdata_from_module_voff(scratch.arena, machine_id, module_handle, rip_voff); ////////////////////////////// //- rjf: pdata -> detect if in epilog @@ -2955,6 +2876,195 @@ ctrl_thread__append_resolved_process_user_bp_traps(Arena *arena, CTRL_MachineID } } +//- rjf: module lifetime open/close work + +internal void +ctrl_thread__module_open(CTRL_MachineID machine_id, DMN_Handle process, DMN_Handle module, Rng1U64 vaddr_range, String8 path) +{ + ////////////////////////////// + //- rjf: open debug info + // + dbgi_binary_open(path); + + ////////////////////////////// + //- rjf: parse module image info + // + Arena *arena = arena_alloc(); + PE_IntelPdata *pdatas = 0; + U64 pdatas_count = 0; + ProfScope("unpack relevant PE info") + { + B32 is_valid = 1; + + //- rjf: read DOS header + PE_DosHeader dos_header = {0}; + if(is_valid) + { + if(!dmn_process_read_struct(process, vaddr_range.min, &dos_header) || + dos_header.magic != PE_DOS_MAGIC) + { + is_valid = 0; + } + } + + //- rjf: read PE magic + U32 pe_magic = 0; + if(is_valid) + { + if(!dmn_process_read_struct(process, vaddr_range.min + dos_header.coff_file_offset, &pe_magic) || + pe_magic != PE_MAGIC) + { + is_valid = 0; + } + } + + //- rjf: read COFF header + U64 coff_header_off = dos_header.coff_file_offset + sizeof(pe_magic); + COFF_Header coff_header = {0}; + if(is_valid) + { + if(!dmn_process_read_struct(process, vaddr_range.min + coff_header_off, &coff_header)) + { + is_valid = 0; + } + } + + //- rjf: unpack range of optional extension header + U32 opt_ext_size = coff_header.optional_header_size; + Rng1U64 opt_ext_off_range = r1u64(coff_header_off + sizeof(coff_header), + coff_header_off + sizeof(coff_header) + opt_ext_size); + + //- rjf: read optional header + U16 optional_magic = 0; + U64 image_base = 0; + U64 entry_point = 0; + U32 data_dir_count = 0; + U64 virt_section_align = 0; + U64 file_section_align = 0; + Rng1U64 *data_dir_franges = 0; + if(opt_ext_size > 0) + { + // rjf: read magic number + U16 opt_ext_magic = 0; + dmn_process_read_struct(process, vaddr_range.min + opt_ext_off_range.min, &opt_ext_magic); + + // rjf: read info + U32 reported_data_dir_offset = 0; + U32 reported_data_dir_count = 0; + switch(opt_ext_magic) + { + case PE_PE32_MAGIC: + { + PE_OptionalHeader32 pe_optional = {0}; + dmn_process_read_struct(process, vaddr_range.min + opt_ext_off_range.min, &pe_optional); + image_base = pe_optional.image_base; + entry_point = pe_optional.entry_point_va; + virt_section_align = pe_optional.section_alignment; + file_section_align = pe_optional.file_alignment; + reported_data_dir_offset = sizeof(pe_optional); + reported_data_dir_count = pe_optional.data_dir_count; + }break; + case PE_PE32PLUS_MAGIC: + { + PE_OptionalHeader32Plus pe_optional = {0}; + dmn_process_read_struct(process, vaddr_range.min + opt_ext_off_range.min, &pe_optional); + image_base = pe_optional.image_base; + entry_point = pe_optional.entry_point_va; + virt_section_align = pe_optional.section_alignment; + file_section_align = pe_optional.file_alignment; + reported_data_dir_offset = sizeof(pe_optional); + reported_data_dir_count = pe_optional.data_dir_count; + }break; + } + + // rjf: find number of data directories + U32 data_dir_max = (opt_ext_size - reported_data_dir_offset) / sizeof(PE_DataDirectory); + data_dir_count = ClampTop(reported_data_dir_count, data_dir_max); + + // rjf: grab pdatas from exceptions section + if(data_dir_count > PE_DataDirectoryIndex_EXCEPTIONS) + { + PE_DataDirectory dir = {0}; + dmn_process_read_struct(process, vaddr_range.min + opt_ext_off_range.min + reported_data_dir_offset + sizeof(PE_DataDirectory)*PE_DataDirectoryIndex_EXCEPTIONS, &dir); + Rng1U64 pdatas_voff_range = r1u64((U64)dir.virt_off, (U64)dir.virt_off + (U64)dir.virt_size); + pdatas_count = dim_1u64(pdatas_voff_range)/sizeof(PE_IntelPdata); + pdatas = push_array(arena, PE_IntelPdata, pdatas_count); + dmn_process_read(process, r1u64(vaddr_range.min + pdatas_voff_range.min, vaddr_range.min + pdatas_voff_range.max), pdatas); + } + } + } + + ////////////////////////////// + //- rjf: insert into cache + // + { + U64 hash = ctrl_hash_from_machine_id_handle(machine_id, module); + U64 slot_idx = hash%ctrl_state->module_image_info_cache.slots_count; + U64 stripe_idx = slot_idx%ctrl_state->module_image_info_cache.stripes_count; + CTRL_ModuleImageInfoCacheSlot *slot = &ctrl_state->module_image_info_cache.slots[slot_idx]; + CTRL_ModuleImageInfoCacheStripe *stripe = &ctrl_state->module_image_info_cache.stripes[stripe_idx]; + OS_MutexScopeW(stripe->rw_mutex) + { + CTRL_ModuleImageInfoCacheNode *node = 0; + for(CTRL_ModuleImageInfoCacheNode *n = slot->first; n != 0; n = n->next) + { + if(n->machine_id == machine_id && dmn_handle_match(n->module, module)) + { + node = n; + break; + } + } + if(!node) + { + node = push_array(arena, CTRL_ModuleImageInfoCacheNode, 1); + DLLPushBack(slot->first, slot->last, node); + node->machine_id = machine_id; + node->module = module; + node->arena = arena; + node->pdatas = pdatas; + node->pdatas_count = pdatas_count; + } + } + } +} + +internal void +ctrl_thread__module_close(CTRL_MachineID machine_id, DMN_Handle module, String8 path) +{ + ////////////////////////////// + //- rjf: evict module image info from cache + // + { + U64 hash = ctrl_hash_from_machine_id_handle(machine_id, module); + U64 slot_idx = hash%ctrl_state->module_image_info_cache.slots_count; + U64 stripe_idx = slot_idx%ctrl_state->module_image_info_cache.stripes_count; + CTRL_ModuleImageInfoCacheSlot *slot = &ctrl_state->module_image_info_cache.slots[slot_idx]; + CTRL_ModuleImageInfoCacheStripe *stripe = &ctrl_state->module_image_info_cache.stripes[stripe_idx]; + OS_MutexScopeW(stripe->rw_mutex) + { + CTRL_ModuleImageInfoCacheNode *node = 0; + for(CTRL_ModuleImageInfoCacheNode *n = slot->first; n != 0; n = n->next) + { + if(n->machine_id == machine_id && dmn_handle_match(n->module, module)) + { + node = n; + break; + } + } + if(node) + { + DLLRemove(slot->first, slot->last, node); + arena_release(node->arena); + } + } + } + + ////////////////////////////// + //- rjf: close debug info + // + dbgi_binary_close(path); +} + //- rjf: attached process running/event gathering internal DMN_Event * @@ -3220,7 +3330,7 @@ ctrl_thread__next_dmn_event(Arena *arena, DMN_CtrlCtx *ctrl_ctx, CTRL_Msg *msg, { CTRL_Event *out_evt = ctrl_event_list_push(scratch.arena, &evts); String8 module_path = event->string; - dbgi_binary_open(module_path); + ctrl_thread__module_open(CTRL_MachineID_Local, event->process, event->module, r1u64(event->address, event->address+event->size), module_path); out_evt->kind = CTRL_EventKind_NewModule; out_evt->msg_id = msg->msg_id; out_evt->machine_id = CTRL_MachineID_Local; @@ -3255,7 +3365,7 @@ ctrl_thread__next_dmn_event(Arena *arena, DMN_CtrlCtx *ctrl_ctx, CTRL_Msg *msg, { CTRL_Event *out_evt = ctrl_event_list_push(scratch.arena, &evts); String8 module_path = event->string; - dbgi_binary_close(module_path); + ctrl_thread__module_close(CTRL_MachineID_Local, event->module, module_path); out_evt->kind = CTRL_EventKind_EndModule; out_evt->msg_id = msg->msg_id; out_evt->machine_id = CTRL_MachineID_Local; diff --git a/src/ctrl/ctrl_core.h b/src/ctrl/ctrl_core.h index e940c0eb..b5d0d281 100644 --- a/src/ctrl/ctrl_core.h +++ b/src/ctrl/ctrl_core.h @@ -497,6 +497,44 @@ struct CTRL_ThreadRegCache CTRL_ThreadRegCacheStripe *stripes; }; +//////////////////////////////// +//~ rjf: Module Image Info Cache Types + +typedef struct CTRL_ModuleImageInfoCacheNode CTRL_ModuleImageInfoCacheNode; +struct CTRL_ModuleImageInfoCacheNode +{ + CTRL_ModuleImageInfoCacheNode *next; + CTRL_ModuleImageInfoCacheNode *prev; + CTRL_MachineID machine_id; + DMN_Handle module; + Arena *arena; + PE_IntelPdata *pdatas; + U64 pdatas_count; +}; + +typedef struct CTRL_ModuleImageInfoCacheSlot CTRL_ModuleImageInfoCacheSlot; +struct CTRL_ModuleImageInfoCacheSlot +{ + CTRL_ModuleImageInfoCacheNode *first; + CTRL_ModuleImageInfoCacheNode *last; +}; + +typedef struct CTRL_ModuleImageInfoCacheStripe CTRL_ModuleImageInfoCacheStripe; +struct CTRL_ModuleImageInfoCacheStripe +{ + Arena *arena; + OS_Handle rw_mutex; +}; + +typedef struct CTRL_ModuleImageInfoCache CTRL_ModuleImageInfoCache; +struct CTRL_ModuleImageInfoCache +{ + U64 slots_count; + CTRL_ModuleImageInfoCacheSlot *slots; + U64 stripes_count; + CTRL_ModuleImageInfoCacheStripe *stripes; +}; + //////////////////////////////// //~ rjf: Wakeup Hook Function Types @@ -519,6 +557,7 @@ struct CTRL_State // rjf: caches CTRL_ProcessMemoryCache process_memory_cache; CTRL_ThreadRegCache thread_reg_cache; + CTRL_ModuleImageInfoCache module_image_info_cache; // rjf: user -> ctrl msg ring buffer U64 u2c_ring_size; @@ -694,6 +733,12 @@ internal U64 ctrl_query_cached_rip_from_thread(CTRL_EntityStore *store, CTRL_Mac //- rjf: thread register writing internal B32 ctrl_thread_write_reg_block(CTRL_MachineID machine_id, DMN_Handle thread, void *block); +//////////////////////////////// +//~ rjf: Module Image Info Functions + +//- rjf: cache lookups +internal PE_IntelPdata *ctrl_intel_pdata_from_module_voff(Arena *arena, CTRL_MachineID machine_id, DMN_Handle module_handle, U64 voff); + //////////////////////////////// //~ rjf: Unwinding Functions @@ -745,6 +790,10 @@ internal void ctrl_thread__entry_point(void *p); internal void ctrl_thread__append_resolved_module_user_bp_traps(Arena *arena, CTRL_MachineID machine_id, DMN_Handle process, DMN_Handle module, CTRL_UserBreakpointList *user_bps, DMN_TrapChunkList *traps_out); internal void ctrl_thread__append_resolved_process_user_bp_traps(Arena *arena, CTRL_MachineID machine_id, DMN_Handle process, CTRL_UserBreakpointList *user_bps, DMN_TrapChunkList *traps_out); +//- rjf: module lifetime open/close work +internal void ctrl_thread__module_open(CTRL_MachineID machine_id, DMN_Handle process, DMN_Handle module, Rng1U64 vaddr_range, String8 path); +internal void ctrl_thread__module_close(CTRL_MachineID machine_id, DMN_Handle module, String8 path); + //- rjf: attached process running/event gathering internal DMN_Event *ctrl_thread__next_dmn_event(Arena *arena, DMN_CtrlCtx *ctrl_ctx, CTRL_Msg *msg, DMN_RunCtrls *run_ctrls, CTRL_Spoof *spoof); diff --git a/src/df/core/df_core.c b/src/df/core/df_core.c index 9ed6b865..c9ed1bf1 100644 --- a/src/df/core/df_core.c +++ b/src/df/core/df_core.c @@ -6233,7 +6233,7 @@ df_query_cached_unwind_from_thread(DF_Entity *thread) //- rjf: no node? -> calculate unwind if needed - store if good & return if(node == 0) { - CTRL_Unwind current_unwind = ctrl_unwind_from_thread(scratch.arena, df_state->ctrl_entity_store, thread->ctrl_machine_id, thread->ctrl_handle, 0); + CTRL_Unwind current_unwind = ctrl_unwind_from_thread(scratch.arena, df_state->ctrl_entity_store, thread->ctrl_machine_id, thread->ctrl_handle, os_now_microseconds()+200); if(!(current_unwind.flags & (CTRL_UnwindFlag_Error|CTRL_UnwindFlag_Stale))) { Architecture arch = df_architecture_from_entity(thread); diff --git a/src/df/gfx/df_gfx.c b/src/df/gfx/df_gfx.c index 42f34332..04a5edbc 100644 --- a/src/df/gfx/df_gfx.c +++ b/src/df/gfx/df_gfx.c @@ -10059,13 +10059,12 @@ df_entity_desc_button(DF_Window *ws, DF_Entity *entity, FuzzyMatchRangeList *nam ui_set_next_background_color(bg_color); } ui_set_next_hover_cursor(OS_Cursor_HandPoint); - UI_Key key = ui_key_from_stringf(ui_top_parent()->key, "entity_ref_button_%p", entity); - UI_Box *box = ui_build_box_from_key(UI_BoxFlag_Clickable| - UI_BoxFlag_DrawBorder| - UI_BoxFlag_DrawBackground| - UI_BoxFlag_DrawHotEffects| - UI_BoxFlag_DrawActiveEffects, - key); + UI_Box *box = ui_build_box_from_stringf(UI_BoxFlag_Clickable| + UI_BoxFlag_DrawBorder| + UI_BoxFlag_DrawBackground| + UI_BoxFlag_DrawHotEffects| + UI_BoxFlag_DrawActiveEffects, + "entity_ref_button_%p", entity); //- rjf: build contents UI_Parent(box) UI_PrefWidth(ui_text_dim(10, 0))