diff options
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/reflection.rs | 5 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 31 | ||||
| -rw-r--r-- | crates/fparkan-render/src/water.rs | 29 | ||||
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 2 | ||||
| -rw-r--r-- | docs/tomes/05-render.md | 17 |
5 files changed, 68 insertions, 16 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/reflection.rs b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs index 62a0d1e..1a9c9ae 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/reflection.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/reflection.rs @@ -221,6 +221,9 @@ impl ReflectionResources { depth_request: fparkan_platform::DepthStencilSupport, render_pass: vk::RenderPass, ) -> Result<(), VulkanSmokeRendererError> { + let side = fparkan_render::water::legacy_embm_reflection_size([extent.0, extent.1]); + let extent = (side, side); + eprintln!("render_reflection target_extent={side}x{side}"); let info = vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(vk::Format::from_raw(format)) @@ -236,7 +239,7 @@ impl ReflectionResources { .usage(vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::SAMPLED) .sharing_mode(vk::SharingMode::EXCLUSIVE) .initial_layout(vk::ImageLayout::UNDEFINED); - // SAFETY: This target uses the validated non-zero swapchain extent and format. + // SAFETY: The validated swapchain format is retained; the square side is in 2..=1024. let image = unsafe { self.device.create_image(&info, None) }.map_err(|result| { VulkanSmokeRendererError::VulkanOperation { context: "vkCreateImage(reflection)", diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index ef19ee8..a016645 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -373,7 +373,7 @@ impl VulkanSmokeRenderer { plane_heights.sort_unstable(); plane_heights.dedup(); if !plane_heights.is_empty() { - eprintln!("render_coverage planar_reflection_render_context_approx={} (native shifted window and bump UV; main-view depth range, sky/fog/FX and swapchain-sized target remain approximations; non-perspective cameras use mirror fallback)", plane_heights.len()); + eprintln!("render_coverage planar_reflection_render_context_approx={} (native shifted window, target size and bump UV; main-view depth range and sky/fog/FX remain approximations; non-perspective cameras use mirror fallback)", plane_heights.len()); eprintln!("render_coverage planar_reflection_other_planes_unsupported={} (nearest active plane is reflected)", plane_heights.len().saturating_sub(1)); } if unsupported_nonplanar != 0 { @@ -1874,17 +1874,27 @@ impl VulkanSmokeRenderer { }, }, ]; - let render_area = vk::Rect2D { - offset: vk::Offset2D { x: 0, y: 0 }, - extent: vk::Extent2D { - width: swapchain.report.plan.extent.0, - height: swapchain.report.plan.extent.1, - }, - }; for reflected in [true, false] { if reflected && reflection_plane.is_none() { continue; } + let extent = if reflected { + let target = resources.reflection.color.as_ref().ok_or( + VulkanSmokeRendererError::InvariantViolation { + context: "active reflection without a color target", + }, + )?; + (target.width, target.height) + } else { + swapchain.report.plan.extent + }; + let render_area = vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: vk::Extent2D { + width: extent.0, + height: extent.1, + }, + }; let framebuffer = if reflected { resources.reflection.framebuffer } else { @@ -2073,10 +2083,7 @@ impl VulkanSmokeRenderer { } else { 0 }) as f32; - let viewport = super::swapchain_resources::draw_viewport( - swapchain.report.plan.extent, - material_mode, - ); + let viewport = super::swapchain_resources::draw_viewport(extent, material_mode); let next_viewport_depth = [viewport.min_depth, viewport.max_depth]; if viewport_depth != Some(next_viewport_depth) { device diff --git a/crates/fparkan-render/src/water.rs b/crates/fparkan-render/src/water.rs index 4f161cc..58a2817 100644 --- a/crates/fparkan-render/src/water.rs +++ b/crates/fparkan-render/src/water.rs @@ -7,6 +7,17 @@ pub const EMBM_BUMP_TILE: f32 = 100.0; /// Native diagonal bump matrix, without luminance modulation. pub const EMBM_COEFFICIENTS: [f32; 2] = [0.01, 0.01]; +/// Square reflection size chosen by Terrain.dll RVA 0x27FC7 from renderer dimensions. +#[must_use] +pub fn legacy_embm_reflection_size(extent: [u32; 2]) -> u32 { + let limit = extent[0].min(extent[1]).min(1024); + let mut side = 2; + while side * 2 <= limit { + side *= 2; + } + side +} + /// Generates the signed bytes written by Terrain.dll RVA 0x2BF50 with CW 0x007F. #[must_use] pub fn legacy_embm_bump_map() -> Vec<[i8; 2]> { @@ -117,6 +128,24 @@ mod tests { use super::*; #[test] + fn reflection_size_matches_native_renderer_dimension_vectors() { + for (extent, expected) in [ + ([1280, 1024], 1024), + ([1280, 720], 512), + ([800, 600], 512), + ([640, 480], 256), + ([1920, 1080], 1024), + ([2048, 2048], 1024), + ([4, 7], 4), + ([3, 300], 2), + ([1, 1], 2), + ([700, 700], 512), + ] { + assert_eq!(legacy_embm_reflection_size(extent), expected, "{extent:?}"); + } + } + + #[test] fn shifted_camera_maps_native_water_corners_and_retains_perspective_depth() { let projection = crate::LegacyD3d7Projection { viewport: [0, 0, 1280, 1024], diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 7b71e9e..ce3400e 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -76,7 +76,7 @@ MSH-запрос со свободной preview-камерой; это не пÐ среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state log на минимальных сценах с одним типом материала. -Горизонтальные грани Land.msh с флагом `0x20000` используют исходную карту Du/Dv и окно shifted-камеры, проверенное исполнением Terrain.dll. Диапазон глубины, небо/туман/FX основного вида и размер target остаются приближениями: `planar_reflection_render_context_approx`; неперспективная камера API использует обычное зеркало. +Горизонтальные грани Land.msh с флагом `0x20000` используют исходную карту Du/Dv, окно shifted-камеры и квадратный размер текстуры, проверенные исполнением DLL. Диапазон глубины, небо/туман/FX основного вида и соответствие размеров при обрезанном capture остаются приближениями: `planar_reflection_render_context_approx`; неперспективная камера API использует обычное зеркало. Raw queue clock задаёт фазу волн отдельно от времени суток и sky elapsed; старые захваты без него считаются в `captured_water_phase_unverified`. Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader. `terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена. diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 53667c8..4aba919 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1169,6 +1169,18 @@ Rust-тест отдельно проверяет углы водоёма и гÑ водоёма с добавленным смещением волн. Для неперспективной камеры API остаётся обычное зеркало с экранными координатами. +Размер изображения отражения выбирается отдельно от размера основного кадра. +Terrain.dll в конструкторе CShade (RVA `0x278E0`, цикл с `0x27FC7`) берёт +ширину и высоту renderer через Ngi32.dll RVA `0x7FB0`. Сторона квадратной +текстуры — наибольшая степень двойки, не превосходящая меньшую из этих +величин и 1024; нижний предел равен 2. Например, экран 1280×1024 даёт +отражение 1024×1024, а 1280×720 — 512×512. Getter размеров и исходный цикл +выбора проверены исполнением DLL на десяти входах; создание нативной текстуры +в этом оракуле пропущено. Vulkan создаёт цвет и глубину с этой стороной и +задаёт каждому проходу собственные viewport и область очистки. Так уменьшение +отражения не обрезает основной кадр и воспроизводит исходный масштаб его +пикселей, от которого зависит чёткость берегов и ряби. + Цветовой target переходит из attachment layout в shader-read layout после записи. Следующий зеркальный проход ждёт предыдущего чтения этой текстуры; основной и отражённый виды используют разные слоты uniform buffer. Во время @@ -1196,9 +1208,10 @@ Rust-тест отдельно проверяет углы водоёма и гÑ В Vulkan карта хранится в двух каналах несжатой текстуры и возвращается к знаковым значениям перед вычислением смещения. -Матрица положения камеры, её окно и координаты волн восстановлены, но весь +Матрица положения камеры, её окно, размер текстуры и координаты волн восстановлены, но весь контекст отражённого вида ещё не повторён. Диапазон глубины, небо, туман и FX -заимствуются из основного вида, а размер target совпадает со swapchain. +заимствуются из основного вида. Для захвата с обрезанным viewport ещё не +подтверждено соответствие размера окна FParkan полному renderer оригинала. Эти ограничения учитываются в `planar_reflection_render_context_approx`; совпадение генератора волн и окна не доказывает совпадения итогового отражения. Один target обслуживает ближайшую |
