1use crate::error::{Error, ErrorCategory, Result};
8use crate::fidelity::FidelityLevel;
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::Path;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum CapabilityProvenance {
17 Verified,
19 Observed,
21 Inferred,
23 Unknown,
25 Provisional,
27}
28
29impl CapabilityProvenance {
30 pub fn as_str(self) -> &'static str {
31 match self {
32 Self::Verified => "verified",
33 Self::Observed => "observed",
34 Self::Inferred => "inferred",
35 Self::Unknown => "unknown",
36 Self::Provisional => "provisional",
37 }
38 }
39
40 pub fn allows_conformance(self) -> bool {
42 matches!(self, Self::Verified | Self::Observed)
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "kebab-case")]
49pub enum SupportState {
50 ImplementedUnverified,
51 VerifiedUnit,
52 VerifiedIntegration,
53 HardwareDifferential,
54 Experimental,
55 Unsupported,
56}
57
58impl SupportState {
59 pub fn as_str(self) -> &'static str {
60 match self {
61 Self::ImplementedUnverified => "implemented-unverified",
62 Self::VerifiedUnit => "verified-unit",
63 Self::VerifiedIntegration => "verified-integration",
64 Self::HardwareDifferential => "hardware-differential",
65 Self::Experimental => "experimental",
66 Self::Unsupported => "unsupported",
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct ProfileField<T> {
74 pub value: Option<T>,
75 pub provenance: CapabilityProvenance,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub source_ref: Option<String>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub notes: Option<String>,
80}
81
82impl<T> ProfileField<T> {
83 pub fn unknown() -> Self {
84 Self {
85 value: None,
86 provenance: CapabilityProvenance::Unknown,
87 source_ref: None,
88 notes: Some("not established; do not invent".into()),
89 }
90 }
91
92 pub fn verified(value: T, source_ref: impl Into<String>) -> Self {
93 Self {
94 value: Some(value),
95 provenance: CapabilityProvenance::Verified,
96 source_ref: Some(source_ref.into()),
97 notes: None,
98 }
99 }
100
101 pub fn provisional(value: T, reason: impl Into<String>) -> Self {
102 Self {
103 value: Some(value),
104 provenance: CapabilityProvenance::Provisional,
105 source_ref: None,
106 notes: Some(reason.into()),
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct ProfileIdentity {
114 pub vendor: String,
115 pub product_name: String,
116 pub architecture_family: String,
117 pub llvm_target: ProfileField<String>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct DeviceProfile {
123 pub schema_version: u32,
124 pub profile_id: String,
125 pub profile_revision: String,
126 pub identity: ProfileIdentity,
127 pub max_fidelity: FidelityLevel,
129 pub conformance_allowed: bool,
131 pub resource_limits: ResourceLimits,
133 #[serde(default)]
135 pub analytical_performance: AnalyticalPerformanceParams,
136 #[serde(default)]
137 pub quirks: Vec<String>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
141pub struct ResourceLimits {
142 pub max_workgroup_size_x: ProfileField<u32>,
143 pub max_workgroup_size_y: ProfileField<u32>,
144 pub max_workgroup_size_z: ProfileField<u32>,
145 pub wavefront_size: ProfileField<u32>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
149pub struct AnalyticalPerformanceParams {
150 #[serde(default)]
152 pub notes: Vec<String>,
153}
154
155impl Default for ProfileField<u32> {
156 fn default() -> Self {
157 Self::unknown()
158 }
159}
160
161impl DeviceProfile {
162 pub const CURRENT_SCHEMA_VERSION: u32 = 1;
163
164 pub fn load_path(path: impl AsRef<Path>) -> Result<Self> {
166 let bytes = fs::read(path.as_ref())?;
167 Self::parse_bytes(&bytes)
168 }
169
170 pub fn parse_bytes(bytes: &[u8]) -> Result<Self> {
172 let profile: DeviceProfile = serde_json::from_slice(bytes)?;
173 profile.validate()?;
174 Ok(profile)
175 }
176
177 pub fn validate(&self) -> Result<()> {
179 if self.schema_version != Self::CURRENT_SCHEMA_VERSION {
180 return Err(Error::new(
181 ErrorCategory::Profile,
182 format!(
183 "unsupported profile schema_version {}; expected {}",
184 self.schema_version,
185 Self::CURRENT_SCHEMA_VERSION
186 ),
187 )
188 .with_remediation("migrate the profile or use a SoftGPU build that supports it"));
189 }
190
191 if self.profile_id.trim().is_empty() {
192 return Err(Error::new(
193 ErrorCategory::Profile,
194 "profile_id must be non-empty",
195 ));
196 }
197
198 if self.identity.vendor.trim().is_empty()
199 || self.identity.product_name.trim().is_empty()
200 || self.identity.architecture_family.trim().is_empty()
201 {
202 return Err(Error::new(
203 ErrorCategory::Profile,
204 "identity.vendor/product_name/architecture_family must be non-empty",
205 ));
206 }
207
208 if self.conformance_allowed {
209 if self.max_fidelity == FidelityLevel::HardwareConformant {
210 return Err(Error::new(
211 ErrorCategory::Profile,
212 "conformance_allowed cannot be true for hardware-conformant max_fidelity until Phase 12 evidence exists",
213 )
214 .with_remediation("set conformance_allowed=false or lower max_fidelity"));
215 }
216 for (name, field) in [
218 (
219 "max_workgroup_size_x",
220 &self.resource_limits.max_workgroup_size_x,
221 ),
222 (
223 "max_workgroup_size_y",
224 &self.resource_limits.max_workgroup_size_y,
225 ),
226 (
227 "max_workgroup_size_z",
228 &self.resource_limits.max_workgroup_size_z,
229 ),
230 ("wavefront_size", &self.resource_limits.wavefront_size),
231 ] {
232 if !field.provenance.allows_conformance() {
233 return Err(Error::new(
234 ErrorCategory::Profile,
235 format!(
236 "conformance_allowed requires verified/observed provenance for {name}; found {}",
237 field.provenance.as_str()
238 ),
239 ));
240 }
241 }
242 if !self.identity.llvm_target.provenance.allows_conformance() {
243 return Err(Error::new(
244 ErrorCategory::Profile,
245 format!(
246 "conformance_allowed requires verified/observed llvm_target; found {}",
247 self.identity.llvm_target.provenance.as_str()
248 ),
249 ));
250 }
251 }
252
253 if let Some(target) = &self.identity.llvm_target.value {
255 if target.trim().is_empty() {
256 return Err(Error::new(
257 ErrorCategory::Profile,
258 "identity.llvm_target.value must not be an empty string when present",
259 ));
260 }
261 }
262
263 Ok(())
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn generic_profile() -> DeviceProfile {
272 DeviceProfile {
273 schema_version: 1,
274 profile_id: "softgpu-generic".into(),
275 profile_revision: "0".into(),
276 identity: ProfileIdentity {
277 vendor: "SoftGPU".into(),
278 product_name: "Generic Test Device".into(),
279 architecture_family: "softgpu-abstract".into(),
280 llvm_target: ProfileField::unknown(),
281 },
282 max_fidelity: FidelityLevel::Abi,
283 conformance_allowed: false,
284 resource_limits: ResourceLimits::default(),
285 analytical_performance: AnalyticalPerformanceParams::default(),
286 quirks: vec![],
287 }
288 }
289
290 #[test]
291 fn valid_generic_profile_passes() {
292 generic_profile().validate().unwrap();
293 }
294
295 #[test]
296 fn wrong_schema_version_fails() {
297 let mut p = generic_profile();
298 p.schema_version = 99;
299 let err = p.validate().unwrap_err();
300 assert_eq!(err.category(), ErrorCategory::Profile);
301 assert!(err.message().contains("schema_version"));
302 }
303
304 #[test]
305 fn conformance_forbidden_with_unknown_limits() {
306 let mut p = generic_profile();
307 p.conformance_allowed = true;
308 p.identity.llvm_target =
309 ProfileField::verified("gfx1201".into(), "docs/sources.md#llvm-amdgpu");
310 let err = p.validate().unwrap_err();
311 assert!(err.message().contains("conformance_allowed"));
312 }
313
314 #[test]
315 fn empty_llvm_target_value_rejected() {
316 let mut p = generic_profile();
317 p.identity.llvm_target = ProfileField {
318 value: Some("".into()),
319 provenance: CapabilityProvenance::Provisional,
320 source_ref: None,
321 notes: Some("bad".into()),
322 };
323 let err = p.validate().unwrap_err();
324 assert!(err.message().contains("llvm_target"));
325 }
326}