1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package gpu_tests
import "../renderer"
import "core:math"
import glsl "core:math/linalg/glsl"
import "core:testing"
@(test)
test_shadow_ubo_size :: proc(t: ^testing.T) {
// Shadow UBO: 256 light_matrices + 16 shadow_count + 16 shadow_params
// + 16 shadow_light_indices + 64 atlas_regions = 368 bytes data,
// padded by Odin's struct alignment (mat4x4 array -> 64-byte align)
ubo_size := size_of(renderer.Shadow_UBO_Data)
// Must be >= 368 bytes (may be larger due to alignment)
testing.expect(t, ubo_size >= 368, "shadow UBO must be at least 368 bytes")
}
@(test)
test_shadow_push_constants_size :: proc(t: ^testing.T) {
// Shadow push constants: 2x mat4 = 128 bytes
testing.expect_value(t, size_of(renderer.Push_Constants_Shadow), 128)
}
@(test)
test_light_matrix_computation :: proc(t: ^testing.T) {
// Test that compute_light_matrix produces valid matrices
light_dir := glsl.vec3{0, -1, 0} // sun straight down
camera_target := glsl.vec3{0, 0, 0}
half_extent: f32 = 10.0
depth: f32 = 20.0
mat := renderer.compute_light_matrix(light_dir, camera_target, half_extent, depth)
// Result should be non-zero (valid matrix)
all_zero := true
for r in 0 ..< 4 {
for c in 0 ..< 4 {
if mat[r, c] != 0 {
all_zero = false
}
}
}
testing.expect(t, !all_zero, "light matrix should not be all zeros")
// The matrix should transform the camera target to somewhere near the center
target_h := glsl.vec4{0, 0, 0, 1}
result := mat * target_h
// After projection, the result should be finite
testing.expect(t, !math.is_nan(result.x), "projected x should be finite")
testing.expect(t, !math.is_nan(result.y), "projected y should be finite")
testing.expect(t, !math.is_nan(result.z), "projected z should be finite")
}
@(test)
test_light_matrix_directional :: proc(t: ^testing.T) {
// A directional light pointing down -Y should create a top-down view
light_dir := glsl.vec3{0, -1, 0}
camera_target := glsl.vec3{5, 0, 5}
half_extent: f32 = 15.0
depth: f32 = 40.0
mat := renderer.compute_light_matrix(light_dir, camera_target, half_extent, depth)
// Camera target should map near the center in clip space (near 0,0)
target_h := glsl.vec4{5, 0, 5, 1}
result := mat * target_h
ndc := glsl.vec3{result.x / result.w, result.y / result.w, result.z / result.w}
testing.expect(t, math.abs(ndc.x) < 0.5, "target should be near center X in clip space")
testing.expect(t, math.abs(ndc.y) < 0.5, "target should be near center Y in clip space")
}
@(test)
test_shadow_caster_max :: proc(t: ^testing.T) {
// Ensure max shadow casters is reasonable
testing.expect_value(t, renderer.MAX_SHADOW_CASTERS, 4)
}
@(test)
test_light_data_casts_shadow :: proc(t: ^testing.T) {
// Verify Light_Data has casts_shadow field
ld := renderer.Light_Data {
type = 0,
enabled = true,
position = {0, -1, 0},
casts_shadow = true,
}
testing.expect(t, ld.casts_shadow, "casts_shadow should be true")
ld2 := renderer.Light_Data{}
testing.expect(t, !ld2.casts_shadow, "casts_shadow should default to false")
}
@(test)
test_atlas_region_single :: proc(t: ^testing.T) {
// Single shadow caster gets the full atlas
region := renderer.compute_atlas_uv_region(0, 1)
testing.expect_value(t, region, [4]f32{0, 0, 1, 1})
}
@(test)
test_atlas_region_multiple :: proc(t: ^testing.T) {
// Multiple shadow casters subdivide into quadrants
r0 := renderer.compute_atlas_uv_region(0, 4)
r1 := renderer.compute_atlas_uv_region(1, 4)
r2 := renderer.compute_atlas_uv_region(2, 4)
r3 := renderer.compute_atlas_uv_region(3, 4)
testing.expect_value(t, r0, [4]f32{0, 0, 0.5, 0.5})
testing.expect_value(t, r1, [4]f32{0.5, 0, 0.5, 0.5})
testing.expect_value(t, r2, [4]f32{0, 0.5, 0.5, 0.5})
testing.expect_value(t, r3, [4]f32{0.5, 0.5, 0.5, 0.5})
}
@(test)
test_atlas_pixel_region :: proc(t: ^testing.T) {
// Single caster gets full atlas in pixel space
region := renderer.compute_atlas_region(0, 1, 4096)
testing.expect_value(t, region.x, i32(0))
testing.expect_value(t, region.y, i32(0))
testing.expect_value(t, region.w, u32(4096))
testing.expect_value(t, region.h, u32(4096))
// Multiple casters get quadrants
r1 := renderer.compute_atlas_region(1, 4, 4096)
testing.expect_value(t, r1.x, i32(2048))
testing.expect_value(t, r1.y, i32(0))
testing.expect_value(t, r1.w, u32(2048))
testing.expect_value(t, r1.h, u32(2048))
}