///|
/// Runtime residency and streaming budget estimates.
///
/// A build service needs a conservative memory estimate before uploading an
/// asset. These values are derived from declared layouts and analysis results;
/// they do not assume a particular graphics API format.
///|
pub(all) struct RuntimeBudget {
max_vertex_bytes : Int
max_index_bytes : Int
max_texture_bytes : Int
max_animation_bytes : Int
max_skin_bytes : Int
max_total_bytes : Int
max_stream_chunks : Int
} derive(Debug, ToJson, Eq)
///|
pub(all) struct RuntimeEstimate {
vertex_bytes : Int
index_bytes : Int
texture_bytes : Int
animation_bytes : Int
skin_bytes : Int
metadata_bytes : Int
total_bytes : Int
resident_bytes : Int
streamable_bytes : Int
stream_chunks : Int
peak_bytes : Int
} derive(Debug, ToJson, Eq)
///|
pub(all) struct RuntimeBudgetResult {
passed : Bool
violations : Array[String]
vertex_overage : Int
index_overage : Int
texture_overage : Int
animation_overage : Int
skin_overage : Int
total_overage : Int
chunk_overage : Int
} derive(Debug, ToJson, Eq)
///|
pub(all) struct StreamChunk {
index : Int
offset : Int
length : Int
kind : String
resident : Bool
dependencies : Array[Int]
valid : Bool
} derive(Debug, ToJson, Eq)
///|
pub fn default_runtime_budget() -> RuntimeBudget {
{
max_vertex_bytes: 256 * 1024 * 1024,
max_index_bytes: 128 * 1024 * 1024,
max_texture_bytes: 512 * 1024 * 1024,
max_animation_bytes: 128 * 1024 * 1024,
max_skin_bytes: 64 * 1024 * 1024,
max_total_bytes: 1024 * 1024 * 1024,
max_stream_chunks: 4096,
}
}
///|
pub fn mobile_runtime_budget() -> RuntimeBudget {
{
max_vertex_bytes: 64 * 1024 * 1024,
max_index_bytes: 32 * 1024 * 1024,
max_texture_bytes: 256 * 1024 * 1024,
max_animation_bytes: 32 * 1024 * 1024,
max_skin_bytes: 16 * 1024 * 1024,
max_total_bytes: 384 * 1024 * 1024,
max_stream_chunks: 1024,
}
}
///|
pub fn runtime_estimate(profile : ProductionProfile) -> RuntimeEstimate {
let vertex = profile.geometry.attribute_bytes
let index = profile.geometry.index_count * 4
let texture = profile.resources.resources.fold(init=0, (sum, resource) => {
if resource.kind == Image || resource.kind == EmbeddedImage {
sum + resource.declared_bytes
} else {
sum
}
})
let animation = profile.animations.fold(init=0, (sum, report) => {
sum +
report.tracks.fold(init=0, (inner, track) => {
inner + track.values.length() * 4 + track.times.length() * 4
})
})
let skin = profile.skins.fold(init=0, (sum, report) => {
sum + report.joint_count * 64
})
let metadata = profile.metrics.scene_nodes * 64 +
profile.metrics.materials * 256
let total = vertex + index + texture + animation + skin + metadata
let streamable = if profile.resources.external_count > 0 {
profile.resources.declared_bytes
} else {
0
}
{
vertex_bytes: vertex,
index_bytes: index,
texture_bytes: texture,
animation_bytes: animation,
skin_bytes: skin,
metadata_bytes: metadata,
total_bytes: total,
resident_bytes: total - streamable,
streamable_bytes: streamable,
stream_chunks: profile.resources.resources.length(),
peak_bytes: total,
}
}
///|
pub fn runtime_budget_result(
estimate : RuntimeEstimate,
budget : RuntimeBudget,
) -> RuntimeBudgetResult {
let vertex_overage = if estimate.vertex_bytes > budget.max_vertex_bytes {
estimate.vertex_bytes - budget.max_vertex_bytes
} else {
0
}
let index_overage = if estimate.index_bytes > budget.max_index_bytes {
estimate.index_bytes - budget.max_index_bytes
} else {
0
}
let texture_overage = if estimate.texture_bytes > budget.max_texture_bytes {
estimate.texture_bytes - budget.max_texture_bytes
} else {
0
}
let animation_overage = if estimate.animation_bytes >
budget.max_animation_bytes {
estimate.animation_bytes - budget.max_animation_bytes
} else {
0
}
let skin_overage = if estimate.skin_bytes > budget.max_skin_bytes {
estimate.skin_bytes - budget.max_skin_bytes
} else {
0
}
let total_overage = if estimate.total_bytes > budget.max_total_bytes {
estimate.total_bytes - budget.max_total_bytes
} else {
0
}
let chunk_overage = if estimate.stream_chunks > budget.max_stream_chunks {
estimate.stream_chunks - budget.max_stream_chunks
} else {
0
}
let violations : Array[String] = []
if vertex_overage > 0 {
violations.push("vertex memory budget exceeded")
}
if index_overage > 0 {
violations.push("index memory budget exceeded")
}
if texture_overage > 0 {
violations.push("texture memory budget exceeded")
}
if animation_overage > 0 {
violations.push("animation memory budget exceeded")
}
if skin_overage > 0 {
violations.push("skin memory budget exceeded")
}
if total_overage > 0 {
violations.push("total runtime memory budget exceeded")
}
if chunk_overage > 0 {
violations.push("stream chunk budget exceeded")
}
{
passed: violations.length() == 0,
violations,
vertex_overage,
index_overage,
texture_overage,
animation_overage,
skin_overage,
total_overage,
chunk_overage,
}
}
///|
pub fn build_stream_chunks(
profile : ProductionProfile,
chunk_size : Int,
) -> Array[StreamChunk] {
let safe_size = if chunk_size < 1 { 1 } else { chunk_size }
let chunks : Array[StreamChunk] = []
let mut offset = 0
let mut index = 0
for resource in profile.resources.resources {
let length = if resource.declared_bytes <= 0 {
1
} else {
resource.declared_bytes
}
let mut consumed = 0
while consumed < length {
let remaining = length - consumed
let size = if remaining < safe_size { remaining } else { safe_size }
chunks.push({
index,
offset,
length: size,
kind: resource_kind_name(resource.kind),
resident: !resource.external,
dependencies: if index == 0 {
[]
} else {
[index - 1]
},
valid: size > 0,
})
offset += size
consumed += size
index += 1
}
}
chunks
}
///|
pub fn stream_chunk_total_bytes(chunks : Array[StreamChunk]) -> Int {
chunks.fold(init=0, (sum, chunk) => sum + chunk.length)
}
///|
pub fn stream_chunk_resident_bytes(chunks : Array[StreamChunk]) -> Int {
chunks.fold(init=0, (sum, chunk) => {
if chunk.resident {
sum + chunk.length
} else {
sum
}
})
}
///|
pub fn stream_chunk_summary_lines(chunks : Array[StreamChunk]) -> Array[String] {
[
"chunks=\{chunks.length()}, total_bytes=\{stream_chunk_total_bytes(chunks)}",
"resident_bytes=\{stream_chunk_resident_bytes(chunks)}",
]
}