Fixed a long-standing issue with the chained hashmap (finally)

This commit is contained in:
Edward R. Gonzalez 2024-06-21 00:26:29 -04:00
parent c405c47e6c
commit a560222d5d
9 changed files with 146 additions and 92 deletions

View File

@ -4,6 +4,8 @@ package VEFontCache
The choice was made to keep the LRU cache implementation as close to the original as possible.
*/
import "base:runtime"
PoolListIter :: i32
PoolListValue :: u64
@ -20,9 +22,10 @@ PoolList :: struct {
back : PoolListIter,
size : u32,
capacity : u32,
dbg_name : string,
}
pool_list_init :: proc( pool : ^PoolList, capacity : u32 )
pool_list_init :: proc( pool : ^PoolList, capacity : u32, dbg_name : string = "" )
{
error : AllocatorError
pool.items, error = make( Array( PoolListItem ), u64(capacity) )
@ -35,6 +38,7 @@ pool_list_init :: proc( pool : ^PoolList, capacity : u32 )
pool.capacity = capacity
pool.dbg_name = dbg_name
using pool
for id in 0 ..< capacity {
@ -72,6 +76,9 @@ pool_list_push_front :: proc( pool : ^PoolList, value : PoolListValue )
items.data[ id ].prev = -1
items.data[ id ].next = front
items.data[ id ].value = value
if pool.dbg_name != "" {
logf("pool_list: pushed %v into id %v", value, id)
}
if front != -1 do items.data[ front ].prev = id
if back == -1 do back = id
@ -98,6 +105,9 @@ pool_list_erase :: proc( pool : ^PoolList, iter : PoolListIter )
iter_node.prev = -1
iter_node.next = -1
// if pool.dbg_name != "" {
// logf("pool_list: erased %v, at id %v", iter_node.value, iter)
// }
iter_node.value = 0
append( & free_list, iter )
@ -115,34 +125,35 @@ pool_list_peek_back :: proc ( pool : ^PoolList ) -> PoolListValue {
}
pool_list_pop_back :: proc( pool : ^PoolList ) -> PoolListValue {
using pool
if size <= 0 do return 0
assert( back != -1 )
if pool.size <= 0 do return 0
assert( pool.back != -1 )
value := items.data[ back ].value
pool_list_erase( pool, back )
value := pool.items.data[ pool.back ].value
pool_list_erase( pool, pool.back )
return value
}
LRU_Link :: struct {
pad_top : u64,
value : i32,
ptr : PoolListIter,
pad_bottom : u64,
}
LRU_Cache :: struct {
capacity : u32,
num : u32,
table : HMapZPL(LRU_Link),
table : HMapChained(LRU_Link),
key_queue : PoolList,
}
LRU_init :: proc( cache : ^LRU_Cache, capacity : u32 ) {
LRU_init :: proc( cache : ^LRU_Cache, capacity : u32, dbg_name : string = "" ) {
error : AllocatorError
cache.capacity = capacity
cache.table, error = hmap_zpl_init( HMapZPL(LRU_Link), u64( hmap_closest_prime( uint(capacity))) )
cache.table, error = make( HMapChained(LRU_Link), hmap_closest_prime( uint(capacity)) )
assert( error == .None, "VEFontCache.LRU_init : Failed to allocate cache's table")
pool_list_init( & cache.key_queue, capacity )
pool_list_init( & cache.key_queue, capacity, dbg_name = dbg_name )
}
LRU_free :: proc( cache : ^LRU_Cache )
@ -152,7 +163,7 @@ LRU_free :: proc( cache : ^LRU_Cache )
LRU_reload :: proc( cache : ^LRU_Cache, allocator : Allocator )
{
hmap_zpl_reload( & cache.table, allocator )
hmap_chained_reload( cache.table, allocator )
pool_list_reload( & cache.key_queue, allocator )
}
@ -162,9 +173,13 @@ LRU_hash_key :: #force_inline proc( key : u64 ) -> ( hash : u64 ) {
return
}
LRU_find :: proc( cache : ^LRU_Cache, key : u64 ) -> ^LRU_Link {
LRU_find :: proc( cache : ^LRU_Cache, key : u64, must_find := false ) -> ^LRU_Link {
hash := LRU_hash_key( key )
link := get( & cache.table, hash )
link := get( cache.table, hash )
// if link == nil && must_find {
// runtime.debug_trap()
// link = get( cache.table, hash )
// }
return link
}
@ -186,8 +201,8 @@ LRU_get_next_evicted :: proc( cache : ^LRU_Cache ) -> u64
return 0xFFFFFFFFFFFFFFFF
}
LRU_peek :: proc( cache : ^LRU_Cache, key : u64 ) -> i32 {
iter := LRU_find( cache, key )
LRU_peek :: proc( cache : ^LRU_Cache, key : u64, must_find := false ) -> i32 {
iter := LRU_find( cache, key, must_find )
if iter == nil {
return -1
}
@ -197,7 +212,7 @@ LRU_peek :: proc( cache : ^LRU_Cache, key : u64 ) -> i32 {
LRU_put :: proc( cache : ^LRU_Cache, key : u64, value : i32 ) -> u64
{
hash_key := LRU_hash_key( key )
iter := get( & cache.table, hash_key )
iter := get( cache.table, hash_key )
if iter != nil {
LRU_refresh( cache, key )
iter.value = value
@ -209,14 +224,19 @@ LRU_put :: proc( cache : ^LRU_Cache, key : u64, value : i32 ) -> u64
evict = pool_list_pop_back( & cache.key_queue )
evict_hash := LRU_hash_key( evict )
hmap_zpl_remove( & cache.table, evict_hash )
// if cache.table.dbg_name != "" {
// logf("%v: Evicted %v with hash: %v", cache.table.dbg_name, evict, evict_hash)
// }
hmap_chained_remove( cache.table, evict_hash )
cache.num -= 1
}
pool_list_push_front( & cache.key_queue, key )
// if cache.table.dbg_name != "" {
// logf("%v: Pushed %v with hash: %v", cache.table.dbg_name, key, hash_key )
// }
// set( cache.table, hash_key, LRU_Link {
set( & cache.table, hash_key, LRU_Link {
set( cache.table, hash_key, LRU_Link {
value = value,
ptr = cache.key_queue.front
})
@ -227,6 +247,9 @@ LRU_put :: proc( cache : ^LRU_Cache, key : u64, value : i32 ) -> u64
LRU_refresh :: proc( cache : ^LRU_Cache, key : u64 ) {
link := LRU_find( cache, key )
// if cache.table.dbg_name != "" {
// logf("%v: Refreshed %v", cache.table.dbg_name, key)
// }
pool_list_erase( & cache.key_queue, link.ptr )
pool_list_push_front( & cache.key_queue, key )
link.ptr = cache.key_queue.front

View File

@ -15,6 +15,7 @@ Changes:
*/
package VEFontCache
import "base:runtime"
import "core:math"
import "core:mem"
@ -625,7 +626,7 @@ cache_glyph_to_atlas :: proc( ctx : ^Context, font : FontID, glyph_index : Glyph
next_evict_codepoint := LRU_get_next_evicted( & region.state )
assert( next_evict_codepoint != 0xFFFFFFFFFFFFFFFF )
atlas_index = LRU_peek( & region.state, next_evict_codepoint )
atlas_index = LRU_peek( & region.state, next_evict_codepoint, must_find = true )
assert( atlas_index != -1 )
evicted := LRU_put( & region.state, lru_code, atlas_index )
@ -763,16 +764,17 @@ shape_text_cached :: proc( ctx : ^Context, font : FontID, text_utf8 : string ) -
if shape_cache_idx == -1
{
if shape_cache.next_cache_id < i32(state.capacity) {
shape_cache_idx = shape_cache.next_cache_id
LRU_put( state, hash, shape_cache_idx )
shape_cache_idx = shape_cache.next_cache_id
shape_cache.next_cache_id += 1
evicted := LRU_put( state, hash, shape_cache_idx )
assert( evicted == hash )
}
else
{
next_evict_idx := LRU_get_next_evicted( state )
assert( next_evict_idx != 0xFFFFFFFFFFFFFFFF )
shape_cache_idx = LRU_peek( state, next_evict_idx )
shape_cache_idx = LRU_peek( state, next_evict_idx, must_find = true )
assert( shape_cache_idx != - 1 )
LRU_put( state, hash, shape_cache_idx )

View File

@ -8,6 +8,8 @@ import "core:mem"
Kilobyte :: mem.Kilobyte
slice_ptr :: mem.slice_ptr
Arena :: mem.Arena
arena_allocator :: mem.arena_allocator
@ -46,6 +48,7 @@ hmap_chained_destroy :: grime.hmap_chained_destroy
hmap_chained_init :: grime.hmap_chained_init
hmap_chained_get :: grime.hmap_chained_get
hmap_chained_remove :: grime.hmap_chained_remove
hmap_chained_reload :: grime.hmap_chained_reload
hmap_chained_set :: grime.hmap_chained_set
hmap_closest_prime :: grime.hmap_closest_prime

View File

@ -13,8 +13,8 @@ and direct pointers are kept across the codebase instead of a key to the slot.
*/
package grime
import "core:mem"
import "base:runtime"
import "core:mem"
HTable_Minimum_Capacity :: 4 * Kilobyte
@ -26,6 +26,7 @@ HMapChainedSlot :: struct( $Type : typeid ) {
}
HMapChainedHeader :: struct( $ Type : typeid ) {
tracker : MemoryTracker,
pool : Pool,
lookup : [] ^HMapChainedSlot(Type),
dbg_name : string,
@ -62,7 +63,7 @@ hmap_chained_init :: proc( $HMapChainedType : typeid/HMapChained($Type), lookup_
) -> (table : HMapChained(Type), error : AllocatorError)
{
header_size := size_of(HMapChainedHeader(Type))
size := header_size + int(lookup_capacity) * size_of( ^HMapChainedSlot(Type)) + size_of(int)
size := header_size + int(lookup_capacity) * size_of( ^HMapChainedSlot(Type))
raw_mem : rawptr
raw_mem, error = alloc( size, allocator = allocator )
@ -79,9 +80,14 @@ hmap_chained_init :: proc( $HMapChainedType : typeid/HMapChained($Type), lookup_
dbg_name = str_intern(str_fmt("%v: pool", dbg_name)).str,
enable_mem_tracking = enable_mem_tracking,
)
data := transmute([^] ^HMapChainedSlot(Type)) (transmute( [^]HMapChainedHeader(Type)) table.header)[1:]
data := transmute(^^HMapChainedSlot(Type)) memory_after_header(table.header)
table.lookup = slice_ptr( data, int(lookup_capacity) )
table.dbg_name = dbg_name
if Track_Memory && enable_mem_tracking {
memtracker_init( & table.tracker, allocator, Kilobyte * 16, dbg_name )
}
return
}
@ -115,9 +121,6 @@ hmap_chained_get :: proc( using self : HMapChained($Type), key : u64) -> ^Type
{
// profile(#procedure)
hash_index := hmap_chained_lookup_id(self, key)
// if hash_index == 565 {
// runtime.debug_trap()
// }
surface_slot := lookup[hash_index]
if surface_slot == nil {
@ -128,9 +131,13 @@ hmap_chained_get :: proc( using self : HMapChained($Type), key : u64) -> ^Type
return & surface_slot.value
}
for slot := surface_slot.next; slot != nil; slot = slot.next {
for slot := surface_slot.next; slot != nil; slot = slot.next
{
if slot.occupied && slot.key == key {
return & surface_slot.value
if self.dbg_name != "" && self.tracker.entries.header != nil {
logf( "%v: Retrieved %v in lookup[%v] which shows key as %v", self.dbg_name, key, hash_index, slot.key )
}
return & slot.value
}
}
@ -139,14 +146,15 @@ hmap_chained_get :: proc( using self : HMapChained($Type), key : u64) -> ^Type
hmap_chained_reload :: proc( self : HMapChained($Type), allocator : Allocator )
{
pool_reload(self.pool, allocator)
// pool_reload(self.pool, allocator)
}
// Returns true if an slot was actually found and marked as vacant
// Entries already found to be vacant will not return true
hmap_chained_remove :: proc( self : HMapChained($Type), key : u64 ) -> b32
{
surface_slot := self.lookup[hmap_chained_lookup_id(self, key)]
hash_index := hmap_chained_lookup_id(self, key)
surface_slot := self.lookup[hash_index]
if surface_slot == nil {
return false
@ -154,13 +162,24 @@ hmap_chained_remove :: proc( self : HMapChained($Type), key : u64 ) -> b32
if surface_slot.occupied && surface_slot.key == key {
surface_slot.occupied = false
surface_slot.value = {}
surface_slot.key = {}
if self.dbg_name != "" && self.tracker.entries.header != nil {
logf( "%v: Removed %v in lookup[%v]", self.dbg_name, key, hash_index )
}
return true
}
for slot := surface_slot.next; slot != nil; slot = slot.next
nest_id : i32 = 1
for slot := surface_slot.next; slot != nil; slot = slot.next
{
if slot.occupied && slot.key == key {
slot.occupied = false
slot.value = {}
slot.key = {}
if self.dbg_name != "" && self.tracker.entries.header != nil {
logf( "%v: Removed %v in lookup[%v] nest_id: %v", self.dbg_name, key, hash_index, nest_id )
}
return true
}
}
@ -176,62 +195,69 @@ hmap_chained_set :: proc( self : HMapChained($Type), key : u64, value : Type ) -
using self
hash_index := hmap_chained_lookup_id(self, key)
surface_slot := lookup[hash_index]
set_slot :: #force_inline proc( using self : HMapChained(Type),
slot : ^HMapChainedSlot(Type),
key : u64,
value : Type
) -> (^ Type, AllocatorError )
{
error := AllocatorError.None
if slot.next == nil {
block : []byte
block, error = pool_grab(pool, false)
next := transmute( ^HMapChainedSlot(Type)) & block[0]
next^ = {}
slot.next = next
next.prev = slot
}
slot.key = key
slot.value = value
slot.occupied = true
return & slot.value, error
}
if surface_slot == nil {
block, error := pool_grab(pool, false)
surface_slot := transmute( ^HMapChainedSlot(Type)) & block[0]
slot_size := size_of(HMapChainedSlot(Type))
if surface_slot == nil
{
block, error := pool_grab(pool, false)
surface_slot := transmute( ^HMapChainedSlot(Type)) raw_data(block)
surface_slot^ = {}
surface_slot.key = key
surface_slot.value = value
surface_slot.occupied = true
if error != AllocatorError.None {
ensure(error != AllocatorError.None, "Allocation failure for chained slot in hash table")
return nil, error
}
lookup[hash_index] = surface_slot
block, error = pool_grab(pool, false)
next := transmute( ^HMapChainedSlot(Type)) & block[0]
next^ = {}
surface_slot.next = next
next.prev = surface_slot
if Track_Memory && tracker.entries.header != nil {
memtracker_register_auto_name_slice( & self.tracker, block)
}
return & surface_slot.value, error
}
if ! surface_slot.occupied
{
result, error := set_slot( self, surface_slot, key, value)
return result, error
surface_slot.key = key
surface_slot.value = value
surface_slot.occupied = true
if dbg_name != "" && tracker.entries.header != nil {
logf( "%v: Set %v in lookup[%v]", self.dbg_name, key, hash_index )
}
return & surface_slot.value, .None
}
slot : ^HMapChainedSlot(Type) = surface_slot.next
for ; slot != nil; slot = slot.next
slot : ^HMapChainedSlot(Type) = surface_slot
nest_id : i32 = 1
for ;; slot = slot.next
{
if !slot.occupied
error : AllocatorError
if slot.next == nil
{
result, error := set_slot( self, slot, key, value)
return result, error
block : []byte
block, error = pool_grab(pool, false)
next := transmute( ^HMapChainedSlot(Type)) raw_data(block)
slot.next = next
slot.next^ = {}
slot.next.prev = slot
if Track_Memory && tracker.entries.header != nil {
memtracker_register_auto_name_slice( & self.tracker, block)
}
}
if ! slot.next.occupied
{
slot.next.key = key
slot.next.value = value
slot.next.occupied = true
if dbg_name != "" && tracker.entries.header != nil {
logf( "%v: Set %v in lookup[%v] nest_id: %v", self.dbg_name, key, hash_index, nest_id )
}
return & slot.next.value, .None
}
nest_id += 1
}
ensure(false, "Somehow got to a null slot that wasn't preemptively allocated from a previus set")
return nil, AllocatorError.None

View File

@ -276,7 +276,7 @@ varena_allocator_proc :: proc(
data = new_region
// log( str_fmt_tmp("varena resize (new): old: %p %v new: %p %v", old_memory, old_size, (& data[0]), size))
when Track_Memory {
if Track_Memory && arena.tracker.entries.header != nil {
memtracker_register_auto_name( & arena.tracker, & data[0], & data[len(data) - 1] )
}
return

View File

@ -128,9 +128,9 @@ setup_memory :: proc( profiler : ^SpallProfiler ) -> ClientMemory
sectr.Memory_Reserve_Persistent,
sectr.Memory_Commit_Initial_Persistent,
growth_policy = nil,
allow_any_resize = false,
allow_any_resize = true,
dbg_name = "persistent",
enable_mem_tracking = true )
enable_mem_tracking = false )
verify( alloc_error == .None, "Failed to allocate persistent virtual arena for the sectr module")
frame, alloc_error = varena_init(

View File

@ -92,7 +92,7 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem
// push( policy_ptr, SlabSizeClass { 512 * Megabyte, 512 * Megabyte, alignment })
alloc_error : AllocatorError
persistent_slab, alloc_error = slab_init( policy_ptr, allocator = persistent_allocator(), dbg_name = Persistent_Slab_DBG_Name, enable_mem_tracking = true )
persistent_slab, alloc_error = slab_init( policy_ptr, allocator = persistent_allocator(), dbg_name = Persistent_Slab_DBG_Name, enable_mem_tracking = false )
verify( alloc_error == .None, "Failed to allocate the persistent slab" )
transient_slab, alloc_error = slab_init( & default_slab_policy, allocator = transient_allocator(), dbg_name = Transient_Slab_DBG_Name )
@ -254,23 +254,23 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem
if true
{
font_provider_startup()
// path_rec_mono_semicasual_reg := strings.concatenate( { Path_Assets, "RecMonoSemicasual-Regular-1.084.ttf" })
// font_rec_mono_semicasual_reg = font_load( path_rec_mono_semicasual_reg, 24.0, "RecMonoSemiCasual_Regular" )
path_rec_mono_semicasual_reg := strings.concatenate( { Path_Assets, "RecMonoSemicasual-Regular-1.084.ttf" })
font_rec_mono_semicasual_reg = font_load( path_rec_mono_semicasual_reg, 24.0, "RecMonoSemiCasual_Regular" )
// path_squidgy_slimes := strings.concatenate( { Path_Assets, "Squidgy Slimes.ttf" } )
// font_squidgy_slimes = font_load( path_squidgy_slimes, 24.0, "Squidgy_Slime" )
path_squidgy_slimes := strings.concatenate( { Path_Assets, "Squidgy Slimes.ttf" } )
font_squidgy_slimes = font_load( path_squidgy_slimes, 24.0, "Squidgy_Slime" )
path_firacode := strings.concatenate( { Path_Assets, "FiraCode-Regular.ttf" } )
font_firacode = font_load( path_firacode, 24.0, "FiraCode" )
// path_open_sans := strings.concatenate( { Path_Assets, "OpenSans-Regular.ttf" } )
// font_open_sans = font_load( path_open_sans, 24.0, "OpenSans" )
path_open_sans := strings.concatenate( { Path_Assets, "OpenSans-Regular.ttf" } )
font_open_sans = font_load( path_open_sans, 24.0, "OpenSans" )
// path_noto_sans := strings.concatenate( { Path_Assets, "NotoSans-Regular.ttf" } )
// font_noto_sans = font_load( path_noto_sans, 24.0, "NotoSans" )
path_noto_sans := strings.concatenate( { Path_Assets, "NotoSans-Regular.ttf" } )
font_noto_sans = font_load( path_noto_sans, 24.0, "NotoSans" )
// path_arial_unicode_ms := strings.concatenate( { Path_Assets, "Arial Unicode MS.ttf" } )
// font_arial_unicode_ms = font_load( path_arial_unicode_ms, 24.0, "Arial_Unicode_MS" )
path_arial_unicode_ms := strings.concatenate( { Path_Assets, "Arial Unicode MS.ttf" } )
font_arial_unicode_ms = font_load( path_arial_unicode_ms, 24.0, "Arial_Unicode_MS" )
default_font = font_firacode
log( "Default font loaded" )

View File

@ -125,7 +125,7 @@ render_mode_screenspace :: proc()
debug.debug_text_vis = true
if debug.debug_text_vis
{
fps_msg := str_fmt( "FPS: %0.2f", fps_avg)
fps_msg := str_fmt( "FPS: %d", frame)
fps_msg_width := measure_text_size( fps_msg, default_font, 12.0, 0.0 ).x
fps_msg_pos := screen_get_corners().top_right - { fps_msg_width, 0 } - { 5, 5 }
debug_draw_text( fps_msg, fps_msg_pos, 38.0, color = Color_Red )
@ -133,7 +133,7 @@ render_mode_screenspace :: proc()
// debug_text( "Screen Width : %v", rl.GetScreenWidth () )
// debug_text( "Screen Height: %v", rl.GetScreenHeight() )
// debug_text( "frametime_target_ms : %f ms", frametime_target_ms )
debug_text( "frametime : %f ms", frametime_delta_ms )
debug_text( "frametime : %d ms", frame )
// debug_text( "frametime_last_elapsed_ms : %f ms", frametime_elapsed_ms )
if replay.mode == ReplayMode.Record {
debug_text( "Recording Input")

View File

@ -70,10 +70,10 @@ font_provider_startup :: proc()
provider_data := & state.font_provider_data; using provider_data
error : AllocatorError
font_cache, error = make( HMapChained(FontDef), hmap_closest_prime(1 * Kilo), persistent_allocator() /*dbg_name = "font_cache"*/ )
font_cache, error = make( HMapChained(FontDef), hmap_closest_prime(1 * Kilo), persistent_allocator(), dbg_name = "font_cache" )
verify( error == AllocatorError.None, "Failed to allocate font_cache" )
ve.init( & provider_data.ve_font_cache, .STB_TrueType, allocator = persistent_slab_allocator() )
ve.init( & provider_data.ve_font_cache, .STB_TrueType, allocator = persistent_allocator() )
log("VEFontCached initialized")
ve.configure_snap( & provider_data.ve_font_cache, u32(state.app_window.extent.x * 2.0), u32(state.app_window.extent.y * 2.0) )
@ -552,7 +552,7 @@ font_load :: proc(path_file : string,
for font_size : i32 = Font_Size_Interval; font_size <= Font_Largest_Px_Size; font_size += Font_Size_Interval
{
logf("Loading at size %v", font_size)
// logf("Loading at size %v", font_size)
id := (font_size / Font_Size_Interval) + (font_size % Font_Size_Interval)
ve_id := & def.size_table[id - 1]
ve_id^ = ve.load_font( & provider_data.ve_font_cache, desired_id, font_data, 14.0 )