Files
bloodborne_pc/tests/test_scene_resolution.cpp
deadinsideandClaude Opus 5.5 bf95396436 0.2: Steam Deck fixes, cheaper live scaling, TAA flicker fix, mods, English launcher
Steam Deck and weak GPUs
- Outputs other than 1080p render at the preset size again (startup patch, as in 0.1);
  live resolution changes are now a setting (live_resolution=0|1|auto, off by default).
- Texture cache on integrated GPUs: thresholds follow the driver's current memory budget.
  The old budget (~1 GB on the Deck) kept the collector in its aggressive mode, evicting
  textures used 2-3 frames ago on every submission.
- Evicted GPU-written images are written back synchronously, while their pages are still
  protected (a deferred write could land over memory the game had reused).
- run.sh uses bash builtins only (the AppImage has no sed: the game did not start on NixOS).

Rendering
- Live scaling also reduces the game's half-resolution post-processing (960x540, including
  the bloom mip chain, one proxy per level).
- TAA: history of thin surfaces missed by a jitter phase is kept when nothing moves
  (railings, window bars: static-camera flicker p99.9 -45%).
- Scene texture LOD bias log2(render/output) in G-buffer passes while upscaling.
- Present on its own thread and at most one frame of GPU run-ahead (even frame pacing).
- Pipeline cache rebuilt instead of disabled when the shader profile changes.
- GPU idle only after the draw pipeline and deferred fences are done (work_retired).
- Recompiler fixes backported from shadPS4; integer interpolation fix for pre-Turing NVIDIA.
- Sharpness up to 2.0 (an extra RCAS pass); native-resolution TAA mode.

Launcher, mods, patches
- English launcher (follows the system language), loose-file mods with load order,
  third-party shadPS4 XML patches, 60/90 FPS patches with pointer slots fixed.
- AppImage: host NVIDIA Vulkan driver support, MangoHud, no FSR 4.1.1 models
  (BB_PACKAGE_FSR411=1 bundles local ones).

Tools: route recording/replay (F9), consecutive frame dumps, scene scaling diagnostics.
Details: docs/CHANGES_2026-10-02.md, docs/CHANGES_2026-10-03.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 08:17:26 +03:00

202 lines
11 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
// Exercises the production target/resolve path on Vulkan (Lavapipe works, no game/window).
#include <cassert>
#include <cmath>
#include <cstdio>
#include <cstring>
#include "common/slot_vector.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_runtime.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/renderer_vulkan/vk_scene_resolution.h"
#include "video_core/texture_cache/blit_helper.h"
#include <vk_mem_alloc.h>
int main() {
using namespace Vulkan;
Instance instance(0, false);
// Calls emitted by this executable use a local dispatcher. libbbgpu.so initializes
// its own default dispatcher while constructing Instance.
static vk::detail::DynamicLoader loader;
vk::detail::DispatchLoaderDynamic dispatch;
dispatch.init(
loader.getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr"));
dispatch.init(instance.GetInstance());
dispatch.init(instance.GetDevice());
Scheduler scheduler(instance);
Runtime runtime(instance, scheduler);
Common::SlotVector<VideoCore::ImageView> views;
Common::SlotVector<VideoCore::Image> images;
VideoCore::ImageInfo ci;
ci.size = {1920,1080,1};
ci.resources = {1,1};
ci.type = AmdGpu::ImageType::Color2D;
ci.pixel_format = vk::Format::eR8G8B8A8Unorm;
ci.num_bits = 32;
const auto color_id = images.insert(instance, runtime, views, ci);
auto di = ci;
di.pixel_format = vk::Format::eD32SfloatS8Uint;
di.props.is_depth = di.props.has_stencil = true;
const auto depth_id = images.insert(instance, runtime, views, di);
SceneTargets targets(instance, scheduler, runtime, [&](VideoCore::ImageId id, u64 uid) {
if (!images.is_allocated(id)) return static_cast<VideoCore::Image*>(nullptr);
return !uid || images[id].image_uid == uid ? &images[id] : nullptr;
});
const bool color_eligible = targets.Eligible(images[color_id]);
const bool depth_eligible = targets.Eligible(images[depth_id]);
std::fprintf(stderr, "Scene target eligibility: color=%d depth=%d\n",
color_eligible, depth_eligible);
if (!color_eligible || !depth_eligible) {
std::puts("Scene targets: unsupported format blit, live scaling skipped");
return 0;
}
VkBuffer readback{};
VmaAllocation allocation{};
VmaAllocationInfo ai{};
const VkBufferCreateInfo bi{.sType=VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size=16, .usage=VK_BUFFER_USAGE_TRANSFER_DST_BIT};
const VmaAllocationCreateInfo ac{.flags=VMA_ALLOCATION_CREATE_MAPPED_BIT |
VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, .usage=VMA_MEMORY_USAGE_AUTO,
.requiredFlags=VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
assert(vmaCreateBuffer(instance.GetAllocator(), &bi, &ac, &readback, &allocation, &ai) == VK_SUCCESS);
const auto pixel = [&](vk::Image image, vk::ImageAspectFlagBits aspect,
vk::ImageLayout layout, u32 w, u32 h) {
scheduler.EndRendering();
const auto cmd = scheduler.CommandBuffer();
const vk::MemoryBarrier2 b{.srcStageMask=vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask=vk::AccessFlagBits2::eMemoryWrite,
.dstStageMask=vk::PipelineStageFlagBits2::eTransfer,
.dstAccessMask=vk::AccessFlagBits2::eTransferRead};
cmd.pipelineBarrier2({.memoryBarrierCount=1,.pMemoryBarriers=&b}, dispatch);
const vk::BufferImageCopy region{.imageSubresource={aspect,0,0,1},
.imageOffset={s32(w-1),s32(h-1),0},.imageExtent={1,1,1}};
cmd.copyImageToBuffer(image, layout, readback, region, dispatch);
const vk::MemoryBarrier2 host{.srcStageMask=vk::PipelineStageFlagBits2::eTransfer,
.srcAccessMask=vk::AccessFlagBits2::eTransferWrite,
.dstStageMask=vk::PipelineStageFlagBits2::eHost,
.dstAccessMask=vk::AccessFlagBits2::eHostRead};
cmd.pipelineBarrier2({.memoryBarrierCount=1,.pMemoryBarriers=&host}, dispatch);
scheduler.Finish();
u32 result;
std::memcpy(&result, ai.pMappedData, 4);
return result;
};
VideoCore::ImageViewInfo cv;
cv.format = ci.pixel_format;
auto dv = cv;
dv.format = di.pixel_format;
const auto clear_native = [&](u32 rgba, u32 stencil = 7) {
vk::ClearValue clear{};
clear.color.float32 = std::array{float(rgba & 255)/255, float((rgba>>8)&255)/255,
float((rgba>>16)&255)/255, 1.0f};
runtime.ClearImage(&images[color_id], {}, clear);
runtime.Transit(&images[depth_id], vk::ImageLayout::eTransferDstOptimal,
vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferWrite);
runtime.FlushBarriers();
scheduler.CommandBuffer().clearDepthStencilImage(
images[depth_id].GetImage(), vk::ImageLayout::eTransferDstOptimal,
vk::ClearDepthStencilValue{0.25f, stencil},
vk::ImageSubresourceRange{vk::ImageAspectFlagBits::eDepth |
vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1}, dispatch);
};
constexpr u32 stencil_values[] = {0, 255, 128, 85, 170, 7, 13, 255, 0, 170};
u32 stencil_index = 0;
for (int preset : {3,1,4,2,0,3,0,1,0,3}) {
const u32 copied_stencil = stencil_values[stencil_index++];
const u32 resolved_stencil = copied_stencil ^ 255;
const auto output = stencil_index > 7 ? SceneResolution::Size{3840,2160}
: SceneResolution::Size{};
const auto size = SceneResolution::ForPreset(preset, output);
targets.SetSize(size);
assert(SceneResolution::Unpack(SceneResolution::Pack(size)) == size);
clear_native(0xffff0000, copied_stencil); // blue
auto c = targets.Read(color_id,cv);
assert(pixel(c.image,vk::ImageAspectFlagBits::eColor,c.layout,size.width,size.height)==0xffff0000);
auto d = targets.Read(depth_id,dv);
const u32 depth_bits = pixel(d.image,vk::ImageAspectFlagBits::eDepth,d.layout,size.width,size.height);
assert(std::bit_cast<float>(depth_bits)==0.25f);
assert((pixel(d.image,vk::ImageAspectFlagBits::eStencil,d.layout,size.width,size.height)&255)==copied_stencil);
c = targets.Attachment(color_id,cv);
d = targets.Attachment(depth_id,dv);
auto cmd = scheduler.CommandBuffer();
const vk::RenderingAttachmentInfo color{.imageView=c.view,.imageLayout=c.layout,
.loadOp=vk::AttachmentLoadOp::eClear,.storeOp=vk::AttachmentStoreOp::eStore,
.clearValue=vk::ClearValue{.color={.float32=std::array{1.f,0.f,0.f,1.f}}}};
const vk::RenderingAttachmentInfo depth{.imageView=d.view,.imageLayout=d.layout,
.loadOp=vk::AttachmentLoadOp::eClear,.storeOp=vk::AttachmentStoreOp::eStore,
.clearValue=vk::ClearValue{.depthStencil={0.75f,resolved_stencil}}};
cmd.beginRendering({.renderArea={{0,0},{size.width,size.height}},.layerCount=1,
.colorAttachmentCount=1,.pColorAttachments=&color,
.pDepthAttachment=&depth,.pStencilAttachment=&depth}, dispatch);
cmd.endRendering(dispatch);
runtime.Transit(&images[color_id],vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits2::eTransfer,vk::AccessFlagBits2::eTransferRead);
runtime.FlushBarriers();
assert(pixel(images[color_id].GetImage(),vk::ImageAspectFlagBits::eColor,
vk::ImageLayout::eTransferSrcOptimal,1920,1080)==0xff0000ff);
runtime.Transit(&images[depth_id],vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits2::eTransfer,vk::AccessFlagBits2::eTransferRead);
runtime.FlushBarriers();
assert(std::bit_cast<float>(pixel(images[depth_id].GetImage(),vk::ImageAspectFlagBits::eDepth,
vk::ImageLayout::eTransferSrcOptimal,1920,1080))==0.75f);
assert((pixel(images[depth_id].GetImage(),vk::ImageAspectFlagBits::eStencil,
vk::ImageLayout::eTransferSrcOptimal,1920,1080)&255)==resolved_stencil);
clear_native(0xff00ff00); // overwrite after reduced render: proxy must be invalidated
c=targets.Read(color_id,cv);
assert(pixel(c.image,vk::ImageAspectFlagBits::eColor,c.layout,size.width,size.height)==0xff00ff00);
std::printf("Scene targets: %ux%u color/depth/stencil roundtrip PASS\n",size.width,size.height);
}
// Half-resolution mip chain (the game's bloom pyramid): one proxy per level, scaled by the
// scene's factor, resolved into its own native level.
auto hi = ci;
hi.size = {960, 540, 1};
hi.resources = {4, 1};
const auto half_id = images.insert(instance, runtime, views, hi);
const auto scene = SceneResolution::ForPreset(4); // 640x360 (x3 of 1080p)
targets.SetSize(scene);
auto& half = images[half_id];
assert(targets.Eligible(half) && !targets.EligibleScene(half));
assert((targets.ProxySize(half, 0) == SceneResolution::Size{320, 180}));
assert((targets.ProxySize(half, 2) == SceneResolution::Size{80, 45}));
auto lv = cv;
lv.range.base.level = 2;
const auto proxy = targets.Attachment(half_id, lv);
{
auto cmd = scheduler.CommandBuffer();
const vk::RenderingAttachmentInfo color{.imageView=proxy.view,.imageLayout=proxy.layout,
.loadOp=vk::AttachmentLoadOp::eClear,.storeOp=vk::AttachmentStoreOp::eStore,
.clearValue=vk::ClearValue{.color={.float32=std::array{0.f,1.f,0.f,1.f}}}};
cmd.beginRendering({.renderArea={{0,0},{80,45}},.layerCount=1,
.colorAttachmentCount=1,.pColorAttachments=&color}, dispatch);
cmd.endRendering(dispatch);
}
assert(targets.ProxyCurrent(half, 2, 1) && !targets.ProxyCurrent(half, 0, 4));
// A native access resolves the level: its last texel (239, 134) is the proxy's colour.
runtime.Transit(&half, vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferRead);
runtime.FlushBarriers();
{
scheduler.EndRendering();
const auto cmd = scheduler.CommandBuffer();
const vk::MemoryBarrier2 b{.srcStageMask=vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask=vk::AccessFlagBits2::eMemoryWrite,
.dstStageMask=vk::PipelineStageFlagBits2::eTransfer,
.dstAccessMask=vk::AccessFlagBits2::eTransferRead};
cmd.pipelineBarrier2({.memoryBarrierCount=1,.pMemoryBarriers=&b}, dispatch);
const vk::BufferImageCopy region{.imageSubresource={vk::ImageAspectFlagBits::eColor,2,0,1},
.imageOffset={239,134,0},.imageExtent={1,1,1}};
cmd.copyImageToBuffer(half.GetImage(), vk::ImageLayout::eTransferSrcOptimal, readback,
region, dispatch);
scheduler.Finish();
u32 result;
std::memcpy(&result, ai.pMappedData, 4);
assert(result == 0xff00ff00);
}
std::puts("Scene targets: half-resolution mip level proxy roundtrip PASS");
scheduler.Finish();
vmaDestroyBuffer(instance.GetAllocator(),readback,allocation);
}