CultLib/packages/cultmath/tools/generate-swizzles.ps1
Meta Vens 7821e06d55 Add HLSL vector conversions, int clamp, and double frac to CultMath
Aetheria Cut 8a needs int2(float2), clamp(int, int, int), and
frac(double). Fill them in CultMath as dxc compiles them:

- generate-swizzles.ps1 emits same-size conversion constructors and
  math functions between float, int, and bool vectors. dxc lowers
  float to int as fptosi (toward zero; NaN and out-of-range undefined),
  numeric to bool as != 0 (fcmp une, NaN is true), bool to numeric as
  0 or 1. floatN(int) splat keeps floatN(0) from going ambiguous
  against floatN(intN).
- Int and int-vector clamp are min(max(x, a), b), matching dxc's
  IMin(IMax(x, a), b); inverted bounds return b.
- frac(double) keeps double precision. dxc demotes double frac to
  float, as it does exp and lerp; CultMath's existing double overloads
  already diverge that way, and design.md now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:16:32 +02:00

159 lines
6.7 KiB
PowerShell

param(
[string]$OutputPath = ""
)
# Writes src\CultMath\Swizzles.g.cs: HLSL swizzles and mixed constructors for
# CultMath's vector types. The output is checked in; rerun after changing the
# family table below and commit both files together.
$ErrorActionPreference = "Stop"
if (-not $OutputPath) {
$OutputPath = Join-Path $PSScriptRoot "..\src\CultMath\Swizzles.g.cs"
}
# Functions: emit math.<type>(...) constructor functions. double has none because
# a math.double3 method group would hide the double3 type name under
# `using static CultMath.math;` and HLSL shader code does not construct doubles.
$families = @(
@{ Scalar = "float"; Sizes = @(2, 3, 4); Functions = $true },
@{ Scalar = "int"; Sizes = @(2, 3, 4); Functions = $true },
@{ Scalar = "bool"; Sizes = @(2, 3, 4); Functions = $true },
@{ Scalar = "double"; Sizes = @(2, 3); Functions = $false }
)
# Same-size vector conversions, as dxc lowers them: float to int is fptosi (toward zero; NaN
# and out-of-range are undefined in DXIL), int to float is sitofp, numeric to bool is `!= 0`
# (fcmp une, so NaN is true), bool to numeric is 0 or 1. Keyed "target<-source"; {0} is a
# source component. Identity forms (intN(intN), ...) come from the composition loop, which
# also keeps an int vector argument off the lossy intN(floatN) overload.
$conversions = [ordered]@{
"float<-int" = "{0}"
"float<-bool" = "{0} ? 1.0f : 0.0f"
"int<-float" = "(int){0}"
"int<-bool" = "{0} ? 1 : 0"
"bool<-float" = "{0} != 0.0f"
"bool<-int" = "{0} != 0"
}
$componentFields = @("x", "y", "z", "w")
$namings = @(@("x", "y", "z", "w"), @("r", "g", "b", "a"))
function Get-Compositions([int]$total) {
# Ordered part sizes summing to $total, excluding the all-scalar form that the
# hand-written base constructor already owns.
$results = [System.Collections.Generic.List[object]]::new()
function Expand([int]$remaining, [int[]]$prefix) {
if ($remaining -eq 0) {
if (@($prefix | Where-Object { $_ -ne 1 }).Count -gt 0) {
$results.Add($prefix)
}
return
}
for ($part = 1; $part -le $remaining; $part++) {
Expand ($remaining - $part) ($prefix + $part)
}
}
Expand $total @()
return , $results
}
$builder = [System.Text.StringBuilder]::new()
function Line([string]$text = "") { [void]$builder.Append($text).Append("`n") }
Line "// <auto-generated>"
Line "// Generated by tools/generate-swizzles.ps1. Do not edit by hand."
Line "// </auto-generated>"
Line ""
Line "namespace CultMath;"
$functionLines = [System.Collections.Generic.List[string]]::new()
foreach ($family in $families) {
$scalar = $family.Scalar
$maxSize = ($family.Sizes | Measure-Object -Maximum).Maximum
foreach ($size in $family.Sizes) {
$type = "$scalar$size"
Line ""
Line "public partial struct $type"
Line "{"
foreach ($composition in (Get-Compositions $size)) {
$parameters = @()
$arguments = @()
$offset = 0
foreach ($part in $composition) {
$name = -join $componentFields[$offset..($offset + $part - 1)]
if ($part -eq 1) {
$parameters += "$scalar $name"
$arguments += $name
} else {
$parameters += "$scalar$part $name"
for ($i = 0; $i -lt $part; $i++) { $arguments += "$name.$($componentFields[$i])" }
}
$offset += $part
}
$parameterList = $parameters -join ", "
Line " public $type($parameterList) : this($($arguments -join ', ')) { }"
if ($family.Functions) {
$names = @($parameters | ForEach-Object { ($_ -split " ")[1] })
$functionLines.Add(" public static $type $type($parameterList) => new($($names -join ', '));")
}
}
foreach ($source in @("float", "int", "bool")) {
$expression = $conversions["$scalar<-$source"]
if (-not $expression) { continue }
$converted = for ($i = 0; $i -lt $size; $i++) { $expression -f "value.$($componentFields[$i])" }
Line " public $type($source$size value) : this($($converted -join ', ')) { }"
if ($family.Functions) {
$functionLines.Add(" public static $type $type($source$size value) => new(value);")
}
}
if ($scalar -eq "float" -and $family.Functions) {
# floatN(int) splat: without it floatN(0) is ambiguous between floatN(float) and floatN(intN).
$functionLines.Add(" public static $type $type(int value) => new($((@('value') * $size) -join ', '));")
}
# Single-component color aliases; x/y/z/w are the fields themselves.
$letters = $namings[1]
for ($i = 0; $i -lt $size; $i++) {
Line " public $scalar $($letters[$i]) { readonly get => $($componentFields[$i]); set => $($componentFields[$i]) = value; }"
}
foreach ($letters in $namings) {
for ($length = 2; $length -le $maxSize; $length++) {
$count = [math]::Pow($size, $length)
for ($index = 0; $index -lt $count; $index++) {
[int[]]$digits = @()
[int]$remainder = $index
for ($d = 0; $d -lt $length; $d++) {
$digits = @([int]($remainder % $size)) + $digits
$remainder = [int][math]::Floor($remainder / $size)
}
$name = -join ($digits | ForEach-Object { $letters[$_] })
$getter = ($digits | ForEach-Object { $componentFields[$_] }) -join ", "
$resultType = "$scalar$length"
$unique = @($digits | Select-Object -Unique).Count -eq $length
if ($unique) {
$assignments = for ($d = 0; $d -lt $length; $d++) { "$($componentFields[$digits[$d]]) = value.$($componentFields[$d]);" }
Line " public $resultType $name { readonly get => new($getter); set { $($assignments -join ' ') } }"
} else {
Line " public readonly $resultType $name => new($getter);"
}
}
}
}
Line "}"
}
}
Line ""
Line "public static partial class math"
Line "{"
foreach ($functionLine in $functionLines) { Line $functionLine }
Line "}"
$fullPath = [System.IO.Path]::GetFullPath($OutputPath)
$content = $builder.ToString() -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($fullPath, $content, [System.Text.UTF8Encoding]::new($false))
Write-Host "Wrote $fullPath ($((Get-Item -LiteralPath $fullPath).Length) bytes)"