fix(render): match native reflection target sizes
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s

This commit is contained in:
Valentin Popov committed 2026-11-01 08:26:46 +04:00
1 parent 0f8b14abbf
commit 651f5e10a7
5 files changed
+68 -16

No files matched your search

@@ -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)",
+19 -12
View File
@@ -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
+29
View File
@@ -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]> {
@@ -116,6 +127,24 @@ pub fn legacy_embm_reflection_clip(
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 {
+1 -1
View File
@@ -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`; сама семантика оставшихся специальных проходов ещё не установлена.
+15 -2
View File
@@ -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 обслуживает ближайшую