Compare commits
8 Commits
1af8b5cdfb
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2acbf51637 | |||
| 61d94265dd | |||
| 962a814b84 | |||
| 08f8a9d0b5 | |||
| f2da356580 | |||
| 6ac41b22f8 | |||
| a69b1e8199 | |||
| 6a0a984310 |
+20
-11
@@ -765,11 +765,14 @@ compute_backdrop_group_work_region :: proc(
|
||||
max_x += halo_logical
|
||||
max_y += halo_logical
|
||||
|
||||
// Convert to physical pixels and clamp to swapchain bounds.
|
||||
phys_min_x := math.max(min_x * dpi, 0)
|
||||
phys_min_y := math.max(min_y * dpi, 0)
|
||||
phys_max_x := math.min(max_x * dpi, f32(swapchain_width))
|
||||
phys_max_y := math.min(max_y * dpi, f32(swapchain_height))
|
||||
// Clamp the min corner to 0, but let the max corner's 6σ halo extend past the swapchain edge
|
||||
// into the working texture's unused area (at factor > 1), capped to the texture extent. Keeps
|
||||
// the composite's bilinear upsample off the unwritten texels just past a clamped edge.
|
||||
downsample_factor := compute_backdrop_downsample_factor(sigma_logical)
|
||||
phys_min_x := max(min_x * dpi, 0)
|
||||
phys_min_y := max(min_y * dpi, 0)
|
||||
phys_max_x := min(max_x * dpi, f32(swapchain_width * downsample_factor))
|
||||
phys_max_y := min(max_y * dpi, f32(swapchain_height * downsample_factor))
|
||||
|
||||
if phys_max_x <= phys_min_x || phys_max_y <= phys_min_y do return 0, 0, 0, 0
|
||||
|
||||
@@ -868,12 +871,18 @@ run_backdrop_bracket :: proc(
|
||||
working_w := (region_w + downsample_factor - 1) / downsample_factor
|
||||
working_h := (region_h + downsample_factor - 1) / downsample_factor
|
||||
|
||||
// Working textures are sized at min factor (2). At factor=4 we have only half the texture
|
||||
// area available in each axis. Clamp to the texture extent for either case.
|
||||
wt_w := pipeline.cached_width / downsample_factor
|
||||
wt_h := pipeline.cached_height / downsample_factor
|
||||
if working_x + working_w > wt_w do working_w = wt_w - working_x
|
||||
if working_y + working_h > wt_h do working_h = wt_h - working_y
|
||||
// Clamp to the full texture extent (not cached/factor): the working textures are full
|
||||
// swapchain res, so a factor-N group's halo can spill into the unused remainder. Writing it
|
||||
// keeps the composite's bilinear upsample off unwritten texels at the right/bottom edge.
|
||||
texture_width := pipeline.cached_width
|
||||
texture_height := pipeline.cached_height
|
||||
// Skip fully off-screen groups; also guards the unsigned clamps below from underflow.
|
||||
if working_x >= texture_width || working_y >= texture_height {
|
||||
i = group_end
|
||||
continue
|
||||
}
|
||||
if working_x + working_w > texture_width do working_w = texture_width - working_x
|
||||
if working_y + working_h > texture_height do working_h = texture_height - working_y
|
||||
if working_w == 0 || working_h == 0 {
|
||||
i = group_end
|
||||
continue
|
||||
|
||||
@@ -730,21 +730,9 @@ flush_deferred_and_close_backdrop_scope :: proc(
|
||||
prepare_clay_batch :: proc(
|
||||
base_layer: ^Layer,
|
||||
batch: ^ClayBatch,
|
||||
mouse_wheel_delta: [2]f32,
|
||||
frame_time: f32 = 0,
|
||||
custom_draw: Custom_Draw = nil,
|
||||
temp_allocator := context.temp_allocator,
|
||||
) {
|
||||
mouse_pos: [2]f32
|
||||
mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y)
|
||||
|
||||
// Update clay internals
|
||||
clay.SetPointerState(
|
||||
clay.Vector2{mouse_pos.x - base_layer.bounds.x, mouse_pos.y - base_layer.bounds.y},
|
||||
.LEFT in mouse_flags,
|
||||
)
|
||||
clay.UpdateScrollContainers(true, mouse_wheel_delta, frame_time)
|
||||
|
||||
layer := base_layer
|
||||
command_count := int(batch.cmds.length)
|
||||
deferred_indices := make([dynamic]i32, 0, 16, temp_allocator)
|
||||
|
||||
@@ -314,7 +314,7 @@ clay_borders :: proc() {
|
||||
bounds = base_layer.bounds,
|
||||
cmds = clay.EndLayout(0),
|
||||
}
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0})
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch)
|
||||
draw.end(gpu, window)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ hellope_clay :: proc() {
|
||||
bounds = base_layer.bounds,
|
||||
cmds = clay.EndLayout(0),
|
||||
}
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0})
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch)
|
||||
draw.end(gpu, window)
|
||||
}
|
||||
}
|
||||
@@ -372,7 +372,7 @@ hellope_custom :: proc() {
|
||||
bounds = base_layer.bounds,
|
||||
cmds = clay.EndLayout(0),
|
||||
}
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0}, custom_draw = draw_custom)
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch, custom_draw = draw_custom)
|
||||
draw.end(gpu, window)
|
||||
}
|
||||
|
||||
|
||||
+119
-44
@@ -120,10 +120,52 @@ spinlock_try_lock :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool
|
||||
return lock_acquired
|
||||
}
|
||||
|
||||
// Spins until the lock is acquired, relaxing the CPU between attempts.
|
||||
spinlock_lock :: #force_inline proc "contextless" (lock: ^Spinlock) {
|
||||
for !spinlock_try_lock(lock) {
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
}
|
||||
|
||||
spinlock_unlock :: #force_inline proc "contextless" (lock: ^Spinlock) {
|
||||
intrinsics.atomic_store_explicit(lock, false, .Release)
|
||||
}
|
||||
|
||||
// Spins until the lock is acquired, then unlocks at the end of the calling scope. Always returns
|
||||
// true so it can guard a critical section from within an `if`:
|
||||
//
|
||||
// if spinlock_guard(&lock) {
|
||||
// // critical section
|
||||
// }
|
||||
@(deferred_in = spinlock_unlock)
|
||||
spinlock_guard :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool {
|
||||
spinlock_lock(lock)
|
||||
return true
|
||||
}
|
||||
|
||||
// Tries to acquire the lock once without spinning. Returns true and unlocks at the end of the
|
||||
// calling scope if acquired, otherwise returns false and does nothing:
|
||||
//
|
||||
// if spinlock_try_guard(&lock) {
|
||||
// // critical section, entered only if the lock was acquired
|
||||
// }
|
||||
@(deferred_in_out = spinlock_try_guard_unlock)
|
||||
spinlock_try_guard :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool {
|
||||
return spinlock_try_lock(lock)
|
||||
}
|
||||
|
||||
// Deferred companion of `spinlock_try_guard`; unlocks only when the lock was actually acquired.
|
||||
@(private)
|
||||
spinlock_try_guard_unlock :: #force_inline proc "contextless" (lock: ^Spinlock, locked: bool) {
|
||||
if locked {
|
||||
spinlock_unlock(lock)
|
||||
}
|
||||
}
|
||||
|
||||
lock :: proc {
|
||||
spinlock_lock,
|
||||
}
|
||||
|
||||
try_lock :: proc {
|
||||
spinlock_try_lock,
|
||||
}
|
||||
@@ -132,6 +174,14 @@ unlock :: proc {
|
||||
spinlock_unlock,
|
||||
}
|
||||
|
||||
guard :: proc {
|
||||
spinlock_guard,
|
||||
}
|
||||
|
||||
try_guard :: proc {
|
||||
spinlock_try_guard,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -139,10 +189,10 @@ import "core:sync"
|
||||
import "core:testing"
|
||||
import "core:thread"
|
||||
|
||||
// Multiple threads will each add 1.0 this many times.
|
||||
// If any updates are lost due to race conditions, the final sum will be wrong.
|
||||
@(test)
|
||||
test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
|
||||
// Multiple threads will each add 1.0 this many times.
|
||||
// If any updates are lost due to race conditions, the final sum will be wrong.
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 10_000
|
||||
|
||||
@@ -184,10 +234,10 @@ test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, shared_value, expected)
|
||||
}
|
||||
|
||||
// Start with a known value, multiple threads subtract.
|
||||
// If any updates are lost due to race conditions, the final result will be wrong.
|
||||
@(test)
|
||||
test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
|
||||
// Start with a known value, multiple threads subtract.
|
||||
// If any updates are lost due to race conditions, the final result will be wrong.
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 10_000
|
||||
|
||||
@@ -228,11 +278,11 @@ test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, shared_value, 0.0)
|
||||
}
|
||||
|
||||
// Each thread multiplies by 2.0 then divides by 2.0.
|
||||
// Since these are inverses, the final value should equal the starting value
|
||||
// regardless of how operations interleave.
|
||||
@(test)
|
||||
test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
|
||||
// Each thread multiplies by 2.0 then divides by 2.0.
|
||||
// Since these are inverses, the final value should equal the starting value
|
||||
// regardless of how operations interleave.
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 10_000
|
||||
|
||||
@@ -274,10 +324,10 @@ test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, shared_value, 1000.0)
|
||||
}
|
||||
|
||||
// Verify the f32 type dispatch works correctly under contention.
|
||||
// Same approach as the f64 add test but with f32.
|
||||
@(test)
|
||||
test_atomic_add_with_f32 :: proc(t: ^testing.T) {
|
||||
// Verify the f32 type dispatch works correctly under contention.
|
||||
// Same approach as the f64 add test but with f32.
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 10_000
|
||||
|
||||
@@ -319,17 +369,17 @@ test_atomic_add_with_f32 :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, shared_value, expected)
|
||||
}
|
||||
|
||||
// Tests that the memory order passed to atomic_float_op's CAS success condition
|
||||
// provides full ordering guarantees for the entire float operation.
|
||||
//
|
||||
// Both sides use atomic_add_float (not raw intrinsics) to verify:
|
||||
// - Release on CAS success publishes prior non-atomic writes
|
||||
// - Acquire on CAS success makes those writes visible to the reader
|
||||
//
|
||||
// NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model.
|
||||
// On ARM or other weak-memory architectures, using Relaxed here would likely cause failures.
|
||||
@(test)
|
||||
test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
||||
// Tests that the memory order passed to atomic_float_op's CAS success condition
|
||||
// provides full ordering guarantees for the entire float operation.
|
||||
//
|
||||
// Both sides use atomic_add_float (not raw intrinsics) to verify:
|
||||
// - Release on CAS success publishes prior non-atomic writes
|
||||
// - Acquire on CAS success makes those writes visible to the reader
|
||||
//
|
||||
// NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model.
|
||||
// On ARM or other weak-memory architectures, using Relaxed here would likely cause failures.
|
||||
NUM_READERS :: 4
|
||||
|
||||
Shared_State :: struct {
|
||||
@@ -426,17 +476,20 @@ test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Stress test for every spinlock acquisition variant: N threads contend on a
|
||||
// single lock and perform a deliberate non-atomic read-modify-write on shared
|
||||
// data. Each iteration rotates through spinlock_try_lock, spinlock_lock,
|
||||
// spinlock_guard, and spinlock_try_guard so every variant runs concurrently and
|
||||
// must uphold mutual exclusion on the same lock.
|
||||
//
|
||||
// If mutual exclusion holds:
|
||||
// - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD
|
||||
// - `concurrent_holders` never exceeds 1
|
||||
//
|
||||
// A multi-step RMW (read → relax → write) widens the critical section so
|
||||
// any failure to exclude is virtually guaranteed to corrupt the counter.
|
||||
@(test)
|
||||
test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
// Stress test for spinlock_try_lock: N threads spin-acquire the lock and
|
||||
// perform a deliberate non-atomic read-modify-write on shared data.
|
||||
//
|
||||
// If mutual exclusion holds:
|
||||
// - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD
|
||||
// - `concurrent_holders` never exceeds 1
|
||||
//
|
||||
// A multi-step RMW (read → relax → write) widens the critical section so
|
||||
// any failure to exclude is virtually guaranteed to corrupt the counter.
|
||||
test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
NUM_THREADS :: 8
|
||||
ITERATIONS_PER_THREAD :: 50_000
|
||||
|
||||
@@ -461,21 +514,9 @@ test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
barrier: sync.Barrier
|
||||
sync.barrier_init(&barrier, NUM_THREADS)
|
||||
|
||||
thread_proc :: proc(th: ^thread.Thread) {
|
||||
ctx := cast(^Thread_Data)th.data
|
||||
s := ctx.shared
|
||||
|
||||
// All threads rendezvous here for maximum contention.
|
||||
sync.barrier_wait(ctx.barrier)
|
||||
|
||||
for _ in 0 ..< ITERATIONS_PER_THREAD {
|
||||
// Spin on try_lock until we acquire it.
|
||||
for !spinlock_try_lock(&s.lock) {
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
|
||||
// --- critical section start ---
|
||||
|
||||
// The single critical section every acquisition variant must protect. Sharing
|
||||
// it guarantees they all stress the exact same non-atomic read-modify-write.
|
||||
critical_section :: proc(s: ^Shared) {
|
||||
// Atomically bump the holder count so we can detect overlapping holders.
|
||||
holders := intrinsics.atomic_add_explicit(&s.concurrent_holders, 1, .Relaxed)
|
||||
|
||||
@@ -494,10 +535,44 @@ test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
|
||||
s.counter = val + 1
|
||||
|
||||
intrinsics.atomic_sub_explicit(&s.concurrent_holders, 1, .Relaxed)
|
||||
}
|
||||
|
||||
// --- critical section end ---
|
||||
thread_proc :: proc(th: ^thread.Thread) {
|
||||
ctx := cast(^Thread_Data)th.data
|
||||
s := ctx.shared
|
||||
|
||||
// All threads rendezvous here for maximum contention.
|
||||
sync.barrier_wait(ctx.barrier)
|
||||
|
||||
for i in 0 ..< ITERATIONS_PER_THREAD {
|
||||
// Rotate through every acquisition variant so they all contend on the
|
||||
// same lock simultaneously and must each uphold mutual exclusion.
|
||||
switch i & 3 {
|
||||
case 0:
|
||||
// Manual spin on try_lock until we acquire it.
|
||||
for !spinlock_try_lock(&s.lock) {
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
critical_section(s)
|
||||
spinlock_unlock(&s.lock)
|
||||
case 1:
|
||||
// Blocking lock that loops internally until acquired.
|
||||
spinlock_lock(&s.lock)
|
||||
critical_section(s)
|
||||
spinlock_unlock(&s.lock)
|
||||
case 2: // Scoped guard: unlocks automatically at the end of the block.
|
||||
if spinlock_guard(&s.lock) {
|
||||
critical_section(s)
|
||||
}
|
||||
case 3: // Scoped try-guard: retry until acquired, auto-unlocks on success.
|
||||
for {
|
||||
if spinlock_try_guard(&s.lock) {
|
||||
critical_section(s)
|
||||
break
|
||||
}
|
||||
intrinsics.cpu_relax()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package quantity
|
||||
|
||||
import "base:intrinsics"
|
||||
|
||||
LITERS_PER_GALLON :: 3.785411784
|
||||
|
||||
//----- Liters ----------------------------------
|
||||
Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
@@ -14,6 +16,13 @@ liters_to_milli_liters :: #force_inline proc "contextless" (
|
||||
return Milli_Liters(V){liters.v * MILLI}
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
liters_to_gallons :: #force_inline proc "contextless" (
|
||||
liters: Liters($V),
|
||||
) -> Gallons(V) where intrinsics.type_is_float(V) {
|
||||
return Gallons(V){liters.v / LITERS_PER_GALLON}
|
||||
}
|
||||
|
||||
//----- Milliliters ----------------------------------
|
||||
Milli_Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
@@ -26,17 +35,34 @@ milli_liters_to_liters :: #force_inline proc "contextless" (
|
||||
return Liters(V){milli_liters.v / MILLI}
|
||||
}
|
||||
|
||||
//----- Gallons ----------------------------------
|
||||
Gallons :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
gallons_to_liters :: #force_inline proc "contextless" (
|
||||
gallons: Gallons($V),
|
||||
) -> Liters(V) where intrinsics.type_is_float(V) {
|
||||
return Liters(V){gallons.v * LITERS_PER_GALLON}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Conversion Overloads ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
to_liters :: proc {
|
||||
milli_liters_to_liters,
|
||||
gallons_to_liters,
|
||||
}
|
||||
|
||||
to_milli_liters :: proc {
|
||||
liters_to_milli_liters,
|
||||
}
|
||||
|
||||
to_gallons :: proc {
|
||||
liters_to_gallons,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -57,3 +83,19 @@ test_milli_liters_to_liters :: proc(t: ^testing.T) {
|
||||
|
||||
testing.expect_value(t, liters, Liters(int){12})
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_gallons_to_liters :: proc(t: ^testing.T) {
|
||||
gallons := Gallons(f32){1}
|
||||
liters := to_liters(gallons)
|
||||
|
||||
testing.expect(t, liters.v > 3.78 && liters.v < 3.79)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_liters_to_gallons :: proc(t: ^testing.T) {
|
||||
liters := Liters(f32){3.785411784}
|
||||
gallons := to_gallons(liters)
|
||||
|
||||
testing.expect(t, gallons.v > 0.99 && gallons.v < 1.01)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,58 @@ package quantity
|
||||
|
||||
import "base:intrinsics"
|
||||
|
||||
//----- Liters Per Minute ----------------------------------
|
||||
Liters_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
liters_per_minute_to_gallons_per_minute :: #force_inline proc "contextless" (
|
||||
liters_per_minute: Liters_Per_Minute($V),
|
||||
) -> Gallons_Per_Minute(V) where intrinsics.type_is_float(V) {
|
||||
return Gallons_Per_Minute(V){liters_per_minute.v / LITERS_PER_GALLON}
|
||||
}
|
||||
|
||||
//----- Gallons Per Minute ----------------------------------
|
||||
Gallons_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
|
||||
v: V,
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
gallons_per_minute_to_liters_per_minute :: #force_inline proc "contextless" (
|
||||
gallons_per_minute: Gallons_Per_Minute($V),
|
||||
) -> Liters_Per_Minute(V) where intrinsics.type_is_float(V) {
|
||||
return Liters_Per_Minute(V){gallons_per_minute.v * LITERS_PER_GALLON}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Conversion Overloads ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
to_liters_per_minute :: proc {
|
||||
gallons_per_minute_to_liters_per_minute,
|
||||
}
|
||||
|
||||
to_gallons_per_minute :: proc {
|
||||
liters_per_minute_to_gallons_per_minute,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_gallons_per_minute_to_liters_per_minute :: proc(t: ^testing.T) {
|
||||
gallons_per_minute := Gallons_Per_Minute(f32){1}
|
||||
liters_per_minute := to_liters_per_minute(gallons_per_minute)
|
||||
|
||||
testing.expect(t, liters_per_minute.v > 3.78 && liters_per_minute.v < 3.79)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_liters_per_minute_to_gallons_per_minute :: proc(t: ^testing.T) {
|
||||
liters_per_minute := Liters_Per_Minute(f32){3.785411784}
|
||||
gallons_per_minute := to_gallons_per_minute(liters_per_minute)
|
||||
|
||||
testing.expect(t, gallons_per_minute.v > 0.99 && gallons_per_minute.v < 1.01)
|
||||
}
|
||||
|
||||
Vendored
+4
-4
@@ -68,9 +68,9 @@ main :: proc() {
|
||||
db_handle: mdb.Dbi
|
||||
// Put transaction
|
||||
key := 7
|
||||
key_val := mdb.blittable_val(&key)
|
||||
key_val := mdb.pod_val(&key)
|
||||
put_data := 12
|
||||
put_data_val := mdb.blittable_val(&put_data)
|
||||
put_data_val := mdb.pod_val(&put_data)
|
||||
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
|
||||
mdb.panic_on_err(mdb.dbi_open(txn_handle, nil, {}, &db_handle))
|
||||
mdb.panic_on_err(mdb.put(txn_handle, db_handle, &key_val, &put_data_val, {}))
|
||||
@@ -80,7 +80,7 @@ main :: proc() {
|
||||
data_val: mdb.Val
|
||||
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
|
||||
mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val, &data_val))
|
||||
data_cpy := mdb.blittable_copy(&data_val, int)
|
||||
mdb.panic_on_err(mdb.txn_commit(txn_handle))
|
||||
data_cpy := mdb.pod_copy(data_val, int)
|
||||
mdb.txn_abort(txn_handle)
|
||||
fmt.println("Get result:", data_cpy)
|
||||
}
|
||||
|
||||
Vendored
+52
-15
@@ -169,58 +169,86 @@ import "core:fmt"
|
||||
import "core:reflect"
|
||||
import "core:sys/posix"
|
||||
|
||||
import b "../../basic"
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Added Odin Helpers ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Wrap a blittable value's bytes as an LMDB Val.
|
||||
// Wrap a POD value's bytes as an LMDB Val.
|
||||
// T must be a contiguous type with no indirection (no pointers, slices, strings, maps, etc.).
|
||||
blittable_val :: #force_inline proc(val_ptr: ^$T) -> Val {
|
||||
pod_val :: #force_inline proc(val_ptr: ^$T) -> Val {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"blitval: type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||
"pod_val: type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
}
|
||||
return Val{size_of(T), val_ptr}
|
||||
}
|
||||
|
||||
// Reads a blittable T out of the LMDB memory map by copying it into caller
|
||||
// Reads a POD T out of the LMDB memory map by copying it into caller
|
||||
// storage. The returned T has no lifetime tie to the transaction.
|
||||
blittable_copy :: #force_inline proc(val: ^Val, $T: typeid) -> T {
|
||||
pod_copy :: #force_inline proc(val: Val, $T: typeid) -> T {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"blitval_copy: type '%v' contains indirection and cannot be read directly from LMDB",
|
||||
"pod_copy: type '%v' contains indirection and cannot be read directly from LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
}
|
||||
when b.ODIN_BOUNDS_CHECK {
|
||||
fmt.assertf(
|
||||
val.size == size_of(T),
|
||||
"size_of(%v) (%v) != val.size (%v)",
|
||||
typeid_of(T),
|
||||
size_of(T),
|
||||
val.size,
|
||||
)
|
||||
}
|
||||
return (cast(^T)val.data)^
|
||||
}
|
||||
|
||||
// Zero-copy pointer view into the LMDB memory map as a ^T.
|
||||
// Useful for large blittable types where you want to read individual fields
|
||||
// Useful for large POD types where you want to read individual fields
|
||||
// without copying the entire value (e.g. ptr.timestamp, ptr.flags).
|
||||
// MUST NOT be written through — writes either segfault (default env mode)
|
||||
// or silently corrupt the database (ENV_WRITEMAP).
|
||||
// MUST NOT be retained past txn_commit, txn_abort, or any subsequent write
|
||||
// operation on the same env — the pointer is invalidated.
|
||||
blittable_view :: #force_inline proc(val: ^Val, $T: typeid) -> ^T {
|
||||
pod_view :: #force_inline proc(val: Val, $T: typeid) -> ^T {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"blitval_view: type '%v' contains indirection and cannot be viewed directly from LMDB",
|
||||
"pod_view: type '%v' contains indirection and cannot be viewed directly from LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
}
|
||||
when b.ODIN_BOUNDS_CHECK {
|
||||
fmt.assertf(
|
||||
val.size == size_of(T),
|
||||
"size_of(%v) (%v) != val.size (%v)",
|
||||
typeid_of(T),
|
||||
size_of(T),
|
||||
val.size,
|
||||
)
|
||||
}
|
||||
return cast(^T)val.data
|
||||
}
|
||||
|
||||
// Wrap a slice of blittable elements as an LMDB Val for use with put/get.
|
||||
// Wrap a slice of POD elements as an LMDB Val for use with put/get.
|
||||
// T must be a contiguous type with no indirection.
|
||||
// The caller's slice must remain valid (not freed, not resized) for the
|
||||
// duration of the put call that consumes this Val.
|
||||
slice_val :: #force_inline proc(s: []$T) -> Val {
|
||||
pod_slice_val :: #force_inline proc(s: []$T) -> Val {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"slice_val: element type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||
"pod_slice_val: element type '%v' contains indirection and cannot be stored directly in LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
}
|
||||
return Val{uint(len(s) * size_of(T)), raw_data(s)}
|
||||
}
|
||||
|
||||
@@ -231,12 +259,21 @@ slice_val :: #force_inline proc(s: []$T) -> Val {
|
||||
// MUST be copied (e.g. slice.clone) if it needs to outlive the current
|
||||
// transaction; the view is invalidated by txn_commit, txn_abort, or any
|
||||
// subsequent write operation on the same env.
|
||||
slice_view :: #force_inline proc(val: ^Val, $T: typeid) -> []T {
|
||||
pod_slice_view :: #force_inline proc(val: Val, $T: typeid) -> []T {
|
||||
when ODIN_DEBUG {
|
||||
fmt.assertf(
|
||||
reflect.has_no_indirections(type_info_of(T)),
|
||||
"slice_view: element type '%v' contains indirection and cannot be read directly from LMDB",
|
||||
"pod_slice_view: element type '%v' contains indirection and cannot be read directly from LMDB",
|
||||
typeid_of(T),
|
||||
)
|
||||
fmt.assertf(
|
||||
val.size % size_of(T) == 0,
|
||||
"pod_slice_view: val.size (%v) is not a multiple of size_of(%v) (%v)",
|
||||
val.size,
|
||||
typeid_of(T),
|
||||
size_of(T),
|
||||
)
|
||||
}
|
||||
return (cast([^]T)val.data)[:val.size / size_of(T)]
|
||||
}
|
||||
|
||||
@@ -253,7 +290,7 @@ string_val :: #force_inline proc(s: string) -> Val {
|
||||
// MUST be copied (e.g. strings.clone) if it needs to outlive the current
|
||||
// transaction; the view is invalidated by txn_commit, txn_abort, or any
|
||||
// subsequent write operation on the same env.
|
||||
string_view :: #force_inline proc(val: ^Val) -> string {
|
||||
string_view :: #force_inline proc(val: Val) -> string {
|
||||
return string((cast([^]u8)val.data)[:val.size])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user