///|
/// Shader pipeline contracts (outline only).
///
/// Ebiten refs:
/// - shader.go
/// - internal/graphics/shader.go
/// - internal/shader/shader.go
/// - internal/ui/shader.go
/// - internal/shaderir/program.go
/// - internal/graphicscommand/commandqueue.go
/// - internal/builtinshader/shader.go
///|
pub(all) enum ShaderUnit {
Pixels
Texels
} derive(Debug)
///|
pub impl Show for ShaderUnit with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct ShaderEntrypoints {
vertex : String
fragment : String
} derive(Debug)
///|
pub impl Show for ShaderEntrypoints with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct ShaderCompileRequest {
source : String
unit_hint : ShaderUnit?
src_image_count : Int
entrypoints : ShaderEntrypoints
debug_name : String
} derive(Debug)
///|
pub impl Show for ShaderCompileRequest with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct ShaderIR {
source : String
unit : ShaderUnit
noperspective : Bool
src_image_count : Int
entrypoints : ShaderEntrypoints
debug_name : String
source_hash : ShaderSourceHash
} derive(Debug)
///|
pub impl Show for ShaderIR with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct ShaderSourceHash {
value : String
} derive(Debug)
///|
pub impl Show for ShaderSourceHash with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) enum UniformValue {
Bool(Bool)
Int(Int)
Float(Double)
Bools(Array[Bool])
Ints(Array[Int])
Floats(Array[Double])
} derive(Debug)
///|
pub impl Show for UniformValue with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct NamedUniform {
name : String
value : UniformValue
} derive(Debug)
///|
pub impl Show for NamedUniform with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct UniformLayout {
names : Array[String]
dword_counts : Array[Int]
preserved_prefix_dwords : Int
} derive(Debug)
///|
pub impl Show for UniformLayout with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct IntSize {
width : Int
height : Int
} derive(Debug)
///|
pub impl Show for IntSize with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct FloatRect {
x : Double
y : Double
width : Double
height : Double
} derive(Debug)
///|
pub impl Show for FloatRect with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct PreservedUniformContext {
dst_texture_size : IntSize
dst_region : FloatRect
src_texture_sizes : Array[IntSize]
src_regions : Array[FloatRect]
} derive(Debug)
///|
pub impl Show for PreservedUniformContext with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) struct PackedUniforms {
dwords : Array[Int]
} derive(Debug)
///|
pub impl Show for PackedUniforms with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) enum BuiltinShaderFilter {
Nearest
Linear
Pixelated
} derive(Debug)
///|
pub impl Show for BuiltinShaderFilter with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub(all) enum BuiltinShaderAddress {
Unsafe
ClampToZero
ClampToEdge
Repeat
MirrorRepeat
} derive(Debug)
///|
pub impl Show for BuiltinShaderAddress with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
/// Per-axis sampler specification for texture sampling.
pub(all) struct SamplerSpec {
filter : BuiltinShaderFilter
address_u : BuiltinShaderAddress
address_v : BuiltinShaderAddress
} derive(Debug)
///|
pub impl Show for SamplerSpec with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub fn default_sampler_spec() -> SamplerSpec {
{
filter: BuiltinShaderFilter::Nearest,
address_u: BuiltinShaderAddress::Unsafe,
address_v: BuiltinShaderAddress::Unsafe,
}
}
///|
/// Convenience: create SamplerSpec with same address on both axes.
pub fn sampler_spec(
filter : BuiltinShaderFilter,
address : BuiltinShaderAddress,
) -> SamplerSpec {
{ filter, address_u: address, address_v: address }
}
///|
pub(all) struct BuiltinShaderKey {
filter : BuiltinShaderFilter
address : BuiltinShaderAddress
use_color_m : Bool
} derive(Debug)
///|
pub impl Show for BuiltinShaderKey with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
/// Extended shader key that supports per-axis address modes.
pub(all) struct BuiltinShaderKeyEx {
sampler : SamplerSpec
use_color_m : Bool
} derive(Debug)
///|
pub impl Show for BuiltinShaderKeyEx with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
/// Convert a classic BuiltinShaderKey to extended form.
pub fn BuiltinShaderKey::to_ex(self : BuiltinShaderKey) -> BuiltinShaderKeyEx {
{
sampler: {
filter: self.filter,
address_u: self.address,
address_v: self.address,
},
use_color_m: self.use_color_m,
}
}
///|
/// Frontend for shader sources: lowers a `ShaderCompileRequest` into a
/// `ShaderIR` and computes a stable hash. The hash is used to dedupe
/// builtin shader variants in the backend cache.
///
/// `BasicShaderFrontend` is the reference implementation.
pub(open) trait ShaderFrontend {
/// Compile the source string into a `ShaderIR`.
/// Ebiten ref: internal/graphics/shader.go (CompileShader).
fn compile_ir(Self, request : ShaderCompileRequest) -> ShaderIR raise
/// Compute a stable content-addressed hash of a compile request.
/// Two requests with the same hash MUST produce identical IR.
/// Ebiten ref: internal/graphics/shader.go (CalcSourceHash).
fn calc_source_hash(Self, request : ShaderCompileRequest) -> ShaderSourceHash raise
}
///|
/// Packs user-visible uniforms (`NamedUniform`s) into the dense dword
/// buffer the backend actually uploads, and prepends the engine-defined
/// "preserved" prefix (destination size, source regions, etc.).
///
/// `BasicUniformCanonicalizer` is the reference implementation.
pub(open) trait UniformCanonicalizer {
/// Append the user-supplied uniforms in the order declared by
/// `layout`. Missing uniforms should raise.
/// Ebiten ref: internal/ui/shader.go (AppendUniforms).
fn append_user_uniforms(
Self,
layout : UniformLayout,
uniforms : Array[NamedUniform],
) -> PackedUniforms raise
/// Prepend the preserved uniform prefix (`dst_texture_size`,
/// `dst_region`, `src_texture_sizes`, `src_regions`) ahead of the
/// user payload.
/// Ebiten ref: internal/graphicscommand/commandqueue.go (prependPreservedUniforms).
fn prepend_preserved_uniforms(
Self,
uniforms : PackedUniforms,
context : PreservedUniformContext,
) -> PackedUniforms
/// Strip uniforms that don't appear in the compiled IR; lets the
/// backend skip uploading unused values.
/// Ebiten ref: internal/shaderir/program.go (FilterUniformVariables).
fn filter_unused_uniforms(
Self,
ir : ShaderIR,
layout : UniformLayout,
uniforms : PackedUniforms,
) -> PackedUniforms
}
///|
/// Source of WGSL / shader code for built-in shaders keyed by filter +
/// address mode (and optionally a color matrix).
///
/// `BasicBuiltinShaderSourceRepo` is the reference implementation; it
/// caches generated WGSL by key.
pub(open) trait BuiltinShaderSourceRepo {
/// Look up the shader for the basic (filter, address, color_m) key.
/// Ebiten ref: internal/builtinshader/shader.go (ShaderSource).
fn shader_source(Self, key : BuiltinShaderKey) -> String
/// Look up the shader for the extended (per-axis sampler) key. Used
/// where the U and V axes have different wrap modes.
fn shader_source_ex(Self, key : BuiltinShaderKeyEx) -> String
}
///|
/// Compose `append_user_uniforms` + `prepend_preserved_uniforms` +
/// `filter_unused_uniforms` and normalize the result to the layout
/// length. Most callers want this rather than the trait methods
/// individually.
pub fn[T : UniformCanonicalizer] build_canonical_uniforms(
canonicalizer : T,
layout : UniformLayout,
user_uniforms : Array[NamedUniform],
context : PreservedUniformContext,
ir : ShaderIR,
) -> PackedUniforms raise {
let user = canonicalizer.append_user_uniforms(layout, user_uniforms)
let preserved = canonicalizer.prepend_preserved_uniforms(user, context)
let filtered = canonicalizer.filter_unused_uniforms(ir, layout, preserved)
normalize_uniform_length(layout, filtered)
}
///|
pub struct BasicShaderFrontend {} derive(Debug)
///|
pub struct BasicUniformCanonicalizer {} derive(Debug)
///|
struct BuiltinShaderSourceCacheEntry {
key : BuiltinShaderKey
source : String
last_used_tick : Int
} derive(Debug)
///|
struct BuiltinShaderSourceCacheEntryEx {
key : BuiltinShaderKeyEx
source : String
last_used_tick : Int
} derive(Debug)
///|
pub struct BuiltinShaderCacheStats {
hit_count : Int
miss_count : Int
} derive(Debug)
///|
pub impl Show for BuiltinShaderCacheStats with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
pub struct BasicBuiltinShaderSourceRepo {
mut cache : Array[BuiltinShaderSourceCacheEntry]
mut cache_ex : Array[BuiltinShaderSourceCacheEntryEx]
mut tick : Int
max_cache_entries : Int
mut hit_count : Int
mut miss_count : Int
} derive(Debug)
///|
/// Sanity-check the request before handing it to a `ShaderFrontend`.
/// Returns None if valid, Some(error_message) if invalid.
pub fn ShaderCompileRequest::validate(self : ShaderCompileRequest) -> String? {
if self.source.length() == 0 {
return Some("shader source must not be empty")
}
if self.entrypoints.vertex.length() == 0 {
return Some("vertex entrypoint must not be empty")
}
if self.entrypoints.fragment.length() == 0 {
return Some("fragment entrypoint must not be empty")
}
if self.src_image_count < 0 {
return Some("src_image_count must not be negative")
}
None
}
///|
pub fn ShaderUnit::eq(self : ShaderUnit, rhs : ShaderUnit) -> Bool {
match self {
ShaderUnit::Pixels =>
match rhs {
ShaderUnit::Pixels => true
_ => false
}
ShaderUnit::Texels =>
match rhs {
ShaderUnit::Texels => true
_ => false
}
}
}
///|
pub fn ShaderSourceHash::eq(
self : ShaderSourceHash,
rhs : ShaderSourceHash,
) -> Bool {
self.value == rhs.value
}
///|
pub fn new_basic_shader_frontend() -> BasicShaderFrontend {
BasicShaderFrontend::{ }
}
///|
pub fn new_basic_uniform_canonicalizer() -> BasicUniformCanonicalizer {
BasicUniformCanonicalizer::{ }
}
///|
pub fn new_basic_builtin_shader_source_repo() -> BasicBuiltinShaderSourceRepo {
{
cache: [],
cache_ex: [],
tick: 0,
max_cache_entries: 8,
hit_count: 0,
miss_count: 0,
}
}
///|
pub fn BasicBuiltinShaderSourceRepo::cache_size(
self : BasicBuiltinShaderSourceRepo,
) -> Int {
self.cache.length() + self.cache_ex.length()
}
///|
pub fn BasicBuiltinShaderSourceRepo::cache_limit(
self : BasicBuiltinShaderSourceRepo,
) -> Int {
self.max_cache_entries
}
///|
pub fn BasicBuiltinShaderSourceRepo::clear_cache(
self : BasicBuiltinShaderSourceRepo,
) -> Unit {
self.cache = []
self.cache_ex = []
self.hit_count = 0
self.miss_count = 0
}
///|
pub fn BasicBuiltinShaderSourceRepo::cache_stats(
self : BasicBuiltinShaderSourceRepo,
) -> BuiltinShaderCacheStats {
{ hit_count: self.hit_count, miss_count: self.miss_count }
}
///|
/// Sanity-check a uniform layout for common errors. Returns None if
/// valid, Some(error_message) if invalid.
pub fn UniformLayout::validate(self : UniformLayout) -> String? {
if self.names.length() != self.dword_counts.length() {
return Some(
"names length (\{self.names.length()}) must match dword_counts length (\{self.dword_counts.length()})",
)
}
for i in 0.. Int {
let src_count = if self.src_texture_sizes.length() <=
self.src_regions.length() {
self.src_texture_sizes.length()
} else {
self.src_regions.length()
}
2 + 4 + src_count * (2 + 4)
}
///|
/// Check that the preserved-uniform context is internally consistent.
/// Returns None if valid, Some(error_message) if invalid.
pub fn PreservedUniformContext::validate(
self : PreservedUniformContext,
) -> String? {
if self.src_texture_sizes.length() != self.src_regions.length() {
return Some(
"src_texture_sizes length (\{self.src_texture_sizes.length()}) must match src_regions length (\{self.src_regions.length()})",
)
}
if self.dst_texture_size.width <= 0 || self.dst_texture_size.height <= 0 {
return Some("dst_texture_size must have positive dimensions")
}
None
}
///|
/// Convert a Double to IEEE 754 f32 bit representation as Int.
pub fn double_to_f32_bits(v : Double) -> Int {
let bits = v.reinterpret_as_int64()
let ubits = bits.reinterpret_as_uint64()
let sign = ((ubits >> 63) & 1UL).reinterpret_as_int64().to_int()
let exp = ((ubits >> 52) & 0x7FFUL).reinterpret_as_int64().to_int()
let mantissa = bits & 0xFFFFFFFFFFFFFL
if exp == 0x7FF {
// Infinity or NaN
if mantissa != 0L {
(sign << 31) | 0x7FC00000
} else {
(sign << 31) | 0x7F800000
}
} else if exp == 0 {
sign << 31
} else { // zero/denormal → zero
let f32_exp = exp - 1023 + 127
if f32_exp >= 255 {
(sign << 31) | 0x7F800000
} else if f32_exp <= 0 {
sign << 31
} else {
let f32_mantissa = (mantissa.reinterpret_as_uint64() >> 29)
.reinterpret_as_int64()
.to_int()
(sign << 31) | (f32_exp << 23) | f32_mantissa
}
}
}
///|
/// Convert IEEE 754 f32 bits (as Int) back to Double.
pub fn f32_bits_to_double(bits : Int) -> Double {
let sign = (bits.reinterpret_as_uint() >> 31).reinterpret_as_int() & 1
let exp = (bits.reinterpret_as_uint() >> 23).reinterpret_as_int() & 0xFF
let mantissa = bits & 0x7FFFFF
if exp == 0xFF {
if mantissa != 0 {
// NaN
0.0 / 0.0
} else if sign != 0 {
-1.0 / 0.0
} else {
1.0 / 0.0
}
} else if exp == 0 {
if sign != 0 {
-0.0
} else {
0.0
}
} else {
let f64_exp = (exp - 127 + 1023).to_int64()
let f64_mantissa = mantissa.to_int64() << 29
let f64_bits = (sign.to_int64() << 63) | (f64_exp << 52) | f64_mantissa
f64_bits.reinterpret_as_double()
}
}
///|
fn uniform_to_dwords(value : UniformValue) -> Array[Int] {
match value {
Bool(v) => [if v { 1 } else { 0 }]
Int(v) => [v]
Float(v) => [double_to_f32_bits(v)]
Bools(vs) => vs.map(fn(v) { if v { 1 } else { 0 } })
Ints(vs) => vs
Floats(vs) => vs.map(fn(v) { double_to_f32_bits(v) })
}
}
///|
fn find_named_uniform(
uniforms : Array[NamedUniform],
name : String,
) -> UniformValue? {
for uniform in uniforms {
if uniform.name == name {
return Some(uniform.value)
}
}
None
}
///|
fn normalized_src_image_count(src_image_count : Int) -> Int {
if src_image_count < 0 {
0
} else {
src_image_count
}
}
///|
fn shader_unit_tag(unit : ShaderUnit) -> String {
match unit {
ShaderUnit::Pixels => "pixels"
ShaderUnit::Texels => "texels"
}
}
///|
fn is_space_like(ch : Char) -> Bool {
ch == ' ' || ch == '\t'
}
///|
fn char_array_starts_with(
chars : Array[Char],
offset : Int,
prefix : Array[Char],
) -> Bool {
if offset < 0 || offset + prefix.length() > chars.length() {
false
} else {
for i in 0.. ShaderUnit? {
let chars = source.to_array()
let directive = "//kage:unit".to_array()
let pixels = "pixels".to_array()
let texels = "texels".to_array()
let chars_len = chars.length()
let directive_len = directive.length()
if chars_len < directive_len {
None
} else {
for i in 0..<(chars_len - directive_len + 1) {
if !char_array_starts_with(chars, i, directive) {
continue
}
let mut cursor = i + directive_len
while cursor < chars_len && is_space_like(chars[cursor]) {
cursor = cursor + 1
}
if char_array_starts_with(chars, cursor, texels) {
let right = cursor + texels.length()
if right >= chars_len || !is_identifier_char(chars[right]) {
return Some(ShaderUnit::Texels)
}
}
if char_array_starts_with(chars, cursor, pixels) {
let right = cursor + pixels.length()
if right >= chars_len || !is_identifier_char(chars[right]) {
return Some(ShaderUnit::Pixels)
}
}
}
None
}
}
///|
fn resolve_shader_unit(request : ShaderCompileRequest) -> ShaderUnit {
match parse_kage_unit_directive(request.source) {
Some(unit) => unit
None =>
match request.unit_hint {
Some(unit) => unit
None => ShaderUnit::Pixels
}
}
}
///|
fn preprocess_shader_source(
source : String,
unit : ShaderUnit,
entrypoints : ShaderEntrypoints,
debug_name : String,
) -> String {
let normalized = source
.replace_all(old="\r\n", new="\n")
.replace_all(old="\r", new="\n")
"// kagura:shader\n// debug:\{debug_name}\n// unit:\{shader_unit_tag(unit)}\n// vertex:\{entrypoints.vertex}\n// fragment:\{entrypoints.fragment}\n\{normalized}"
}
///|
fn mix_hash(seed : Int, value : Int) -> Int {
seed * 16777619 + value + 31
}
///|
fn calc_request_hash_value(
preprocessed_source : String,
debug_name : String,
src_image_count : Int,
entrypoints : ShaderEntrypoints,
unit : ShaderUnit,
) -> String {
let mut seed = 146959810
seed = mix_hash(seed, preprocessed_source.hash())
seed = mix_hash(seed, debug_name.hash())
seed = mix_hash(seed, entrypoints.vertex.hash())
seed = mix_hash(seed, entrypoints.fragment.hash())
seed = mix_hash(seed, src_image_count)
seed = mix_hash(
seed,
match unit {
ShaderUnit::Pixels => 1
ShaderUnit::Texels => 2
},
)
seed.to_string()
}
///|
fn normalized_dword_count(value : Int) -> Int {
if value <= 0 {
0
} else {
value
}
}
///|
fn layout_entry_count(layout : UniformLayout) -> Int {
let names_len = layout.names.length()
let counts_len = layout.dword_counts.length()
if names_len <= counts_len {
names_len
} else {
counts_len
}
}
///|
fn normalized_preserved_prefix_dwords(layout : UniformLayout) -> Int {
if layout.preserved_prefix_dwords <= 0 {
0
} else {
layout.preserved_prefix_dwords
}
}
///|
fn expected_user_uniform_dwords(layout : UniformLayout) -> Int {
let mut total = 0
let entry_count = layout_entry_count(layout)
for i in 0.. PackedUniforms {
let expected_preserved = normalized_preserved_prefix_dwords(layout)
let expected_user = expected_user_uniform_dwords(layout)
let dwords : Array[Int] = []
for i in 0.. expected_user {
uniforms.dwords.length() - expected_user
} else {
0
}
for i in 0.. Bool {
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '_'
}
///|
fn contains_uniform_identifier(source : String, name : String) -> Bool {
let source_chars = source.to_array()
let name_chars = name.to_array()
let source_len = source_chars.length()
let name_len = name_chars.length()
if name_len == 0 || source_len < name_len {
false
} else {
for i in 0..<(source_len - name_len + 1) {
let mut matched = true
for j in 0..= source_len {
true
} else {
!is_identifier_char(source_chars[right_index])
}
if left_ok && right_ok {
return true
}
}
}
false
}
}
///|
pub impl ShaderFrontend for BasicShaderFrontend with fn compile_ir(
_self,
request,
) {
let src_image_count = normalized_src_image_count(request.src_image_count)
let unit = resolve_shader_unit(request)
let noperspective = parse_kage_noperspective_directive(request.source)
let source = preprocess_shader_source(
request.source,
unit,
request.entrypoints,
request.debug_name,
)
let hash = {
value: calc_request_hash_value(
source,
request.debug_name,
src_image_count,
request.entrypoints,
unit,
),
}
{
source,
unit,
noperspective,
src_image_count,
entrypoints: request.entrypoints,
debug_name: request.debug_name,
source_hash: hash,
}
}
///|
pub impl ShaderFrontend for BasicShaderFrontend with fn calc_source_hash(
_self,
request,
) {
let src_image_count = normalized_src_image_count(request.src_image_count)
let unit = resolve_shader_unit(request)
let source = preprocess_shader_source(
request.source,
unit,
request.entrypoints,
request.debug_name,
)
{
value: calc_request_hash_value(
source,
request.debug_name,
src_image_count,
request.entrypoints,
unit,
),
}
}
///|
pub impl UniformCanonicalizer for BasicUniformCanonicalizer with fn append_user_uniforms(
_self,
layout,
uniforms,
) {
let dwords : Array[Int] = []
let entry_count = layout_entry_count(layout)
for i in 0.. {
let encoded = uniform_to_dwords(value)
let mut written = 0
for v in encoded {
if written >= expected {
break
}
dwords.push(v)
written = written + 1
}
while written < expected {
dwords.push(0)
written = written + 1
}
}
None =>
for _ in 0.. expected_user {
uniforms.dwords.length() - expected_user
} else {
0
}
let out : Array[Int] = []
for value in uniforms.dwords {
out.push(value)
}
let mut cursor = 0
for i in 0.. Bool {
match lhs {
BuiltinShaderFilter::Nearest =>
match rhs {
BuiltinShaderFilter::Nearest => true
_ => false
}
BuiltinShaderFilter::Linear =>
match rhs {
BuiltinShaderFilter::Linear => true
_ => false
}
BuiltinShaderFilter::Pixelated =>
match rhs {
BuiltinShaderFilter::Pixelated => true
_ => false
}
}
}
///|
fn builtin_address_eq(
lhs : BuiltinShaderAddress,
rhs : BuiltinShaderAddress,
) -> Bool {
match lhs {
BuiltinShaderAddress::Unsafe =>
match rhs {
BuiltinShaderAddress::Unsafe => true
_ => false
}
BuiltinShaderAddress::ClampToZero =>
match rhs {
BuiltinShaderAddress::ClampToZero => true
_ => false
}
BuiltinShaderAddress::ClampToEdge =>
match rhs {
BuiltinShaderAddress::ClampToEdge => true
_ => false
}
BuiltinShaderAddress::Repeat =>
match rhs {
BuiltinShaderAddress::Repeat => true
_ => false
}
BuiltinShaderAddress::MirrorRepeat =>
match rhs {
BuiltinShaderAddress::MirrorRepeat => true
_ => false
}
}
}
///|
fn builtin_shader_key_eq(
lhs : BuiltinShaderKey,
rhs : BuiltinShaderKey,
) -> Bool {
builtin_filter_eq(lhs.filter, rhs.filter) &&
builtin_address_eq(lhs.address, rhs.address) &&
lhs.use_color_m == rhs.use_color_m
}
///|
fn sampler_spec_eq(lhs : SamplerSpec, rhs : SamplerSpec) -> Bool {
builtin_filter_eq(lhs.filter, rhs.filter) &&
builtin_address_eq(lhs.address_u, rhs.address_u) &&
builtin_address_eq(lhs.address_v, rhs.address_v)
}
///|
fn builtin_shader_key_ex_eq(
lhs : BuiltinShaderKeyEx,
rhs : BuiltinShaderKeyEx,
) -> Bool {
sampler_spec_eq(lhs.sampler, rhs.sampler) &&
lhs.use_color_m == rhs.use_color_m
}
///|
fn find_builtin_shader_cache_index(
cache : Array[BuiltinShaderSourceCacheEntry],
key : BuiltinShaderKey,
) -> Int? {
for i in 0.. Int? {
for i in 0.. Unit {
if repo.max_cache_entries <= 0 {
repo.cache_ex = []
} else {
let total = repo.cache.length() + repo.cache_ex.length()
let mut remaining = total
while remaining > repo.max_cache_entries {
// Find oldest across both caches
let mut oldest_tick = 2147483647
let mut evict_classic = true
let mut evict_index = -1
for i in 0.. Unit {
if repo.max_cache_entries <= 0 {
repo.cache = []
} else {
while repo.cache.length() > repo.max_cache_entries {
let mut oldest_index = 0
let mut oldest_tick = repo.cache[0].last_used_tick
for i in 1.. String {
match filter {
BuiltinShaderFilter::Nearest => "nearest"
BuiltinShaderFilter::Linear => "linear"
BuiltinShaderFilter::Pixelated => "pixelated"
}
}
///|
fn builtin_address_tag(address : BuiltinShaderAddress) -> String {
match address {
BuiltinShaderAddress::Unsafe => "unsafe"
BuiltinShaderAddress::ClampToZero => "clamp_to_zero"
BuiltinShaderAddress::ClampToEdge => "clamp_to_edge"
BuiltinShaderAddress::Repeat => "repeat"
BuiltinShaderAddress::MirrorRepeat => "mirror_repeat"
}
}
///|
fn builtin_address_snippet(address : BuiltinShaderAddress) -> String {
match address {
BuiltinShaderAddress::Unsafe => "// address: unsafe\n let sample_uv = uv;"
BuiltinShaderAddress::ClampToZero =>
"if uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 {\n return vec4f(0.0, 0.0, 0.0, 0.0);\n }\n let sample_uv = uv;"
BuiltinShaderAddress::ClampToEdge =>
"let sample_uv = clamp(uv, vec2f(0.0), vec2f(1.0));"
BuiltinShaderAddress::Repeat =>
"// address: repeat\n let sample_uv = fract(uv);"
BuiltinShaderAddress::MirrorRepeat =>
"// address: mirror_repeat\n let sample_uv = abs(fract(uv * 0.5) * 2.0 - 1.0);"
}
}
///|
/// Generate per-axis address snippet for SamplerSpec.
fn builtin_address_snippet_per_axis(spec : SamplerSpec) -> String {
if builtin_address_eq(spec.address_u, spec.address_v) {
builtin_address_snippet(spec.address_u)
} else {
let u_snippet = single_axis_address_expr(spec.address_u, "uv.x")
let v_snippet = single_axis_address_expr(spec.address_v, "uv.y")
"let sample_uv = vec2f(\{u_snippet}, \{v_snippet});"
}
}
///|
fn single_axis_address_expr(
address : BuiltinShaderAddress,
coord : String,
) -> String {
match address {
BuiltinShaderAddress::Unsafe => coord
BuiltinShaderAddress::ClampToZero =>
"select(0.0, \{coord}, \{coord} >= 0.0 && \{coord} <= 1.0)"
BuiltinShaderAddress::ClampToEdge => "clamp(\{coord}, 0.0, 1.0)"
BuiltinShaderAddress::Repeat => "fract(\{coord})"
BuiltinShaderAddress::MirrorRepeat =>
"abs(fract(\{coord} * 0.5) * 2.0 - 1.0)"
}
}
///|
fn builtin_sample_snippet(filter : BuiltinShaderFilter) -> String {
match filter {
BuiltinShaderFilter::Nearest =>
"textureSampleLevel(tex, nearest_sampler, sample_uv, 0.0)"
BuiltinShaderFilter::Linear =>
"textureSample(tex, linear_sampler, sample_uv)"
BuiltinShaderFilter::Pixelated =>
"textureSampleLevel(tex, nearest_sampler, floor(sample_uv * vec2f(256.0, 256.0)) / vec2f(256.0, 256.0), 0.0)"
}
}
///|
fn builtin_color_m_snippet(use_color_m : Bool) -> String {
if use_color_m {
"let color_m = mat4x4f(\n" +
" vec4f(1.0, 0.0, 0.0, 0.0),\n" +
" vec4f(0.0, 1.0, 0.0, 0.0),\n" +
" vec4f(0.0, 0.0, 1.0, 0.0),\n" +
" vec4f(0.0, 0.0, 0.0, 1.0),\n" +
" );\n" +
" color = color_m * color;"
} else {
"// color matrix disabled"
}
}
///|
fn build_builtin_shader_source(key : BuiltinShaderKey) -> String {
let address_snippet = builtin_address_snippet(key.address)
let sample_snippet = builtin_sample_snippet(key.filter)
let color_m_snippet = builtin_color_m_snippet(key.use_color_m)
let color_m_tag = if key.use_color_m { "on" } else { "off" }
let header = "// kagura builtin shader\n" +
"// filter:\{builtin_filter_tag(key.filter)}\n" +
"// address:\{builtin_address_tag(key.address)}\n" +
"// color_m:\{color_m_tag}\n"
let bindings = "@group(0) @binding(0) var tex: texture_2d;\n" +
"@group(0) @binding(1) var nearest_sampler: sampler;\n" +
"@group(0) @binding(2) var linear_sampler: sampler;\n\n"
let structs = "struct VertexOutput {\n" +
" @builtin(position) pos: vec4f,\n" +
" @location(0) uv: vec2f,\n" +
"};\n\n"
let body = "@fragment\n" +
"fn fs_main(in: VertexOutput) -> @location(0) vec4f {\n" +
" let uv = in.uv;\n" +
" \{address_snippet}\n" +
" var color = \{sample_snippet};\n" +
" \{color_m_snippet}\n" +
" return color;\n" +
"}\n"
header + bindings + structs + body
}
///|
/// Build shader source from extended key with per-axis address support.
pub fn build_builtin_shader_source_ex(key : BuiltinShaderKeyEx) -> String {
let address_snippet = builtin_address_snippet_per_axis(key.sampler)
let sample_snippet = builtin_sample_snippet(key.sampler.filter)
let color_m_snippet = builtin_color_m_snippet(key.use_color_m)
let color_m_tag = if key.use_color_m { "on" } else { "off" }
let header = "// kagura builtin shader\n" +
"// filter:\{builtin_filter_tag(key.sampler.filter)}\n" +
"// address_u:\{builtin_address_tag(key.sampler.address_u)}\n" +
"// address_v:\{builtin_address_tag(key.sampler.address_v)}\n" +
"// color_m:\{color_m_tag}\n"
let bindings = "@group(0) @binding(0) var tex: texture_2d;\n" +
"@group(0) @binding(1) var nearest_sampler: sampler;\n" +
"@group(0) @binding(2) var linear_sampler: sampler;\n\n"
let structs = "struct VertexOutput {\n" +
" @builtin(position) pos: vec4f,\n" +
" @location(0) uv: vec2f,\n" +
"};\n\n"
let body = "@fragment\n" +
"fn fs_main(in: VertexOutput) -> @location(0) vec4f {\n" +
" let uv = in.uv;\n" +
" \{address_snippet}\n" +
" var color = \{sample_snippet};\n" +
" \{color_m_snippet}\n" +
" return color;\n" +
"}\n"
header + bindings + structs + body
}
///|
pub impl BuiltinShaderSourceRepo for BasicBuiltinShaderSourceRepo with fn shader_source(
self,
key,
) {
self.tick = self.tick + 1
match find_builtin_shader_cache_index(self.cache, key) {
Some(index) => {
let entry = self.cache[index]
self.cache[index] = {
key: entry.key,
source: entry.source,
last_used_tick: self.tick,
}
self.hit_count = self.hit_count + 1
entry.source
}
None => {
let source = build_builtin_shader_source(key)
self.cache.push({ key, source, last_used_tick: self.tick })
self.miss_count = self.miss_count + 1
trim_builtin_shader_cache(self)
source
}
}
}
///|
pub impl BuiltinShaderSourceRepo for BasicBuiltinShaderSourceRepo with fn shader_source_ex(
self,
key,
) {
self.tick = self.tick + 1
match find_builtin_shader_cache_ex_index(self.cache_ex, key) {
Some(index) => {
let entry = self.cache_ex[index]
self.cache_ex[index] = {
key: entry.key,
source: entry.source,
last_used_tick: self.tick,
}
self.hit_count = self.hit_count + 1
entry.source
}
None => {
let source = build_builtin_shader_source_ex(key)
self.cache_ex.push({ key, source, last_used_tick: self.tick })
self.miss_count = self.miss_count + 1
trim_builtin_shader_cache_ex(self)
source
}
}
}
///|
pub fn BuiltinShaderFilter::to_int(self : BuiltinShaderFilter) -> Int {
match self {
BuiltinShaderFilter::Nearest => 0
BuiltinShaderFilter::Linear => 1
BuiltinShaderFilter::Pixelated => 2
}
}
///|
pub fn BuiltinShaderFilter::from_int(value : Int) -> BuiltinShaderFilter {
match value {
1 => BuiltinShaderFilter::Linear
2 => BuiltinShaderFilter::Pixelated
_ => BuiltinShaderFilter::Nearest
}
}
///|
pub fn BuiltinShaderAddress::to_int(self : BuiltinShaderAddress) -> Int {
match self {
BuiltinShaderAddress::Unsafe => 0
BuiltinShaderAddress::ClampToZero => 1
BuiltinShaderAddress::ClampToEdge => 2
BuiltinShaderAddress::Repeat => 3
BuiltinShaderAddress::MirrorRepeat => 4
}
}
///|
pub fn BuiltinShaderAddress::from_int(value : Int) -> BuiltinShaderAddress {
match value {
1 => BuiltinShaderAddress::ClampToZero
2 => BuiltinShaderAddress::ClampToEdge
3 => BuiltinShaderAddress::Repeat
4 => BuiltinShaderAddress::MirrorRepeat
_ => BuiltinShaderAddress::Unsafe
}
}
///|
/// Returns a minimal valid builtin shader source for the default 2D pipeline.
/// Use this instead of passing arbitrary strings (e.g. game title) to new_shader.
pub fn default_builtin_shader_source() -> String {
build_builtin_shader_source({
filter: BuiltinShaderFilter::Nearest,
address: BuiltinShaderAddress::Unsafe,
use_color_m: false,
})
}
///|
pub fn default_shader_entrypoints() -> ShaderEntrypoints {
{ vertex: "vs_main", fragment: "fs_main" }
}
///|
pub fn parse_kage_noperspective_directive(source : String) -> Bool {
let chars = source.to_array()
let directive = "//kage:noperspective".to_array()
let chars_len = chars.length()
let directive_len = directive.length()
if chars_len < directive_len {
false
} else {
for i in 0..<(chars_len - directive_len + 1) {
if char_array_starts_with(chars, i, directive) {
// Check that it's at line start or preceded by newline
let at_start = i == 0 || chars[i - 1] == '\n'
if at_start {
return true
}
}
}
false
}
}