Compare commits
2 Commits
efab61a45c
...
7346e695c4
| Author | SHA1 | Date | |
|---|---|---|---|
|
7346e695c4
|
|||
|
bb827c3928
|
@@ -1,4 +1,6 @@
|
|||||||
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// Resource payload that can be either borrowed from mapped bytes or owned.
|
/// Resource payload that can be either borrowed from mapped bytes or owned.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -42,3 +44,18 @@ impl OutputBuffer for Vec<u8> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively collects all files under `root`.
|
||||||
|
pub fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
|
||||||
|
let Ok(entries) = fs::read_dir(root) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
collect_files_recursive(&path, out);
|
||||||
|
} else if path.is_file() {
|
||||||
|
out.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,4 +4,9 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
encoding_rs = "0.8"
|
||||||
nres = { path = "../nres" }
|
nres = { path = "../nres" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
proptest = "1"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
use encoding_rs::WINDOWS_1251;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
@@ -347,13 +348,18 @@ fn parse_res10_names(data: &[u8], node_count: usize) -> Result<Vec<Option<String
|
|||||||
} else {
|
} else {
|
||||||
slice
|
slice
|
||||||
};
|
};
|
||||||
let decoded = String::from_utf8_lossy(text).to_string();
|
let decoded = decode_cp1251(text);
|
||||||
out.push(Some(decoded));
|
out.push(Some(decoded));
|
||||||
off = end;
|
off = end;
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_cp1251(bytes: &[u8]) -> String {
|
||||||
|
let (decoded, _, _) = WINDOWS_1251.decode(bytes);
|
||||||
|
decoded.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
struct RawResource {
|
struct RawResource {
|
||||||
meta: nres::EntryMeta,
|
meta: nres::EntryMeta,
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use common::collect_files_recursive;
|
||||||
use nres::Archive;
|
use nres::Archive;
|
||||||
|
use proptest::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
|
|
||||||
let Ok(entries) = fs::read_dir(root) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
collect_files_recursive(&path, out);
|
|
||||||
} else if path.is_file() {
|
|
||||||
out.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nres_test_files() -> Vec<PathBuf> {
|
fn nres_test_files() -> Vec<PathBuf> {
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("..")
|
.join("..")
|
||||||
@@ -169,18 +157,17 @@ fn res13_single_batch(index_start: u32, index_count: u16) -> Vec<u8> {
|
|||||||
batch
|
batch
|
||||||
}
|
}
|
||||||
|
|
||||||
fn res10_names(names: &[Option<&str>]) -> Vec<u8> {
|
fn res10_names_raw(names: &[Option<&[u8]>]) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for name in names {
|
for name in names {
|
||||||
match name {
|
match name {
|
||||||
Some(name) => {
|
Some(name) => {
|
||||||
let bytes = name.as_bytes();
|
|
||||||
out.extend_from_slice(
|
out.extend_from_slice(
|
||||||
&u32::try_from(bytes.len())
|
&u32::try_from(name.len())
|
||||||
.expect("name size overflow in test")
|
.expect("name size overflow in test")
|
||||||
.to_le_bytes(),
|
.to_le_bytes(),
|
||||||
);
|
);
|
||||||
out.extend_from_slice(bytes);
|
out.extend_from_slice(name);
|
||||||
out.push(0);
|
out.push(0);
|
||||||
}
|
}
|
||||||
None => out.extend_from_slice(&0u32.to_le_bytes()),
|
None => out.extend_from_slice(&0u32.to_le_bytes()),
|
||||||
@@ -189,6 +176,11 @@ fn res10_names(names: &[Option<&str>]) -> Vec<u8> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn res10_names(names: &[Option<&str>]) -> Vec<u8> {
|
||||||
|
let raw: Vec<Option<&[u8]>> = names.iter().map(|name| name.map(str::as_bytes)).collect();
|
||||||
|
res10_names_raw(&raw)
|
||||||
|
}
|
||||||
|
|
||||||
fn base_synthetic_entries() -> Vec<SyntheticEntry> {
|
fn base_synthetic_entries() -> Vec<SyntheticEntry> {
|
||||||
vec![
|
vec![
|
||||||
synthetic_entry(RES1_NODE_TABLE, "Res1", 38, res1_stride38_nodes(1, Some(0))),
|
synthetic_entry(RES1_NODE_TABLE, "Res1", 38, res1_stride38_nodes(1, Some(0))),
|
||||||
@@ -339,6 +331,22 @@ fn parse_synthetic_model_with_optional_res4_res5_res10() {
|
|||||||
assert_eq!(model.node_names, Some(vec![Some("Hull".to_string()), None]));
|
assert_eq!(model.node_names, Some(vec![Some("Hull".to_string()), None]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_res10_names_decodes_cp1251() {
|
||||||
|
let mut entries = base_synthetic_entries();
|
||||||
|
entries[0] = synthetic_entry(RES1_NODE_TABLE, "Res1", 38, res1_stride38_nodes(1, Some(0)));
|
||||||
|
entries.push(synthetic_entry(
|
||||||
|
RES10_NAMES,
|
||||||
|
"Res10",
|
||||||
|
1,
|
||||||
|
res10_names_raw(&[Some(&[0xC0])]),
|
||||||
|
));
|
||||||
|
let payload = build_nested_nres(&entries);
|
||||||
|
|
||||||
|
let model = parse_model_payload(&payload).expect("failed to parse model with cp1251 name");
|
||||||
|
assert_eq!(model.node_names, Some(vec![Some("А".to_string())]));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_fails_when_required_resource_missing() {
|
fn parse_fails_when_required_resource_missing() {
|
||||||
let mut entries = base_synthetic_entries();
|
let mut entries = base_synthetic_entries();
|
||||||
@@ -419,3 +427,12 @@ fn parse_fails_for_batch_index_range_out_of_bounds() {
|
|||||||
})
|
})
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(64))]
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_model_payload_never_panics_on_random_bytes(data in proptest::collection::vec(any::<u8>(), 0..8192)) {
|
||||||
|
let _ = parse_model_payload(&data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use common::collect_files_recursive;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||||
@@ -13,20 +14,6 @@ struct SyntheticEntry<'a> {
|
|||||||
data: &'a [u8],
|
data: &'a [u8],
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
|
|
||||||
let Ok(entries) = fs::read_dir(root) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
collect_files_recursive(&path, out);
|
|
||||||
} else if path.is_file() {
|
|
||||||
out.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nres_test_files() -> Vec<PathBuf> {
|
fn nres_test_files() -> Vec<PathBuf> {
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("..")
|
.join("..")
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ edition = "2021"
|
|||||||
msh-core = { path = "../msh-core" }
|
msh-core = { path = "../msh-core" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
nres = { path = "../nres" }
|
nres = { path = "../nres" }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use msh_core::Model;
|
use msh_core::Model;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub const DEFAULT_UV_SCALE: f32 = 1024.0;
|
pub const DEFAULT_UV_SCALE: f32 = 1024.0;
|
||||||
|
|
||||||
@@ -11,21 +12,24 @@ pub struct RenderVertex {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RenderMesh {
|
pub struct RenderMesh {
|
||||||
pub vertices: Vec<RenderVertex>,
|
pub vertices: Vec<RenderVertex>,
|
||||||
|
pub indices: Vec<u16>,
|
||||||
pub batch_count: usize,
|
pub batch_count: usize,
|
||||||
|
pub index_overflow: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderMesh {
|
impl RenderMesh {
|
||||||
pub fn triangle_count(&self) -> usize {
|
pub fn triangle_count(&self) -> usize {
|
||||||
self.vertices.len() / 3
|
self.indices.len() / 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds an expanded triangle list for a specific LOD/group pair.
|
/// Builds an indexed triangle mesh for a specific LOD/group pair.
|
||||||
///
|
|
||||||
/// The output is suitable for simple `glDrawArrays(GL_TRIANGLES, ...)` paths.
|
|
||||||
pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh {
|
pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh {
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
|
let mut indices = Vec::new();
|
||||||
|
let mut index_remap: HashMap<usize, u16> = HashMap::new();
|
||||||
let mut batch_count = 0usize;
|
let mut batch_count = 0usize;
|
||||||
|
let mut index_overflow = false;
|
||||||
let uv0 = model.uv0.as_ref();
|
let uv0 = model.uv0.as_ref();
|
||||||
|
|
||||||
for node_index in 0..model.node_count {
|
for node_index in 0..model.node_count {
|
||||||
@@ -49,13 +53,26 @@ pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let batch_out_start = indices.len();
|
||||||
|
let mut batch_valid = true;
|
||||||
for &idx in &model.indices[index_start..index_end] {
|
for &idx in &model.indices[index_start..index_end] {
|
||||||
let final_idx_u64 = u64::from(batch.base_vertex).saturating_add(u64::from(idx));
|
let final_idx_u64 = u64::from(batch.base_vertex).saturating_add(u64::from(idx));
|
||||||
let Ok(final_idx) = usize::try_from(final_idx_u64) else {
|
let Ok(final_idx) = usize::try_from(final_idx_u64) else {
|
||||||
continue;
|
batch_valid = false;
|
||||||
|
break;
|
||||||
};
|
};
|
||||||
let Some(pos) = model.positions.get(final_idx) else {
|
let Some(pos) = model.positions.get(final_idx) else {
|
||||||
continue;
|
batch_valid = false;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let local_index = if let Some(&mapped) = index_remap.get(&final_idx) {
|
||||||
|
mapped
|
||||||
|
} else {
|
||||||
|
let Ok(mapped) = u16::try_from(vertices.len()) else {
|
||||||
|
index_overflow = true;
|
||||||
|
batch_valid = false;
|
||||||
|
break;
|
||||||
};
|
};
|
||||||
let uv = uv0
|
let uv = uv0
|
||||||
.and_then(|uvs| uvs.get(final_idx))
|
.and_then(|uvs| uvs.get(final_idx))
|
||||||
@@ -71,14 +88,27 @@ pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh
|
|||||||
position: *pos,
|
position: *pos,
|
||||||
uv0: uv,
|
uv0: uv,
|
||||||
});
|
});
|
||||||
|
index_remap.insert(final_idx, mapped);
|
||||||
|
mapped
|
||||||
|
};
|
||||||
|
|
||||||
|
indices.push(local_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !batch_valid {
|
||||||
|
indices.truncate(batch_out_start);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
batch_count += 1;
|
batch_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderMesh {
|
RenderMesh {
|
||||||
vertices,
|
vertices,
|
||||||
|
indices,
|
||||||
batch_count,
|
batch_count,
|
||||||
|
index_overflow,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,10 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use common::collect_files_recursive;
|
||||||
use msh_core::parse_model_payload;
|
use msh_core::parse_model_payload;
|
||||||
use nres::Archive;
|
use nres::Archive;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
|
|
||||||
let Ok(entries) = fs::read_dir(root) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
collect_files_recursive(&path, out);
|
|
||||||
} else if path.is_file() {
|
|
||||||
out.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nres_test_files() -> Vec<PathBuf> {
|
fn nres_test_files() -> Vec<PathBuf> {
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("..")
|
.join("..")
|
||||||
@@ -71,12 +58,20 @@ fn build_render_mesh_for_real_models() {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
let mesh = build_render_mesh(&model, 0, 0);
|
let mesh = build_render_mesh(&model, 0, 0);
|
||||||
if !mesh.vertices.is_empty() {
|
if !mesh.indices.is_empty() {
|
||||||
meshes_non_empty += 1;
|
meshes_non_empty += 1;
|
||||||
}
|
}
|
||||||
if compute_bounds_for_mesh(&mesh.vertices).is_some() {
|
if compute_bounds_for_mesh(&mesh.vertices).is_some() {
|
||||||
bounds_non_empty += 1;
|
bounds_non_empty += 1;
|
||||||
}
|
}
|
||||||
|
for &index in &mesh.indices {
|
||||||
|
assert!(
|
||||||
|
usize::from(index) < mesh.vertices.len(),
|
||||||
|
"index out of bounds for '{}' in {}",
|
||||||
|
entry.meta.name,
|
||||||
|
archive_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
for vertex in &mesh.vertices {
|
for vertex in &mesh.vertices {
|
||||||
assert!(
|
assert!(
|
||||||
vertex.uv0[0].is_finite() && vertex.uv0[1].is_finite(),
|
vertex.uv0[0].is_finite() && vertex.uv0[1].is_finite(),
|
||||||
@@ -189,6 +184,7 @@ fn build_render_mesh_handles_empty_slot_model() {
|
|||||||
|
|
||||||
let mesh = build_render_mesh(&model, 0, 0);
|
let mesh = build_render_mesh(&model, 0, 0);
|
||||||
assert!(mesh.vertices.is_empty());
|
assert!(mesh.vertices.is_empty());
|
||||||
|
assert!(mesh.indices.is_empty());
|
||||||
assert_eq!(mesh.batch_count, 0);
|
assert_eq!(mesh.batch_count, 0);
|
||||||
assert_eq!(mesh.triangle_count(), 0);
|
assert_eq!(mesh.triangle_count(), 0);
|
||||||
}
|
}
|
||||||
@@ -225,9 +221,36 @@ fn build_render_mesh_supports_multi_node_and_uv_scaling() {
|
|||||||
let mesh = build_render_mesh(&model, 0, 0);
|
let mesh = build_render_mesh(&model, 0, 0);
|
||||||
assert_eq!(mesh.batch_count, 2);
|
assert_eq!(mesh.batch_count, 2);
|
||||||
assert_eq!(mesh.vertices.len(), 6);
|
assert_eq!(mesh.vertices.len(), 6);
|
||||||
|
assert_eq!(mesh.indices, vec![0, 1, 2, 3, 4, 5]);
|
||||||
assert_eq!(mesh.triangle_count(), 2);
|
assert_eq!(mesh.triangle_count(), 2);
|
||||||
assert_eq!(mesh.vertices[0].uv0, [1.0, -1.0]);
|
assert_eq!(mesh.vertices[0].uv0, [1.0, -1.0]);
|
||||||
assert_eq!(mesh.vertices[1].uv0, [0.5, 0.25]);
|
assert_eq!(mesh.vertices[1].uv0, [0.5, 0.25]);
|
||||||
assert_eq!(mesh.vertices[2].uv0, [0.0, 0.0]);
|
assert_eq!(mesh.vertices[2].uv0, [0.0, 0.0]);
|
||||||
assert_eq!(mesh.vertices[3].uv0, [1.0, 1.0]);
|
assert_eq!(mesh.vertices[3].uv0, [1.0, 1.0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_render_mesh_deduplicates_shared_vertices() {
|
||||||
|
let model = msh_core::Model {
|
||||||
|
node_stride: 38,
|
||||||
|
node_count: 1,
|
||||||
|
nodes_raw: nodes_with_slot_refs(&[Some(0)]),
|
||||||
|
slots: vec![slot(0, 1)],
|
||||||
|
positions: vec![
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[0.0, 1.0, 0.0],
|
||||||
|
[1.0, 1.0, 0.0],
|
||||||
|
],
|
||||||
|
normals: None,
|
||||||
|
uv0: None,
|
||||||
|
indices: vec![0, 1, 2, 2, 1, 3],
|
||||||
|
batches: vec![batch(0, 6, 0)],
|
||||||
|
node_names: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mesh = build_render_mesh(&model, 0, 0);
|
||||||
|
assert_eq!(mesh.vertices.len(), 4);
|
||||||
|
assert_eq!(mesh.indices, vec![0, 1, 2, 2, 1, 3]);
|
||||||
|
assert_eq!(mesh.triangle_count(), 2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,10 +13,18 @@ msh-core = { path = "../msh-core" }
|
|||||||
nres = { path = "../nres" }
|
nres = { path = "../nres" }
|
||||||
render-core = { path = "../render-core" }
|
render-core = { path = "../render-core" }
|
||||||
texm = { path = "../texm" }
|
texm = { path = "../texm" }
|
||||||
sdl2 = { version = "0.37", optional = true, default-features = false, features = ["bundled", "static-link"] }
|
|
||||||
glow = { version = "0.16", optional = true }
|
glow = { version = "0.16", optional = true }
|
||||||
image = { version = "0.25", optional = true, default-features = false, features = ["png"] }
|
image = { version = "0.25", optional = true, default-features = false, features = ["png"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
sdl2 = { version = "0.37", optional = true, default-features = false, features = ["use-pkgconfig"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||||
|
sdl2 = { version = "0.37", optional = true, default-features = false, features = ["bundled", "static-link"] }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "parkan-render-demo"
|
name = "parkan-render-demo"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# render-demo
|
# render-demo
|
||||||
|
|
||||||
Тестовый рендерер Parkan-моделей на Rust (`SDL2 + OpenGL ES 2.0`).
|
Тестовый рендерер Parkan-моделей на Rust (`SDL2 + OpenGL`: GLES2 с fallback на Core 3.3).
|
||||||
|
|
||||||
## Назначение
|
## Назначение
|
||||||
|
|
||||||
@@ -18,6 +18,16 @@ cargo run -p render-demo --features demo -- \
|
|||||||
--group 0
|
--group 0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### macOS prerequisites
|
||||||
|
|
||||||
|
Для macOS `render-demo` ожидает системный SDL2 через `pkg-config`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install sdl2 pkg-config
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого запускайте той же командой `cargo run ... --features demo`.
|
||||||
|
|
||||||
Параметры:
|
Параметры:
|
||||||
|
|
||||||
- `--archive` (обязательный): NRes-архив с `.msh` entry.
|
- `--archive` (обязательный): NRes-архив с `.msh` entry.
|
||||||
@@ -70,4 +80,4 @@ cargo run -p render-demo --features demo -- \
|
|||||||
## Ограничения
|
## Ограничения
|
||||||
|
|
||||||
- Используется только базовая texture-фаза (без полной material/fx анимации).
|
- Используется только базовая texture-фаза (без полной material/fx анимации).
|
||||||
- Вывод через `glDrawArrays(GL_TRIANGLES)` из расширенного triangle-list (позиции+UV).
|
- Вывод через `glDrawElements(GL_TRIANGLES)` с index-buffer (позиции+UV).
|
||||||
|
|||||||
@@ -435,23 +435,10 @@ fn decode_texture_entry(archive: &Archive, entry: EntryRef<'_>) -> Result<Loaded
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use common::collect_files_recursive;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
|
|
||||||
let Ok(entries) = fs::read_dir(root) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
collect_files_recursive(&path, out);
|
|
||||||
} else if path.is_file() {
|
|
||||||
out.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn archive_with_msh() -> Option<PathBuf> {
|
fn archive_with_msh() -> Option<PathBuf> {
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("..")
|
.join("..")
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ struct GpuTexture {
|
|||||||
handle: glow::NativeTexture,
|
handle: glow::NativeTexture,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
enum GlBackend {
|
||||||
|
Gles2,
|
||||||
|
Core33,
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_args() -> Result<Args, String> {
|
fn parse_args() -> Result<Args, String> {
|
||||||
let mut archive = None;
|
let mut archive = None;
|
||||||
let mut model = None;
|
let mut model = None;
|
||||||
@@ -221,12 +227,17 @@ fn run(args: Args) -> Result<(), String> {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let mesh = build_render_mesh(&loaded_model.model, args.lod, args.group);
|
let mesh = build_render_mesh(&loaded_model.model, args.lod, args.group);
|
||||||
if mesh.vertices.is_empty() {
|
if mesh.indices.is_empty() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"model has no renderable triangles for lod={} group={}",
|
"model has no renderable triangles for lod={} group={}",
|
||||||
args.lod, args.group
|
args.lod, args.group
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if mesh.index_overflow {
|
||||||
|
eprintln!(
|
||||||
|
"warning: mesh exceeds u16 index space and may be partially rendered on GLES2 targets"
|
||||||
|
);
|
||||||
|
}
|
||||||
let Some((bounds_min, bounds_max)) = compute_bounds_for_mesh(&mesh.vertices) else {
|
let Some((bounds_min, bounds_max)) = compute_bounds_for_mesh(&mesh.vertices) else {
|
||||||
return Err(String::from("failed to compute mesh bounds"));
|
return Err(String::from("failed to compute mesh bounds"));
|
||||||
};
|
};
|
||||||
@@ -260,35 +271,7 @@ fn run(args: Args) -> Result<(), String> {
|
|||||||
.video()
|
.video()
|
||||||
.map_err(|err| format!("failed to init SDL2 video: {err}"))?;
|
.map_err(|err| format!("failed to init SDL2 video: {err}"))?;
|
||||||
|
|
||||||
{
|
let (window, _gl_ctx, gl_backend) = create_window_and_context(&video, &args)?;
|
||||||
let gl_attr = video.gl_attr();
|
|
||||||
gl_attr.set_context_profile(sdl2::video::GLProfile::GLES);
|
|
||||||
gl_attr.set_context_version(2, 0);
|
|
||||||
gl_attr.set_depth_size(24);
|
|
||||||
gl_attr.set_double_buffer(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut window_builder = video.window(
|
|
||||||
"Parkan Render Demo (SDL2 + OpenGL ES 2.0)",
|
|
||||||
args.width,
|
|
||||||
args.height,
|
|
||||||
);
|
|
||||||
window_builder.opengl();
|
|
||||||
if args.capture.is_some() {
|
|
||||||
window_builder.hidden();
|
|
||||||
} else {
|
|
||||||
window_builder.resizable();
|
|
||||||
}
|
|
||||||
let window = window_builder
|
|
||||||
.build()
|
|
||||||
.map_err(|err| format!("failed to create window: {err}"))?;
|
|
||||||
|
|
||||||
let gl_ctx = window
|
|
||||||
.gl_create_context()
|
|
||||||
.map_err(|err| format!("failed to create OpenGL context: {err}"))?;
|
|
||||||
window
|
|
||||||
.gl_make_current(&gl_ctx)
|
|
||||||
.map_err(|err| format!("failed to make GL context current: {err}"))?;
|
|
||||||
let _ = if args.capture.is_some() {
|
let _ = if args.capture.is_some() {
|
||||||
video.gl_set_swap_interval(0)
|
video.gl_set_swap_interval(0)
|
||||||
} else {
|
} else {
|
||||||
@@ -304,12 +287,13 @@ fn run(args: Args) -> Result<(), String> {
|
|||||||
vertex_data.push(vertex.uv0[1]);
|
vertex_data.push(vertex.uv0[1]);
|
||||||
}
|
}
|
||||||
let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data);
|
let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data);
|
||||||
|
let index_bytes = u16_slice_to_ne_bytes(&mesh.indices);
|
||||||
|
|
||||||
let gl = unsafe {
|
let gl = unsafe {
|
||||||
glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _)
|
glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _)
|
||||||
};
|
};
|
||||||
|
|
||||||
let program = unsafe { create_program(&gl)? };
|
let program = unsafe { create_program(&gl, gl_backend)? };
|
||||||
let u_mvp = unsafe { gl.get_uniform_location(program, "u_mvp") };
|
let u_mvp = unsafe { gl.get_uniform_location(program, "u_mvp") };
|
||||||
let u_use_tex = unsafe { gl.get_uniform_location(program, "u_use_tex") };
|
let u_use_tex = unsafe { gl.get_uniform_location(program, "u_use_tex") };
|
||||||
let u_tex = unsafe { gl.get_uniform_location(program, "u_tex") };
|
let u_tex = unsafe { gl.get_uniform_location(program, "u_tex") };
|
||||||
@@ -319,11 +303,16 @@ fn run(args: Args) -> Result<(), String> {
|
|||||||
.ok_or_else(|| String::from("shader attribute a_uv is missing"))?;
|
.ok_or_else(|| String::from("shader attribute a_uv is missing"))?;
|
||||||
|
|
||||||
let vbo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? };
|
let vbo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? };
|
||||||
|
let ebo = unsafe { gl.create_buffer().map_err(|e| e.to_string())? };
|
||||||
unsafe {
|
unsafe {
|
||||||
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
||||||
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, &vertex_bytes, glow::STATIC_DRAW);
|
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, &vertex_bytes, glow::STATIC_DRAW);
|
||||||
|
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
|
||||||
|
gl.buffer_data_u8_slice(glow::ELEMENT_ARRAY_BUFFER, &index_bytes, glow::STATIC_DRAW);
|
||||||
|
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
|
||||||
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
||||||
}
|
}
|
||||||
|
let vao = unsafe { create_vertex_layout_if_needed(&gl, gl_backend, vbo, ebo, a_pos, a_uv)? };
|
||||||
|
|
||||||
let gpu_texture = if let Some(texture) = resolved_texture.as_ref() {
|
let gpu_texture = if let Some(texture) = resolved_texture.as_ref() {
|
||||||
Some(unsafe { create_texture(&gl, texture)? })
|
Some(unsafe { create_texture(&gl, texture)? })
|
||||||
@@ -341,8 +330,10 @@ fn run(args: Args) -> Result<(), String> {
|
|||||||
a_pos,
|
a_pos,
|
||||||
a_uv,
|
a_uv,
|
||||||
vbo,
|
vbo,
|
||||||
|
ebo,
|
||||||
|
vao,
|
||||||
gpu_texture.as_ref(),
|
gpu_texture.as_ref(),
|
||||||
mesh.vertices.len(),
|
mesh.indices.len(),
|
||||||
&args,
|
&args,
|
||||||
center,
|
center,
|
||||||
camera_distance,
|
camera_distance,
|
||||||
@@ -360,8 +351,10 @@ fn run(args: Args) -> Result<(), String> {
|
|||||||
a_pos,
|
a_pos,
|
||||||
a_uv,
|
a_uv,
|
||||||
vbo,
|
vbo,
|
||||||
|
ebo,
|
||||||
|
vao,
|
||||||
gpu_texture.as_ref(),
|
gpu_texture.as_ref(),
|
||||||
mesh.vertices.len(),
|
mesh.indices.len(),
|
||||||
&args,
|
&args,
|
||||||
center,
|
center,
|
||||||
camera_distance,
|
camera_distance,
|
||||||
@@ -372,6 +365,10 @@ fn run(args: Args) -> Result<(), String> {
|
|||||||
if let Some(texture) = gpu_texture {
|
if let Some(texture) = gpu_texture {
|
||||||
gl.delete_texture(texture.handle);
|
gl.delete_texture(texture.handle);
|
||||||
}
|
}
|
||||||
|
if let Some(vao) = vao {
|
||||||
|
gl.delete_vertex_array(vao);
|
||||||
|
}
|
||||||
|
gl.delete_buffer(ebo);
|
||||||
gl.delete_buffer(vbo);
|
gl.delete_buffer(vbo);
|
||||||
gl.delete_program(program);
|
gl.delete_program(program);
|
||||||
}
|
}
|
||||||
@@ -379,6 +376,97 @@ fn run(args: Args) -> Result<(), String> {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_window_and_context(
|
||||||
|
video: &sdl2::VideoSubsystem,
|
||||||
|
args: &Args,
|
||||||
|
) -> Result<(sdl2::video::Window, sdl2::video::GLContext, GlBackend), String> {
|
||||||
|
let candidates = [
|
||||||
|
(GlBackend::Gles2, sdl2::video::GLProfile::GLES, 2, 0),
|
||||||
|
(GlBackend::Core33, sdl2::video::GLProfile::Core, 3, 3),
|
||||||
|
];
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
for (backend, profile, major, minor) in candidates {
|
||||||
|
{
|
||||||
|
let gl_attr = video.gl_attr();
|
||||||
|
gl_attr.set_context_profile(profile);
|
||||||
|
gl_attr.set_context_version(major, minor);
|
||||||
|
gl_attr.set_depth_size(24);
|
||||||
|
gl_attr.set_double_buffer(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut window_builder = video.window(
|
||||||
|
"Parkan Render Demo (SDL2 + OpenGL)",
|
||||||
|
args.width,
|
||||||
|
args.height,
|
||||||
|
);
|
||||||
|
window_builder.opengl();
|
||||||
|
if args.capture.is_some() {
|
||||||
|
window_builder.hidden();
|
||||||
|
} else {
|
||||||
|
window_builder.resizable();
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = match window_builder.build() {
|
||||||
|
Ok(window) => window,
|
||||||
|
Err(err) => {
|
||||||
|
errors.push(format!(
|
||||||
|
"{profile:?} {major}.{minor}: window build failed ({err})"
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let gl_ctx = match window.gl_create_context() {
|
||||||
|
Ok(ctx) => ctx,
|
||||||
|
Err(err) => {
|
||||||
|
errors.push(format!(
|
||||||
|
"{profile:?} {major}.{minor}: context create failed ({err})"
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = window.gl_make_current(&gl_ctx) {
|
||||||
|
errors.push(format!(
|
||||||
|
"{profile:?} {major}.{minor}: make current failed ({err})"
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok((window, gl_ctx, backend));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!(
|
||||||
|
"failed to create OpenGL context. Attempts: {}",
|
||||||
|
errors.join(" | ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn create_vertex_layout_if_needed(
|
||||||
|
gl: &glow::Context,
|
||||||
|
backend: GlBackend,
|
||||||
|
vbo: glow::NativeBuffer,
|
||||||
|
ebo: glow::NativeBuffer,
|
||||||
|
a_pos: u32,
|
||||||
|
a_uv: u32,
|
||||||
|
) -> Result<Option<glow::NativeVertexArray>, String> {
|
||||||
|
if backend != GlBackend::Core33 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let vao = gl.create_vertex_array().map_err(|e| e.to_string())?;
|
||||||
|
gl.bind_vertex_array(Some(vao));
|
||||||
|
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
||||||
|
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
|
||||||
|
gl.enable_vertex_attrib_array(a_pos);
|
||||||
|
gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
|
||||||
|
gl.enable_vertex_attrib_array(a_uv);
|
||||||
|
gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
|
||||||
|
gl.bind_vertex_array(None);
|
||||||
|
Ok(Some(vao))
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_texture(args: &Args, model_name: &str) -> Result<Option<LoadedTexture>, String> {
|
fn resolve_texture(args: &Args, model_name: &str) -> Result<Option<LoadedTexture>, String> {
|
||||||
if args.no_texture {
|
if args.no_texture {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -452,8 +540,10 @@ fn run_capture(
|
|||||||
a_pos: u32,
|
a_pos: u32,
|
||||||
a_uv: u32,
|
a_uv: u32,
|
||||||
vbo: glow::NativeBuffer,
|
vbo: glow::NativeBuffer,
|
||||||
|
ebo: glow::NativeBuffer,
|
||||||
|
vao: Option<glow::NativeVertexArray>,
|
||||||
texture: Option<&GpuTexture>,
|
texture: Option<&GpuTexture>,
|
||||||
vertex_count: usize,
|
index_count: usize,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
center: [f32; 3],
|
center: [f32; 3],
|
||||||
camera_distance: f32,
|
camera_distance: f32,
|
||||||
@@ -478,8 +568,10 @@ fn run_capture(
|
|||||||
a_pos,
|
a_pos,
|
||||||
a_uv,
|
a_uv,
|
||||||
vbo,
|
vbo,
|
||||||
|
ebo,
|
||||||
|
vao,
|
||||||
texture,
|
texture,
|
||||||
vertex_count,
|
index_count,
|
||||||
args.width,
|
args.width,
|
||||||
args.height,
|
args.height,
|
||||||
&mvp,
|
&mvp,
|
||||||
@@ -504,8 +596,10 @@ fn run_interactive(
|
|||||||
a_pos: u32,
|
a_pos: u32,
|
||||||
a_uv: u32,
|
a_uv: u32,
|
||||||
vbo: glow::NativeBuffer,
|
vbo: glow::NativeBuffer,
|
||||||
|
ebo: glow::NativeBuffer,
|
||||||
|
vao: Option<glow::NativeVertexArray>,
|
||||||
texture: Option<&GpuTexture>,
|
texture: Option<&GpuTexture>,
|
||||||
vertex_count: usize,
|
index_count: usize,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
center: [f32; 3],
|
center: [f32; 3],
|
||||||
camera_distance: f32,
|
camera_distance: f32,
|
||||||
@@ -543,8 +637,10 @@ fn run_interactive(
|
|||||||
a_pos,
|
a_pos,
|
||||||
a_uv,
|
a_uv,
|
||||||
vbo,
|
vbo,
|
||||||
|
ebo,
|
||||||
|
vao,
|
||||||
texture,
|
texture,
|
||||||
vertex_count,
|
index_count,
|
||||||
w,
|
w,
|
||||||
h,
|
h,
|
||||||
&mvp,
|
&mvp,
|
||||||
@@ -584,8 +680,10 @@ unsafe fn draw_frame(
|
|||||||
a_pos: u32,
|
a_pos: u32,
|
||||||
a_uv: u32,
|
a_uv: u32,
|
||||||
vbo: glow::NativeBuffer,
|
vbo: glow::NativeBuffer,
|
||||||
|
ebo: glow::NativeBuffer,
|
||||||
|
vao: Option<glow::NativeVertexArray>,
|
||||||
texture: Option<&GpuTexture>,
|
texture: Option<&GpuTexture>,
|
||||||
vertex_count: usize,
|
index_count: usize,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
mvp: &[f32; 16],
|
mvp: &[f32; 16],
|
||||||
@@ -613,19 +711,33 @@ unsafe fn draw_frame(
|
|||||||
gl.bind_texture(glow::TEXTURE_2D, None);
|
gl.bind_texture(glow::TEXTURE_2D, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(vao) = vao {
|
||||||
|
gl.bind_vertex_array(Some(vao));
|
||||||
|
gl.draw_elements(
|
||||||
|
glow::TRIANGLES,
|
||||||
|
index_count.min(i32::MAX as usize) as i32,
|
||||||
|
glow::UNSIGNED_SHORT,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
gl.bind_vertex_array(None);
|
||||||
|
} else {
|
||||||
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
||||||
|
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
|
||||||
gl.enable_vertex_attrib_array(a_pos);
|
gl.enable_vertex_attrib_array(a_pos);
|
||||||
gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
|
gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
|
||||||
gl.enable_vertex_attrib_array(a_uv);
|
gl.enable_vertex_attrib_array(a_uv);
|
||||||
gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
|
gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
|
||||||
gl.draw_arrays(
|
gl.draw_elements(
|
||||||
glow::TRIANGLES,
|
glow::TRIANGLES,
|
||||||
|
index_count.min(i32::MAX as usize) as i32,
|
||||||
|
glow::UNSIGNED_SHORT,
|
||||||
0,
|
0,
|
||||||
vertex_count.min(i32::MAX as usize) as i32,
|
|
||||||
);
|
);
|
||||||
gl.disable_vertex_attrib_array(a_uv);
|
gl.disable_vertex_attrib_array(a_uv);
|
||||||
gl.disable_vertex_attrib_array(a_pos);
|
gl.disable_vertex_attrib_array(a_pos);
|
||||||
|
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
|
||||||
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
||||||
|
}
|
||||||
gl.bind_texture(glow::TEXTURE_2D, None);
|
gl.bind_texture(glow::TEXTURE_2D, None);
|
||||||
gl.use_program(None);
|
gl.use_program(None);
|
||||||
}
|
}
|
||||||
@@ -680,8 +792,13 @@ fn save_png(path: &Path, width: u32, height: u32, rgba: Vec<u8>) -> Result<(), S
|
|||||||
.map_err(|err| format!("failed to save PNG {}: {err}", path.display()))
|
.map_err(|err| format!("failed to save PNG {}: {err}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn create_program(gl: &glow::Context) -> Result<glow::NativeProgram, String> {
|
unsafe fn create_program(
|
||||||
let vs_src = r#"
|
gl: &glow::Context,
|
||||||
|
backend: GlBackend,
|
||||||
|
) -> Result<glow::NativeProgram, String> {
|
||||||
|
let (vs_src, fs_src) = match backend {
|
||||||
|
GlBackend::Gles2 => (
|
||||||
|
r#"
|
||||||
attribute vec3 a_pos;
|
attribute vec3 a_pos;
|
||||||
attribute vec2 a_uv;
|
attribute vec2 a_uv;
|
||||||
uniform mat4 u_mvp;
|
uniform mat4 u_mvp;
|
||||||
@@ -690,9 +807,8 @@ void main() {
|
|||||||
v_uv = a_uv;
|
v_uv = a_uv;
|
||||||
gl_Position = u_mvp * vec4(a_pos, 1.0);
|
gl_Position = u_mvp * vec4(a_pos, 1.0);
|
||||||
}
|
}
|
||||||
"#;
|
"#,
|
||||||
|
r#"
|
||||||
let fs_src = r#"
|
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
uniform sampler2D u_tex;
|
uniform sampler2D u_tex;
|
||||||
uniform float u_use_tex;
|
uniform float u_use_tex;
|
||||||
@@ -702,7 +818,32 @@ void main() {
|
|||||||
vec4 texColor = texture2D(u_tex, v_uv);
|
vec4 texColor = texture2D(u_tex, v_uv);
|
||||||
gl_FragColor = mix(base, texColor, u_use_tex);
|
gl_FragColor = mix(base, texColor, u_use_tex);
|
||||||
}
|
}
|
||||||
"#;
|
"#,
|
||||||
|
),
|
||||||
|
GlBackend::Core33 => (
|
||||||
|
r#"#version 330 core
|
||||||
|
in vec3 a_pos;
|
||||||
|
in vec2 a_uv;
|
||||||
|
uniform mat4 u_mvp;
|
||||||
|
out vec2 v_uv;
|
||||||
|
void main() {
|
||||||
|
v_uv = a_uv;
|
||||||
|
gl_Position = u_mvp * vec4(a_pos, 1.0);
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
r#"#version 330 core
|
||||||
|
uniform sampler2D u_tex;
|
||||||
|
uniform float u_use_tex;
|
||||||
|
in vec2 v_uv;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
vec4 base = vec4(0.85, 0.90, 1.00, 1.0);
|
||||||
|
vec4 texColor = texture(u_tex, v_uv);
|
||||||
|
fragColor = mix(base, texColor, u_use_tex);
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
let program = gl.create_program().map_err(|e| e.to_string())?;
|
let program = gl.create_program().map_err(|e| e.to_string())?;
|
||||||
let vs = gl
|
let vs = gl
|
||||||
@@ -758,6 +899,14 @@ fn f32_slice_to_ne_bytes(slice: &[f32]) -> Vec<u8> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn u16_slice_to_ne_bytes(slice: &[u16]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(slice.len().saturating_mul(std::mem::size_of::<u16>()));
|
||||||
|
for &value in slice {
|
||||||
|
out.extend_from_slice(&value.to_ne_bytes());
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
fn mat4_identity() -> [f32; 16] {
|
fn mat4_identity() -> [f32; 16] {
|
||||||
[
|
[
|
||||||
1.0, 0.0, 0.0, 0.0, //
|
1.0, 0.0, 0.0, 0.0, //
|
||||||
|
|||||||
@@ -6,3 +6,6 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = "1"
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::compress::lzh::{LZH_MAX_FREQ, LZH_N_CHAR, LZH_R, LZH_T};
|
use crate::compress::lzh::{LZH_MAX_FREQ, LZH_N_CHAR, LZH_R, LZH_T};
|
||||||
use crate::compress::xor::xor_stream;
|
use crate::compress::xor::xor_stream;
|
||||||
|
use common::collect_files_recursive;
|
||||||
use flate2::write::DeflateEncoder;
|
use flate2::write::DeflateEncoder;
|
||||||
use flate2::write::ZlibEncoder;
|
use flate2::write::ZlibEncoder;
|
||||||
use flate2::Compression;
|
use flate2::Compression;
|
||||||
|
use proptest::prelude::*;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct SyntheticRsliEntry {
|
struct SyntheticRsliEntry {
|
||||||
@@ -37,20 +40,6 @@ impl Default for RsliBuildOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
|
|
||||||
let Ok(entries) = fs::read_dir(root) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
collect_files_recursive(&path, out);
|
|
||||||
} else if path.is_file() {
|
|
||||||
out.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rsli_test_files() -> Vec<PathBuf> {
|
fn rsli_test_files() -> Vec<PathBuf> {
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("..")
|
.join("..")
|
||||||
@@ -1335,3 +1324,15 @@ fn rsli_validation_error_cases() {
|
|||||||
}
|
}
|
||||||
let _ = fs::remove_file(&path);
|
let _ = fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(64))]
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_library_is_panic_free_on_random_bytes(data in proptest::collection::vec(any::<u8>(), 0..4096)) {
|
||||||
|
let _ = crate::parse::parse_library(
|
||||||
|
Arc::from(data.into_boxed_slice()),
|
||||||
|
OpenOptions::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
nres = { path = "../nres" }
|
nres = { path = "../nres" }
|
||||||
|
proptest = "1"
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use common::collect_files_recursive;
|
||||||
use nres::Archive;
|
use nres::Archive;
|
||||||
|
use proptest::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) {
|
|
||||||
let Ok(entries) = fs::read_dir(root) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
collect_files_recursive(&path, out);
|
|
||||||
} else if path.is_file() {
|
|
||||||
out.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nres_test_files() -> Vec<PathBuf> {
|
fn nres_test_files() -> Vec<PathBuf> {
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("..")
|
.join("..")
|
||||||
@@ -327,3 +315,16 @@ fn texm_errors_for_page_chunk_and_mip_bounds() {
|
|||||||
Err(Error::MipDataOutOfBounds { .. })
|
Err(Error::MipDataOutOfBounds { .. })
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(64))]
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_texm_is_panic_free_on_random_bytes(payload in proptest::collection::vec(any::<u8>(), 0..4096)) {
|
||||||
|
if let Ok(texture) = parse_texm(&payload) {
|
||||||
|
for mip_index in 0..texture.mip_levels.len() {
|
||||||
|
let _ = decode_mip_rgba8(&texture, &payload, mip_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user