Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ on:
- "**.vsh"
- "**.c"
- "**.h"
- "**.glsl"
- "**.hlsl"
- "**.m"
- "**.md"
- "**.metal"
- "**.wgsl"
- "**/ci.yml"
- ".github/workflows/*.yml"
pull_request:
Expand All @@ -17,8 +21,12 @@ on:
- "**.vsh"
- "**.c"
- "**.h"
- "**.glsl"
- "**.hlsl"
- "**.m"
- "**.md"
- "**.metal"
- "**.wgsl"
- "**/ci.yml"
- ".github/workflows/*.yml"

Expand Down Expand Up @@ -117,6 +125,7 @@ jobs:
fetch-depth: 0
- name: Run tests
env:
GUI_CHECK_GENERATED_SHADERS: "1"
VFLAGS: -no-parallel -cc clang
run: |
test_manifest="${RUNNER_TEMP}/gui-test-files"
Expand Down Expand Up @@ -394,6 +403,7 @@ jobs:
run: v -cc msvc run gui/_windows_preflight.vsh
- name: Run tests
env:
GUI_CHECK_GENERATED_SHADERS: "1"
VFLAGS: -no-parallel -cc msvc
VJOBS: 1
VTEST_ONLY_FN: test_*
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/windows-native-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ on:
description: "Also run exploratory MinGW/MSYS2 smoke"
type: boolean
default: false
run_d3d11:
description: "Also compile D3D11 probes; requires a V build with -d sokol_d3d11 support"
type: boolean
default: false

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
Expand Down Expand Up @@ -280,6 +284,29 @@ jobs:
v -no-parallel -cc msvc -W -o dialogs_smoke.exe gui/examples/dialogs.v
v -no-parallel -cc msvc -W -o printing_smoke.exe gui/examples/printing.v
v -no-parallel -cc msvc -W -o notification_smoke.exe gui/examples/native_notification.v
if ('${{ inputs.run_d3d11 }}' -eq 'true') {
$d3dExamples = @(
@{ Name = 'printing'; Path = 'gui/examples/printing.v'; Warn = $true },
@{ Name = 'gradient_demo'; Path = 'gui/examples/gradient_demo.v'; Warn = $true },
@{ Name = 'shadow_demo'; Path = 'gui/examples/shadow_demo.v'; Warn = $true },
@{ Name = 'blur_demo'; Path = 'gui/examples/blur_demo.v'; Warn = $true },
@{ Name = 'image_clip'; Path = 'gui/examples/image_clip.v'; Warn = $true },
@{ Name = 'svg_demo'; Path = 'gui/examples/svg_demo.v'; Warn = $true },
@{ Name = 'custom_shader'; Path = 'gui/examples/custom_shader.v'; Warn = $true },
@{ Name = 'showcase'; Path = 'gui/examples/showcase.v'; Warn = $false }
)
foreach ($example in $d3dExamples) {
$args = @('-no-parallel', '-cc', 'msvc', '-d', 'sokol_d3d11')
if ($example.Warn) {
$args += '-W'
}
$args += @('-o', "$($example.Name)_d3d11_smoke.exe", $example.Path)
v @args
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
}

clang:
name: Clang native smoke (exploratory)
Expand Down Expand Up @@ -348,6 +375,29 @@ jobs:
v -no-parallel -cc clang -W -o dialogs_smoke.exe gui/examples/dialogs.v
v -no-parallel -cc clang -W -o printing_smoke.exe gui/examples/printing.v
v -no-parallel -cc clang -W -o notification_smoke.exe gui/examples/native_notification.v
if ('${{ inputs.run_d3d11 }}' -eq 'true') {
$d3dExamples = @(
@{ Name = 'printing'; Path = 'gui/examples/printing.v'; Warn = $true },
@{ Name = 'gradient_demo'; Path = 'gui/examples/gradient_demo.v'; Warn = $true },
@{ Name = 'shadow_demo'; Path = 'gui/examples/shadow_demo.v'; Warn = $true },
@{ Name = 'blur_demo'; Path = 'gui/examples/blur_demo.v'; Warn = $true },
@{ Name = 'image_clip'; Path = 'gui/examples/image_clip.v'; Warn = $true },
@{ Name = 'svg_demo'; Path = 'gui/examples/svg_demo.v'; Warn = $true },
@{ Name = 'custom_shader'; Path = 'gui/examples/custom_shader.v'; Warn = $true },
@{ Name = 'showcase'; Path = 'gui/examples/showcase.v'; Warn = $false }
)
foreach ($example in $d3dExamples) {
$args = @('-no-parallel', '-cc', 'clang', '-d', 'sokol_d3d11')
if ($example.Warn) {
$args += '-W'
}
$args += @('-o', "$($example.Name)_d3d11_smoke.exe", $example.Path)
v @args
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
}

mingw:
name: MinGW native smoke (exploratory)
Expand Down Expand Up @@ -424,3 +474,26 @@ jobs:
v -no-parallel -cc gcc -W -o dialogs_smoke.exe gui/examples/dialogs.v
v -no-parallel -cc gcc -W -o printing_smoke.exe gui/examples/printing.v
v -no-parallel -cc gcc -W -o notification_smoke.exe gui/examples/native_notification.v
if [ '${{ inputs.run_d3d11 }}' = 'true' ]; then
for item in \
'printing:gui/examples/printing.v:-W' \
'gradient_demo:gui/examples/gradient_demo.v:-W' \
'shadow_demo:gui/examples/shadow_demo.v:-W' \
'blur_demo:gui/examples/blur_demo.v:-W' \
'image_clip:gui/examples/image_clip.v:-W' \
'svg_demo:gui/examples/svg_demo.v:-W' \
'custom_shader:gui/examples/custom_shader.v:-W' \
'showcase:gui/examples/showcase.v:'
do
IFS=':' read -r name path warn_flag <<< "$item"
args=(-no-parallel -cc gcc -d sokol_d3d11)
if [ -n "$warn_flag" ]; then
args+=("$warn_flag")
fi
args+=(-o "${name}_d3d11_smoke.exe" "$path")
v "${args[@]}"
if [ "$?" -ne 0 ]; then
exit 1
fi
done
fi
206 changes: 206 additions & 0 deletions _shaders_test.v
Original file line number Diff line number Diff line change
@@ -1,6 +1,212 @@
module gui

import math
import os

fn generated_shader_source_path() string {
return os.join_path(os.dir(@FILE), 'shaders', 'gui_builtin.glsl')
}

fn generated_shader_header_path() string {
return os.join_path(os.dir(@FILE), 'shaders', 'gui_builtin.h')
}

fn generated_shader_cmdline() string {
return 'sokol-shdc --input shaders/gui_builtin.glsl --output shaders/gui_builtin.h --slang glsl410:hlsl4:metal_macos'
}

fn generated_shader_programs() []string {
return [
'gui_blur',
'gui_filter_blur_h',
'gui_filter_blur_v',
'gui_filter_color',
'gui_filter_texture',
'gui_gradient',
'gui_image_clip',
'gui_rounded_rect',
'gui_shadow',
]
}

fn generated_shader_sgl_vertex_stages() []string {
return [
'gui_rect_vs',
'gui_shadow_vs',
'gui_gradient_vs',
'gui_filter_sgl_vs',
]
}

fn generated_shader_raw_vertex_stages() []string {
return [
'gui_filter_vs',
]
}

fn read_generated_shader_header() string {
return os.read_file(generated_shader_header_path()) or { panic(err) }
}

fn read_gui_source(name string) string {
return os.read_file(os.join_path(os.dir(@FILE), name)) or { panic(err) }
}

fn count_occurrences(haystack string, needle string) int {
mut count := 0
mut offset := 0
for {
index := haystack.index_after(needle, offset) or { break }
count++
offset = index + needle.len
}
return count
}

fn normalize_whitespace(src string) string {
return src.fields().join(' ')
}

fn sanitize_generated_shader_header(src string) string {
mut lines := []string{}
for line in src.split_into_lines() {
if line.contains('sokol-shdc --input') {
lines << ' ${generated_shader_cmdline()}'
continue
}
lines << line
}
return lines.join('\n')
}

fn test_generated_builtin_shader_files_are_committed() {
assert os.exists(generated_shader_source_path())
assert os.exists(generated_shader_header_path())
header := read_generated_shader_header()
assert header.contains("#version:1# (machine generated, don't edit!)")
assert header.contains('Generated by sokol-shdc')
}

fn test_generated_builtin_shader_header_cmdline_is_sanitized() {
header := read_generated_shader_header()
assert header.contains(generated_shader_cmdline())
assert !header.contains(os.dir(@FILE))
assert !header.contains('--input /')
assert !header.contains('--output /')
}

fn test_generated_builtin_shader_header_has_all_program_descriptors() {
header := read_generated_shader_header()
for program in generated_shader_programs() {
assert header.contains('static inline const sg_shader_desc* ${program}_shader_desc(sg_backend backend)')
}
}

fn test_generated_builtin_shader_header_has_d3d11_descriptor_coverage() {
header := read_generated_shader_header()
program_count := generated_shader_programs().len
assert header.contains('SG_BACKEND_GLCORE')
assert header.contains('SG_BACKEND_METAL_MACOS')
assert count_occurrences(header, 'if (backend == SG_BACKEND_D3D11)') == program_count
assert count_occurrences(header, 'desc.vs.d3d11_target = "vs_4_0";') == program_count
assert count_occurrences(header, 'desc.fs.d3d11_target = "ps_4_0";') == program_count
assert header.contains('gui_rect_vs_source_hlsl4')
assert header.contains('gui_shadow_vs_source_hlsl4')
assert header.contains('gui_gradient_vs_source_hlsl4')
assert header.contains('gui_filter_vs_source_hlsl4')
assert header.contains('gui_filter_sgl_vs_source_hlsl4')
assert header.contains('gui_filter_texture_fs_source_hlsl4')
}

fn test_generated_builtin_shader_header_uses_current_glcore_target() {
header := read_generated_shader_header()
generated_docs := read_gui_source('shaders/README.md')
shader_docs := read_gui_source('docs/SHADERS.md')
normalized_generated_docs := normalize_whitespace(generated_docs)
normalized_shader_docs := normalize_whitespace(shader_docs)

assert header.contains('sokol-shdc --input shaders/gui_builtin.glsl --output shaders/gui_builtin.h --slang glsl410:hlsl4:metal_macos')
assert header.contains('#version 410')
assert header.contains('gui_rect_vs_source_glsl410')
assert header.contains('gui_rounded_rect_fs_source_glsl410')
assert !header.contains('glsl330')
assert !header.contains('#version 330')

assert generated_docs.contains('`glsl410` target is intentional')
assert normalized_generated_docs.contains('Do not rewrite the generated header to `glsl330`')
assert normalized_generated_docs.contains('runtime custom `Shader.glsl` body format')

assert shader_docs.contains('Generated built-in shaders are separate from this runtime `Shader` API')
assert shader_docs.contains('`glsl410` for desktop `SG_BACKEND_GLCORE`')
assert normalized_shader_docs.contains('GLSL 3.3 contract below applies only to runtime custom `Shader.glsl` bodies')
}

fn test_generated_builtin_shader_header_keeps_sgl_binding_contracts() {
header := read_generated_shader_header()
for stage in generated_shader_sgl_vertex_stages() {
assert header.contains('#define ATTR_${stage}_position (0)')
assert header.contains('#define ATTR_${stage}_texcoord0 (1)')
assert header.contains('#define ATTR_${stage}_color0 (2)')
assert header.contains('#define ATTR_${stage}_psize (3)')
assert header.contains('#define SLOT_${stage}_params (0)')
}
assert count_occurrences(header, 'desc.attrs[3].name = "psize";') == 6
assert count_occurrences(header, 'desc.attrs[3].sem_name = "TEXCOORD";') == 6
assert count_occurrences(header, 'desc.attrs[3].sem_index = 3;') == 6
assert header.contains('desc.vs.source = (const char*)gui_filter_sgl_vs_source_hlsl4;')
assert header.contains('#define SLOT_tex (0)')
assert header.contains('#define SLOT_smp (0)')
}

fn test_generated_builtin_shader_header_keeps_raw_filter_binding_contract() {
header := read_generated_shader_header()
for stage in generated_shader_raw_vertex_stages() {
assert header.contains('#define ATTR_${stage}_position (0)')
assert header.contains('#define ATTR_${stage}_texcoord0 (1)')
assert header.contains('#define ATTR_${stage}_color0 (2)')
assert !header.contains('#define ATTR_${stage}_psize (3)')
assert header.contains('#define SLOT_${stage}_params (0)')
}
}

fn test_d3d11_custom_shader_guard_returns_before_runtime_source_pipeline() {
source := read_gui_source('shaders.v')
guard_index := source.index('custom GLSL/Metal shaders are not supported with -d sokol_d3d11') or {
panic(err)
}
glsl_build_index := source.index('fs_source := build_glsl_fragment(shader.glsl)') or {
panic(err)
}
make_shader_index := source.index('shader: gfx.make_shader(&shader_desc)') or { panic(err) }
guard_return_index := source.index_after('return pip', guard_index) or { panic(err) }
assert guard_index < glsl_build_index
assert guard_index < make_shader_index
assert guard_return_index < glsl_build_index
assert guard_return_index < make_shader_index
}

fn test_generated_builtin_shader_header_is_fresh_when_requested() {
if os.getenv('GUI_CHECK_GENERATED_SHADERS') != '1' {
return
}

tmp_dir := os.join_path(os.temp_dir(), 'gui_builtin_shader_fresh_${os.getpid()}')
tmp_shader_dir := os.join_path(tmp_dir, 'shaders')
os.rmdir_all(tmp_dir) or {}
os.mkdir_all(tmp_shader_dir) or { panic(err) }
tmp_source := os.join_path(tmp_shader_dir, 'gui_builtin.glsl')
tmp_header := os.join_path(tmp_shader_dir, 'gui_builtin.h')
os.cp(generated_shader_source_path(), tmp_source) or { panic(err) }

cmd := '${os.quoted_path(@VEXE)} shader -l glsl410 -l hlsl4 -l metal_macos ${os.quoted_path(tmp_source)}'
res := os.execute(cmd)
assert res.exit_code == 0, res.output

checked_in := sanitize_generated_shader_header(read_generated_shader_header())
regenerated := sanitize_generated_shader_header(os.read_file(tmp_header) or { panic(err) })
assert checked_in == regenerated, 'generated shader header is stale; run `v shader -l glsl410 -l hlsl4 -l metal_macos shaders/gui_builtin.glsl`'
os.rmdir_all(tmp_dir) or {}
}

fn test_pack_shader_params() {
// 10.0 px radius -> 40 fixed-point units.
Expand Down
24 changes: 24 additions & 0 deletions _sokol_formats_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module gui

import os

fn gui_module_source(name string) string {
path := os.join_path(os.dir(@FILE), name)
return os.read_file(path) or { panic(err) }
}

fn test_render_target_formats_use_sapp_glue_defaults() {
helper := gui_module_source('sokol_formats.v')
assert helper.contains('sapp.glue_environment().defaults')
assert !helper.contains('PixelFormat.from(sapp.color_format())')
assert !helper.contains('PixelFormat.from(sapp.depth_format())')
}

fn test_raster_and_filter_targets_share_render_target_format_helper() {
for source_name in ['print_raster.v', 'shaders.v'] {
source := gui_module_source(source_name)
assert source.contains('gui_render_target_formats()'), source_name
assert !source.contains('PixelFormat.from(sapp.color_format())'), source_name
assert !source.contains('PixelFormat.from(sapp.depth_format())'), source_name
}
}
Loading
Loading