Compare commits
2 Commits
efab61a45c
...
7346e695c4
| Author | SHA1 | Date | |
|---|---|---|---|
|
7346e695c4
|
|||
|
bb827c3928
|
@@ -1,4 +1,6 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Resource payload that can be either borrowed from mapped bytes or owned.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -42,3 +44,18 @@ impl OutputBuffer for Vec<u8> {
|
||||
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"
|
||||
|
||||
[dependencies]
|
||||
encoding_rs = "0.8"
|
||||
nres = { path = "../nres" }
|
||||
|
||||
[dev-dependencies]
|
||||
common = { path = "../common" }
|
||||
proptest = "1"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod error;
|
||||
|
||||
use crate::error::Error;
|
||||
use encoding_rs::WINDOWS_1251;
|
||||
use std::sync::Arc;
|
||||
|
||||
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 {
|
||||
slice
|
||||
};
|
||||
let decoded = String::from_utf8_lossy(text).to_string();
|
||||
let decoded = decode_cp1251(text);
|
||||
out.push(Some(decoded));
|
||||
off = end;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn decode_cp1251(bytes: &[u8]) -> String {
|
||||
let (decoded, _, _) = WINDOWS_1251.decode(bytes);
|
||||
decoded.into_owned()
|
||||
}
|
||||
|
||||
struct RawResource {
|
||||
meta: nres::EntryMeta,
|
||||
bytes: Vec<u8>,
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use nres::Archive;
|
||||
use proptest::prelude::*;
|
||||
use std::fs;
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -169,18 +157,17 @@ fn res13_single_batch(index_start: u32, index_count: u16) -> Vec<u8> {
|
||||
batch
|
||||
}
|
||||
|
||||
fn res10_names(names: &[Option<&str>]) -> Vec<u8> {
|
||||
fn res10_names_raw(names: &[Option<&[u8]>]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
for name in names {
|
||||
match name {
|
||||
Some(name) => {
|
||||
let bytes = name.as_bytes();
|
||||
out.extend_from_slice(
|
||||
&u32::try_from(bytes.len())
|
||||
&u32::try_from(name.len())
|
||||
.expect("name size overflow in test")
|
||||
.to_le_bytes(),
|
||||
);
|
||||
out.extend_from_slice(bytes);
|
||||
out.extend_from_slice(name);
|
||||
out.push(0);
|
||||
}
|
||||
None => out.extend_from_slice(&0u32.to_le_bytes()),
|
||||
@@ -189,6 +176,11 @@ fn res10_names(names: &[Option<&str>]) -> Vec<u8> {
|
||||
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> {
|
||||
vec![
|
||||
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]));
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn parse_fails_when_required_resource_missing() {
|
||||
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 common::collect_files_recursive;
|
||||
use std::any::Any;
|
||||
use std::fs;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
@@ -13,20 +14,6 @@ struct SyntheticEntry<'a> {
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
|
||||
@@ -7,4 +7,5 @@ edition = "2021"
|
||||
msh-core = { path = "../msh-core" }
|
||||
|
||||
[dev-dependencies]
|
||||
common = { path = "../common" }
|
||||
nres = { path = "../nres" }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use msh_core::Model;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const DEFAULT_UV_SCALE: f32 = 1024.0;
|
||||
|
||||
@@ -11,21 +12,24 @@ pub struct RenderVertex {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RenderMesh {
|
||||
pub vertices: Vec<RenderVertex>,
|
||||
pub indices: Vec<u16>,
|
||||
pub batch_count: usize,
|
||||
pub index_overflow: bool,
|
||||
}
|
||||
|
||||
impl RenderMesh {
|
||||
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.
|
||||
///
|
||||
/// The output is suitable for simple `glDrawArrays(GL_TRIANGLES, ...)` paths.
|
||||
/// Builds an indexed triangle mesh for a specific LOD/group pair.
|
||||
pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh {
|
||||
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 index_overflow = false;
|
||||
let uv0 = model.uv0.as_ref();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let batch_out_start = indices.len();
|
||||
let mut batch_valid = true;
|
||||
for &idx in &model.indices[index_start..index_end] {
|
||||
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 {
|
||||
continue;
|
||||
batch_valid = false;
|
||||
break;
|
||||
};
|
||||
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
|
||||
.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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
RenderMesh {
|
||||
vertices,
|
||||
indices,
|
||||
batch_count,
|
||||
index_overflow,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use msh_core::parse_model_payload;
|
||||
use nres::Archive;
|
||||
use std::fs;
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -71,12 +58,20 @@ fn build_render_mesh_for_real_models() {
|
||||
)
|
||||
});
|
||||
let mesh = build_render_mesh(&model, 0, 0);
|
||||
if !mesh.vertices.is_empty() {
|
||||
if !mesh.indices.is_empty() {
|
||||
meshes_non_empty += 1;
|
||||
}
|
||||
if compute_bounds_for_mesh(&mesh.vertices).is_some() {
|
||||
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 {
|
||||
assert!(
|
||||
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);
|
||||
assert!(mesh.vertices.is_empty());
|
||||
assert!(mesh.indices.is_empty());
|
||||
assert_eq!(mesh.batch_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);
|
||||
assert_eq!(mesh.batch_count, 2);
|
||||
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.vertices[0].uv0, [1.0, -1.0]);
|
||||
assert_eq!(mesh.vertices[1].uv0, [0.5, 0.25]);
|
||||
assert_eq!(mesh.vertices[2].uv0, [0.0, 0.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" }
|
||||
render-core = { path = "../render-core" }
|
||||
texm = { path = "../texm" }
|
||||
sdl2 = { version = "0.37", optional = true, default-features = false, features = ["bundled", "static-link"] }
|
||||
glow = { version = "0.16", optional = true }
|
||||
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]]
|
||||
name = "parkan-render-demo"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
### macOS prerequisites
|
||||
|
||||
Для macOS `render-demo` ожидает системный SDL2 через `pkg-config`:
|
||||
|
||||
```bash
|
||||
brew install sdl2 pkg-config
|
||||
```
|
||||
|
||||
После этого запускайте той же командой `cargo run ... --features demo`.
|
||||
|
||||
Параметры:
|
||||
|
||||
- `--archive` (обязательный): NRes-архив с `.msh` entry.
|
||||
@@ -70,4 +80,4 @@ cargo run -p render-demo --features demo -- \
|
||||
## Ограничения
|
||||
|
||||
- Используется только базовая 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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use std::fs;
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
|
||||
@@ -26,6 +26,12 @@ struct GpuTexture {
|
||||
handle: glow::NativeTexture,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum GlBackend {
|
||||
Gles2,
|
||||
Core33,
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args, String> {
|
||||
let mut archive = 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);
|
||||
if mesh.vertices.is_empty() {
|
||||
if mesh.indices.is_empty() {
|
||||
return Err(format!(
|
||||
"model has no renderable triangles for lod={} 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 {
|
||||
return Err(String::from("failed to compute mesh bounds"));
|
||||
};
|
||||
@@ -260,35 +271,7 @@ fn run(args: Args) -> Result<(), String> {
|
||||
.video()
|
||||
.map_err(|err| format!("failed to init SDL2 video: {err}"))?;
|
||||
|
||||
{
|
||||
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 (window, _gl_ctx, gl_backend) = create_window_and_context(&video, &args)?;
|
||||
let _ = if args.capture.is_some() {
|
||||
video.gl_set_swap_interval(0)
|
||||
} else {
|
||||
@@ -304,12 +287,13 @@ fn run(args: Args) -> Result<(), String> {
|
||||
vertex_data.push(vertex.uv0[1]);
|
||||
}
|
||||
let vertex_bytes = f32_slice_to_ne_bytes(&vertex_data);
|
||||
let index_bytes = u16_slice_to_ne_bytes(&mesh.indices);
|
||||
|
||||
let gl = unsafe {
|
||||
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_use_tex = unsafe { gl.get_uniform_location(program, "u_use_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"))?;
|
||||
|
||||
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 {
|
||||
gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
|
||||
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);
|
||||
}
|
||||
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() {
|
||||
Some(unsafe { create_texture(&gl, texture)? })
|
||||
@@ -341,8 +330,10 @@ fn run(args: Args) -> Result<(), String> {
|
||||
a_pos,
|
||||
a_uv,
|
||||
vbo,
|
||||
ebo,
|
||||
vao,
|
||||
gpu_texture.as_ref(),
|
||||
mesh.vertices.len(),
|
||||
mesh.indices.len(),
|
||||
&args,
|
||||
center,
|
||||
camera_distance,
|
||||
@@ -360,8 +351,10 @@ fn run(args: Args) -> Result<(), String> {
|
||||
a_pos,
|
||||
a_uv,
|
||||
vbo,
|
||||
ebo,
|
||||
vao,
|
||||
gpu_texture.as_ref(),
|
||||
mesh.vertices.len(),
|
||||
mesh.indices.len(),
|
||||
&args,
|
||||
center,
|
||||
camera_distance,
|
||||
@@ -372,6 +365,10 @@ fn run(args: Args) -> Result<(), String> {
|
||||
if let Some(texture) = gpu_texture {
|
||||
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_program(program);
|
||||
}
|
||||
@@ -379,6 +376,97 @@ fn run(args: Args) -> Result<(), String> {
|
||||
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> {
|
||||
if args.no_texture {
|
||||
return Ok(None);
|
||||
@@ -452,8 +540,10 @@ fn run_capture(
|
||||
a_pos: u32,
|
||||
a_uv: u32,
|
||||
vbo: glow::NativeBuffer,
|
||||
ebo: glow::NativeBuffer,
|
||||
vao: Option<glow::NativeVertexArray>,
|
||||
texture: Option<&GpuTexture>,
|
||||
vertex_count: usize,
|
||||
index_count: usize,
|
||||
args: &Args,
|
||||
center: [f32; 3],
|
||||
camera_distance: f32,
|
||||
@@ -478,8 +568,10 @@ fn run_capture(
|
||||
a_pos,
|
||||
a_uv,
|
||||
vbo,
|
||||
ebo,
|
||||
vao,
|
||||
texture,
|
||||
vertex_count,
|
||||
index_count,
|
||||
args.width,
|
||||
args.height,
|
||||
&mvp,
|
||||
@@ -504,8 +596,10 @@ fn run_interactive(
|
||||
a_pos: u32,
|
||||
a_uv: u32,
|
||||
vbo: glow::NativeBuffer,
|
||||
ebo: glow::NativeBuffer,
|
||||
vao: Option<glow::NativeVertexArray>,
|
||||
texture: Option<&GpuTexture>,
|
||||
vertex_count: usize,
|
||||
index_count: usize,
|
||||
args: &Args,
|
||||
center: [f32; 3],
|
||||
camera_distance: f32,
|
||||
@@ -543,8 +637,10 @@ fn run_interactive(
|
||||
a_pos,
|
||||
a_uv,
|
||||
vbo,
|
||||
ebo,
|
||||
vao,
|
||||
texture,
|
||||
vertex_count,
|
||||
index_count,
|
||||
w,
|
||||
h,
|
||||
&mvp,
|
||||
@@ -584,8 +680,10 @@ unsafe fn draw_frame(
|
||||
a_pos: u32,
|
||||
a_uv: u32,
|
||||
vbo: glow::NativeBuffer,
|
||||
ebo: glow::NativeBuffer,
|
||||
vao: Option<glow::NativeVertexArray>,
|
||||
texture: Option<&GpuTexture>,
|
||||
vertex_count: usize,
|
||||
index_count: usize,
|
||||
width: u32,
|
||||
height: u32,
|
||||
mvp: &[f32; 16],
|
||||
@@ -613,19 +711,33 @@ unsafe fn draw_frame(
|
||||
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::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.draw_arrays(
|
||||
gl.draw_elements(
|
||||
glow::TRIANGLES,
|
||||
index_count.min(i32::MAX as usize) as i32,
|
||||
glow::UNSIGNED_SHORT,
|
||||
0,
|
||||
vertex_count.min(i32::MAX as usize) as i32,
|
||||
);
|
||||
gl.disable_vertex_attrib_array(a_uv);
|
||||
gl.disable_vertex_attrib_array(a_pos);
|
||||
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
|
||||
gl.bind_buffer(glow::ARRAY_BUFFER, None);
|
||||
}
|
||||
gl.bind_texture(glow::TEXTURE_2D, 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()))
|
||||
}
|
||||
|
||||
unsafe fn create_program(gl: &glow::Context) -> Result<glow::NativeProgram, String> {
|
||||
let vs_src = r#"
|
||||
unsafe fn create_program(
|
||||
gl: &glow::Context,
|
||||
backend: GlBackend,
|
||||
) -> Result<glow::NativeProgram, String> {
|
||||
let (vs_src, fs_src) = match backend {
|
||||
GlBackend::Gles2 => (
|
||||
r#"
|
||||
attribute vec3 a_pos;
|
||||
attribute vec2 a_uv;
|
||||
uniform mat4 u_mvp;
|
||||
@@ -690,9 +807,8 @@ void main() {
|
||||
v_uv = a_uv;
|
||||
gl_Position = u_mvp * vec4(a_pos, 1.0);
|
||||
}
|
||||
"#;
|
||||
|
||||
let fs_src = r#"
|
||||
"#,
|
||||
r#"
|
||||
precision mediump float;
|
||||
uniform sampler2D u_tex;
|
||||
uniform float u_use_tex;
|
||||
@@ -702,7 +818,32 @@ void main() {
|
||||
vec4 texColor = texture2D(u_tex, v_uv);
|
||||
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 vs = gl
|
||||
@@ -758,6 +899,14 @@ fn f32_slice_to_ne_bytes(slice: &[f32]) -> Vec<u8> {
|
||||
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] {
|
||||
[
|
||||
1.0, 0.0, 0.0, 0.0, //
|
||||
|
||||
@@ -6,3 +6,6 @@ edition = "2021"
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1"
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use super::*;
|
||||
use crate::compress::lzh::{LZH_MAX_FREQ, LZH_N_CHAR, LZH_R, LZH_T};
|
||||
use crate::compress::xor::xor_stream;
|
||||
use common::collect_files_recursive;
|
||||
use flate2::write::DeflateEncoder;
|
||||
use flate2::write::ZlibEncoder;
|
||||
use flate2::Compression;
|
||||
use proptest::prelude::*;
|
||||
use std::any::Any;
|
||||
use std::fs;
|
||||
use std::io::Write as _;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -1335,3 +1324,15 @@ fn rsli_validation_error_cases() {
|
||||
}
|
||||
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"
|
||||
|
||||
[dev-dependencies]
|
||||
common = { path = "../common" }
|
||||
nres = { path = "../nres" }
|
||||
proptest = "1"
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
use super::*;
|
||||
use common::collect_files_recursive;
|
||||
use nres::Archive;
|
||||
use proptest::prelude::*;
|
||||
use std::fs;
|
||||
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> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -327,3 +315,16 @@ fn texm_errors_for_page_chunk_and_mip_bounds() {
|
||||
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