mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-13 20:05:52 -10:00
+24









deadinside
Qiming-Liu
AndiMajore
adds39939
iannitram
maiconjsv
whp199
DominickCobb-rs
Sarxzer
wheat32
ArenaMMORPG
SrShadowy
Gustavo Bomfim
Teefpyre
SrDeTs
IgorWarzocha
itachi-re
Bainianzzz
hideoneric
guitaripod
unTanana
andrR89
ayozetr
JuanraCM
SaeedShow
msisovic
axpico
Aryxa
Obrikash
noelmiller
AyrtonSanabio
Supermedo
Awaken
Claude Opus 5.5
0fc2921c45
- Windows 10/11: native bb-probe.exe (llvm-mingw / MSYS2 CLANG64), WPF launcher in five languages; online module online\bbnet20.dll with both pools (Seamless Hunt + BBSC, The Hunter's Dream with Discord sign-in) and BBSC's local game server; mods and XML patches from the launcher (junctions and hard links without Developer Mode); DLSS / DLSS FG, FSR 4.1.1 and FSR 4.1.2 driver models, FSR 4 from AMD's SDK 2.4 DLLs; NVAPI power profile on NVIDIA. - The old memory model of 0.3 removed; the game's graphics heaps grow on demand. - Faster: occlusion queries counted only inside the game's open queries (164 -> 189 FPS in the heaviest view), ring dedup and copy-shader filter (less PCIe traffic), PGO retrained, Steam Deck draw preparation, FSR 3.1 FG shaders. - Fixes: cloth fluttering above 30 FPS (#58), point-light shadow wedges, HUD item-icon grid, vertex explosions across heap pieces, FPS drop after a save switch, FG ghosts, crash 0x2169edc (#336), FSR 3.1 protected memory (#85), Wayland HiDPI, launcher translations (#381). - New: AA menu (FXAA2_GS, SMAA), FSR 1, GT-VBAO, AMD Anti-Lag 2, trophies.jsonl, shader recipes before play, live resolution by default. - Packages: Windows zip, AppImage, and a Linux folder (tar.zst) with the online module open. - Removed: the shadp2p online module, LEGO Dimensions toy pad and NGS2 code. - Credits: docs/CONTRIBUTORS_0.7.md (pull requests since 0.4 and outside work). Co-authored-by: Qiming-Liu <[email protected]> Co-authored-by: AndiMajore <[email protected]> Co-authored-by: adds39939 <[email protected]> Co-authored-by: iannitram <[email protected]> Co-authored-by: maiconjsv <[email protected]> Co-authored-by: whp199 <[email protected]> Co-authored-by: DominickCobb-rs <[email protected]> Co-authored-by: Sarxzer <[email protected]> Co-authored-by: wheat32 <[email protected]> Co-authored-by: ArenaMMORPG <[email protected]> Co-authored-by: SrShadowy <[email protected]> Co-authored-by: Gustavo Bomfim <[email protected]> Co-authored-by: Teefpyre <[email protected]> Co-authored-by: SrDeTs <[email protected]> Co-authored-by: IgorWarzocha <[email protected]> Co-authored-by: itachi-re <[email protected]> Co-authored-by: Bainianzzz <[email protected]> Co-authored-by: hideoneric <[email protected]> Co-authored-by: guitaripod <[email protected]> Co-authored-by: unTanana <[email protected]> Co-authored-by: andrR89 <[email protected]> Co-authored-by: ayozetr <[email protected]> Co-authored-by: JuanraCM <[email protected]> Co-authored-by: SaeedShow <[email protected]> Co-authored-by: msisovic <[email protected]> Co-authored-by: axpico <[email protected]> Co-authored-by: Aryxa <[email protected]> Co-authored-by: Obrikash <[email protected]> Co-authored-by: noelmiller <[email protected]> Co-authored-by: AyrtonSanabio <[email protected]> Co-authored-by: Supermedo <[email protected]> Co-authored-by: Awaken <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
485 lines
21 KiB
C++
485 lines
21 KiB
C++
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// bbport render: Rasterizer, the dynamic state of a draw (viewports and scissors, depth and
|
|
// stencil, primitive assembly, rasterization, blending), set on the scheduler's DynamicState
|
|
// (only what changed is recorded), and debug markers.
|
|
|
|
#include <mutex>
|
|
#include "shim/bb_env.h"
|
|
#include <unordered_map>
|
|
|
|
#include "render/raster_internal.h"
|
|
|
|
namespace Vulkan {
|
|
|
|
namespace {
|
|
|
|
/// PA_SC_SCREEN/WINDOW/GENERIC_SCISSOR intersected; the window offset moves the latter two.
|
|
Gcn::Scissor CombinedScissor(const Gcn::Regs& regs) {
|
|
const bool offset = !regs.window_scissor.window_offset_disable;
|
|
const s16 offset_x = offset ? regs.window_offset.window_x_offset : 0;
|
|
const s16 offset_y = offset ? regs.window_offset.window_y_offset : 0;
|
|
const auto top_left = [](s16 screen, s16 window, s16 generic, s16 window_offset) {
|
|
return std::max({screen, s16(window + window_offset), s16(generic + window_offset)});
|
|
};
|
|
const auto bottom_right = [](s16 screen, s16 window, s16 generic, s16 window_offset) {
|
|
return std::min({screen, s16(window + window_offset), s16(generic + window_offset)});
|
|
};
|
|
Gcn::Scissor scissor{};
|
|
scissor.top_left_x =
|
|
top_left(regs.screen_scissor.top_left_x, s16(regs.window_scissor.top_left_x),
|
|
s16(regs.generic_scissor.top_left_x), offset_x);
|
|
scissor.top_left_y =
|
|
top_left(regs.screen_scissor.top_left_y, s16(regs.window_scissor.top_left_y),
|
|
s16(regs.generic_scissor.top_left_y), offset_y);
|
|
scissor.bottom_right_x =
|
|
bottom_right(regs.screen_scissor.bottom_right_x, regs.window_scissor.bottom_right_x,
|
|
regs.generic_scissor.bottom_right_x, offset_x);
|
|
scissor.bottom_right_y =
|
|
bottom_right(regs.screen_scissor.bottom_right_y, regs.window_scissor.bottom_right_y,
|
|
regs.generic_scissor.bottom_right_y, offset_y);
|
|
return scissor;
|
|
}
|
|
|
|
/// The depth range of a viewport. With [-W, W] clip space the hardware centers it on zoffset
|
|
/// (as RADV programs it: radv_pipeline_graphics.c, radv_cmd_buffer.c).
|
|
std::pair<float, float> DepthRange(const Gcn::Regs& regs, const Gcn::ViewportBounds& vp,
|
|
bool unrestricted) {
|
|
const auto& ctl = regs.viewport_control;
|
|
const float zoffset = ctl.zoffset_enable ? vp.zoffset : 0.f;
|
|
const float zscale = ctl.zscale_enable ? vp.zscale : 1.f;
|
|
float min_depth = regs.clipper_control.clip_space == Gcn::ClipSpace::MinusWToW
|
|
? zoffset - zscale
|
|
: zoffset;
|
|
float max_depth = zoffset + zscale;
|
|
if (!unrestricted) {
|
|
min_depth = std::max(min_depth, 0.f);
|
|
max_depth = std::min(max_depth, 1.f);
|
|
}
|
|
return {min_depth, max_depth};
|
|
}
|
|
|
|
bool IsListTopology(Gcn::PrimitiveType type) {
|
|
switch (GcnToVk::PrimitiveType(type)) {
|
|
case vk::PrimitiveTopology::ePointList:
|
|
case vk::PrimitiveTopology::eLineList:
|
|
case vk::PrimitiveTopology::eTriangleList:
|
|
case vk::PrimitiveTopology::eLineListWithAdjacency:
|
|
case vk::PrimitiveTopology::eTriangleListWithAdjacency:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Patch lists, and the quad and rect lists drawn as patches (tessellation).
|
|
bool IsPatchListTopology(Gcn::PrimitiveType type) {
|
|
return type == Gcn::PrimitiveType::PatchPrimitive || type == Gcn::PrimitiveType::QuadList ||
|
|
type == Gcn::PrimitiveType::RectList;
|
|
}
|
|
|
|
/// A stencil face uses DB_STENCILREFMASK.STENCILOPVAL when one of its ops is REPLACE_OP.
|
|
bool UsesOpValue(Gcn::StencilFunc fail, Gcn::StencilFunc zpass, Gcn::StencilFunc zfail) {
|
|
return fail == Gcn::StencilFunc::ReplaceOp || zpass == Gcn::StencilFunc::ReplaceOp ||
|
|
zfail == Gcn::StencilFunc::ReplaceOp;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void Rasterizer::UpdateDynamicState(const GraphicsPipeline* pipeline, const bool is_indexed) const {
|
|
BB_SECTION(UpdateDynamicState);
|
|
UpdateViewportScissorState();
|
|
UpdateDepthStencilState();
|
|
UpdatePrimitiveState(is_indexed);
|
|
UpdateRasterizationState();
|
|
UpdateColorBlendingState(pipeline);
|
|
scheduler.GetDynamicState().CommitWith(
|
|
instance.IsDepthBoundsSupported(), instance.IsDynamicColorWriteMaskSupported(),
|
|
instance.IsAttachmentFeedbackLoopLayoutSupported(),
|
|
[&](auto&& command) { scheduler.Record(std::move(command)); });
|
|
}
|
|
|
|
void Rasterizer::UpdateViewportScissorState() const {
|
|
const auto& regs = Regs();
|
|
if (regs.polygon_control.enable_window_offset &&
|
|
(regs.window_offset.window_x_offset != 0 || regs.window_offset.window_y_offset != 0)) {
|
|
LOG_ERROR(Render_Vulkan,
|
|
"PA_SU_SC_MODE_CNTL.VTX_WINDOW_OFFSET_ENABLE support is not yet implemented.");
|
|
}
|
|
const Gcn::Scissor combined = CombinedScissor(regs);
|
|
const auto& ctl = regs.viewport_control;
|
|
const bool scaled = target_scale[0] != 1.0f || target_scale[1] != 1.0f;
|
|
|
|
boost::container::static_vector<vk::Viewport, Gcn::NUM_VIEWPORTS> viewports;
|
|
boost::container::static_vector<vk::Rect2D, Gcn::NUM_VIEWPORTS> scissors;
|
|
for (u32 i = 0; i < Gcn::NUM_VIEWPORTS; i++) {
|
|
const auto& vp = regs.viewports[i];
|
|
if (vp.xscale == 0) {
|
|
continue;
|
|
}
|
|
vk::Viewport viewport{};
|
|
std::tie(viewport.minDepth, viewport.maxDepth) =
|
|
DepthRange(regs, vp, instance.IsDepthRangeUnrestrictedSupported());
|
|
if (regs.IsClipDisabled()) {
|
|
// Screen-space positions: the translated vertex shader maps the hardware's whole
|
|
// window range [0, 16383] to NDC, the viewport covers it.
|
|
viewport.width = float(std::min<u32>(instance.GetMaxViewportWidth(), 16_KB));
|
|
viewport.height = float(std::min<u32>(instance.GetMaxViewportHeight(), 16_KB));
|
|
} else {
|
|
const float xoffset = ctl.xoffset_enable ? vp.xoffset : 0.f;
|
|
const float xscale = ctl.xscale_enable ? vp.xscale : 1.f;
|
|
const float yoffset = ctl.yoffset_enable ? vp.yoffset : 0.f;
|
|
const float yscale = ctl.yscale_enable ? vp.yscale : 1.f;
|
|
// Scaled to the target drawn into (the upscaler's output-size images); the sub-pixel
|
|
// jitter of scene geometry is the same shift as jittering the projection.
|
|
viewport.x = (xoffset - xscale) * target_scale[0] + draw_jitter[0];
|
|
viewport.y = (yoffset - yscale) * target_scale[1] + draw_jitter[1];
|
|
viewport.width = xscale * 2.0f * target_scale[0];
|
|
viewport.height = yscale * 2.0f * target_scale[1];
|
|
}
|
|
viewports.push_back(viewport);
|
|
|
|
Gcn::Scissor scissor = combined;
|
|
if (regs.mode_control.vport_scissor_enable) {
|
|
const auto& own = regs.viewport_scissors[i];
|
|
scissor.top_left_x = std::max(scissor.top_left_x, s16(own.top_left_x));
|
|
scissor.top_left_y = std::max(scissor.top_left_y, s16(own.top_left_y));
|
|
scissor.bottom_right_x =
|
|
std::min(Gcn::Scissor::Clamp(scissor.bottom_right_x), own.bottom_right_x);
|
|
scissor.bottom_right_y =
|
|
std::min(Gcn::Scissor::Clamp(scissor.bottom_right_y), own.bottom_right_y);
|
|
}
|
|
if (scaled) {
|
|
const auto scale = [](s32 v, float f) { return s32(std::lround(float(v) * f)); };
|
|
const s32 x0 = scale(scissor.top_left_x, target_scale[0]);
|
|
const s32 y0 = scale(scissor.top_left_y, target_scale[1]);
|
|
const s32 x1 = scale(scissor.top_left_x + s32(scissor.GetWidth()), target_scale[0]);
|
|
const s32 y1 = scale(scissor.top_left_y + s32(scissor.GetHeight()), target_scale[1]);
|
|
scissors.push_back({
|
|
.offset = {x0, y0},
|
|
.extent = {u32(std::max(x1 - x0, 0)), u32(std::max(y1 - y0, 0))},
|
|
});
|
|
} else {
|
|
scissors.push_back({
|
|
.offset = {scissor.top_left_x, scissor.top_left_y},
|
|
.extent = {scissor.GetWidth(), scissor.GetHeight()},
|
|
});
|
|
}
|
|
}
|
|
if (viewports.empty()) {
|
|
// Vulkan needs one: an empty one outside the target.
|
|
viewports.push_back({.x = -1.0f, .y = -1.0f, .width = 1.0f, .height = 1.0f,
|
|
.minDepth = 0.0f, .maxDepth = 1.0f});
|
|
scissors.push_back({.offset = {0, 0}, .extent = {1, 1}});
|
|
}
|
|
auto& dynamic_state = scheduler.GetDynamicState();
|
|
dynamic_state.SetViewports(viewports);
|
|
dynamic_state.SetScissors(scissors);
|
|
}
|
|
|
|
vk::Rect2D Rasterizer::ClearDrawRect(u32 width, u32 height) const {
|
|
const auto& regs = Regs();
|
|
const Gcn::Scissor combined = CombinedScissor(regs);
|
|
s32 x0 = Gcn::Scissor::Clamp(combined.top_left_x);
|
|
s32 y0 = Gcn::Scissor::Clamp(combined.top_left_y);
|
|
s32 x1 = Gcn::Scissor::Clamp(combined.bottom_right_x);
|
|
s32 y1 = Gcn::Scissor::Clamp(combined.bottom_right_y);
|
|
if (regs.mode_control.vport_scissor_enable) {
|
|
const auto& own = regs.viewport_scissors[0];
|
|
x0 = std::max<s32>(x0, own.top_left_x);
|
|
y0 = std::max<s32>(y0, own.top_left_y);
|
|
x1 = std::min<s32>(x1, own.bottom_right_x);
|
|
y1 = std::min<s32>(y1, own.bottom_right_y);
|
|
}
|
|
const auto& vp = regs.viewports[0];
|
|
const auto& ctl = regs.viewport_control;
|
|
if (!regs.IsClipDisabled() && vp.xscale != 0) {
|
|
const float xoffset = ctl.xoffset_enable ? vp.xoffset : 0.f;
|
|
const float xscale = std::abs(ctl.xscale_enable ? vp.xscale : 1.f);
|
|
const float yoffset = ctl.yoffset_enable ? vp.yoffset : 0.f;
|
|
const float yscale = std::abs(ctl.yscale_enable ? vp.yscale : 1.f);
|
|
x0 = std::max(x0, s32(std::floor(xoffset - xscale)));
|
|
y0 = std::max(y0, s32(std::floor(yoffset - yscale)));
|
|
x1 = std::min(x1, s32(std::ceil(xoffset + xscale)));
|
|
y1 = std::min(y1, s32(std::ceil(yoffset + yscale)));
|
|
}
|
|
x0 = std::clamp<s32>(x0, 0, s32(width));
|
|
y0 = std::clamp<s32>(y0, 0, s32(height));
|
|
x1 = std::clamp<s32>(x1, x0, s32(width));
|
|
y1 = std::clamp<s32>(y1, y0, s32(height));
|
|
return {.offset = {x0, y0}, .extent = {u32(x1 - x0), u32(y1 - y0)}};
|
|
}
|
|
|
|
void Rasterizer::UpdateDepthStencilState() const {
|
|
const auto& regs = Regs();
|
|
const auto& depth = regs.depth_control;
|
|
auto& dynamic_state = scheduler.GetDynamicState();
|
|
|
|
const bool depth_test = depth.depth_enable && regs.depth_buffer.DepthValid();
|
|
dynamic_state.SetDepthTestEnabled(depth_test);
|
|
if (depth_test) {
|
|
// A depth clear pass writes the clear value, not the fragments' depth.
|
|
dynamic_state.SetDepthWriteEnabled(depth.depth_write_enable &&
|
|
!regs.depth_render_control.depth_clear_enable);
|
|
dynamic_state.SetDepthCompareOp(GcnToVk::CompareOp(depth.depth_func));
|
|
}
|
|
dynamic_state.SetDepthBoundsTestEnabled(depth.depth_bounds_enable);
|
|
if (depth.depth_bounds_enable) {
|
|
dynamic_state.SetDepthBounds(regs.depth_bounds_min, regs.depth_bounds_max);
|
|
}
|
|
const bool depth_bias = regs.polygon_control.NeedsBias();
|
|
dynamic_state.SetDepthBiasEnabled(depth_bias);
|
|
if (depth_bias) {
|
|
const auto& offset = regs.poly_offset;
|
|
const bool front = regs.polygon_control.enable_polygon_offset_front;
|
|
dynamic_state.SetDepthBias(front ? offset.front_offset : offset.back_offset,
|
|
offset.depth_bias,
|
|
(front ? offset.front_scale : offset.back_scale) / 16.f);
|
|
}
|
|
|
|
const bool stencil_test = depth.stencil_enable && regs.depth_buffer.StencilValid();
|
|
dynamic_state.SetStencilTestEnabled(stencil_test);
|
|
if (!stencil_test) {
|
|
return;
|
|
}
|
|
const auto& sc = regs.stencil_control;
|
|
const bool two_sided = depth.backface_enable;
|
|
const StencilOps front_ops{
|
|
.fail_op = GcnToVk::StencilOp(sc.stencil_fail_front),
|
|
.pass_op = GcnToVk::StencilOp(sc.stencil_zpass_front),
|
|
.depth_fail_op = GcnToVk::StencilOp(sc.stencil_zfail_front),
|
|
.compare_op = GcnToVk::CompareOp(depth.stencil_ref_func),
|
|
};
|
|
const StencilOps back_ops = two_sided ? StencilOps{
|
|
.fail_op = GcnToVk::StencilOp(sc.stencil_fail_back),
|
|
.pass_op = GcnToVk::StencilOp(sc.stencil_zpass_back),
|
|
.depth_fail_op = GcnToVk::StencilOp(sc.stencil_zfail_back),
|
|
.compare_op = GcnToVk::CompareOp(depth.stencil_bf_func),
|
|
} : front_ops;
|
|
dynamic_state.SetStencilOps(front_ops, back_ops);
|
|
|
|
const auto front = regs.stencil_ref_front;
|
|
const auto back = two_sided ? regs.stencil_ref_back : regs.stencil_ref_front;
|
|
// Vulkan has one reference per face: a face replacing with STENCILOPVAL tests against it
|
|
// too (a conflict only when the test values differ).
|
|
const bool front_op =
|
|
UsesOpValue(sc.stencil_fail_front, sc.stencil_zpass_front, sc.stencil_zfail_front);
|
|
const bool back_op =
|
|
two_sided ? UsesOpValue(sc.stencil_fail_back, sc.stencil_zpass_back, sc.stencil_zfail_back)
|
|
: front_op;
|
|
const auto ref_conflict = [](Gcn::CompareFunc func, const Gcn::StencilRefMask& ref) {
|
|
return func != Gcn::CompareFunc::Always && func != Gcn::CompareFunc::Never &&
|
|
ref.stencil_test_val != ref.stencil_op_val;
|
|
};
|
|
if ((front_op && ref_conflict(depth.stencil_ref_func, front)) ||
|
|
(back_op && two_sided && ref_conflict(depth.stencil_bf_func, back))) {
|
|
LOG_WARNING(Render_Vulkan, "Stencil test requires test_val while ReplaceOp requires "
|
|
"op_val; the stencil test will use op_val");
|
|
}
|
|
dynamic_state.SetStencilReferences(front_op ? front.stencil_op_val : front.stencil_test_val,
|
|
back_op ? back.stencil_op_val : back.stencil_test_val);
|
|
// A stencil clear pass writes the clear value only.
|
|
const bool stencil_clear = regs.depth_render_control.stencil_clear_enable;
|
|
dynamic_state.SetStencilWriteMasks(stencil_clear ? 0U : front.stencil_write_mask,
|
|
stencil_clear ? 0U : back.stencil_write_mask);
|
|
dynamic_state.SetStencilCompareMasks(front.stencil_mask, back.stencil_mask);
|
|
}
|
|
|
|
void Rasterizer::UpdatePrimitiveState(const bool is_indexed) const {
|
|
const auto& regs = Regs();
|
|
const auto type = regs.primitive_type;
|
|
// Restart where the host supports it for the topology.
|
|
const bool restart = (regs.enable_primitive_restart & 1) != 0 &&
|
|
(instance.IsListRestartSupported() || !IsListTopology(type)) &&
|
|
(instance.IsPatchListRestartSupported() || !IsPatchListTopology(type));
|
|
ASSERT_MSG(!is_indexed || !restart || regs.primitive_restart_index == 0xFFFF ||
|
|
regs.primitive_restart_index == 0xFFFFFFFF,
|
|
"Primitive restart index other than -1 is not supported yet");
|
|
auto& dynamic_state = scheduler.GetDynamicState();
|
|
dynamic_state.SetPrimitiveRestartEnabled(restart);
|
|
dynamic_state.SetRasterizerDiscardEnabled(regs.clipper_control.dx_rasterization_kill);
|
|
dynamic_state.SetCullMode(GcnToVk::IsPrimitiveCulled(type)
|
|
? GcnToVk::CullMode(regs.polygon_control.CullingMode())
|
|
: vk::CullModeFlagBits::eNone);
|
|
dynamic_state.SetFrontFace(GcnToVk::FrontFace(regs.polygon_control.front_face));
|
|
}
|
|
|
|
void Rasterizer::UpdateRasterizationState() const {
|
|
scheduler.GetDynamicState().SetLineWidth(Regs().line_control.Width());
|
|
}
|
|
|
|
void Rasterizer::UpdateColorBlendingState(const GraphicsPipeline* pipeline) const {
|
|
auto& dynamic_state = scheduler.GetDynamicState();
|
|
dynamic_state.SetBlendConstants(Regs().blend_constants);
|
|
dynamic_state.SetColorWriteMasks(pipeline->GetGraphicsKey().write_masks);
|
|
dynamic_state.SetAttachmentFeedbackLoopEnabled(attachment_feedback_loop);
|
|
}
|
|
|
|
namespace {
|
|
enum class MarkerOp { Begin, End, Insert };
|
|
|
|
/// Records a debug label (BB_VK_MARKERS=1). The guest's labels are not NUL-terminated.
|
|
void RecordMarker(Scheduler& scheduler, MarkerOp op, std::string_view text,
|
|
std::optional<u32> color = {}) {
|
|
const std::string label{text};
|
|
vk::DebugUtilsLabelEXT info{.pLabelName = label.c_str()};
|
|
if (color) {
|
|
const u32 c = *color;
|
|
info.color = std::array<f32, 4>{f32((c >> 16) & 0xff) / 255.0f,
|
|
f32((c >> 8) & 0xff) / 255.0f, f32(c & 0xff) / 255.0f,
|
|
f32((c >> 24) & 0xff) / 255.0f};
|
|
}
|
|
const auto cmdbuf = scheduler.CommandBuffer();
|
|
switch (op) {
|
|
case MarkerOp::Begin:
|
|
cmdbuf.beginDebugUtilsLabelEXT(info);
|
|
break;
|
|
case MarkerOp::End:
|
|
cmdbuf.endDebugUtilsLabelEXT();
|
|
break;
|
|
case MarkerOp::Insert:
|
|
cmdbuf.insertDebugUtilsLabelEXT(info);
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool MarkersEnabled(bool from_guest) {
|
|
return from_guest ? EmulatorSettings.IsVkGuestMarkersEnabled()
|
|
: EmulatorSettings.IsVkHostMarkersEnabled();
|
|
}
|
|
} // namespace
|
|
|
|
// The command buffer is the recording thread's while it has draws: drained first.
|
|
void Rasterizer::ScopeMarkerBegin(const std::string_view& str, bool from_guest) {
|
|
if (MarkersEnabled(from_guest)) {
|
|
DrainDrawPipe();
|
|
RecordMarker(scheduler, MarkerOp::Begin, str);
|
|
}
|
|
}
|
|
|
|
void Rasterizer::ScopeMarkerEnd(bool from_guest) {
|
|
if (MarkersEnabled(from_guest)) {
|
|
DrainDrawPipe();
|
|
RecordMarker(scheduler, MarkerOp::End, {});
|
|
}
|
|
}
|
|
|
|
void Rasterizer::ScopedMarkerInsert(const std::string_view& str, bool from_guest) {
|
|
if (MarkersEnabled(from_guest)) {
|
|
DrainDrawPipe();
|
|
RecordMarker(scheduler, MarkerOp::Insert, str);
|
|
}
|
|
}
|
|
|
|
void Rasterizer::ScopedMarkerInsertColor(const std::string_view& str, const u32 color,
|
|
bool from_guest) {
|
|
if (MarkersEnabled(from_guest)) {
|
|
DrainDrawPipe();
|
|
RecordMarker(scheduler, MarkerOp::Insert, str, color);
|
|
}
|
|
}
|
|
|
|
std::thread::id Rasterizer::GetGpuCommandProcessorThread() {
|
|
return liverpool->GetGpuCommandProcessorThread();
|
|
}
|
|
|
|
|
|
} // namespace Vulkan
|
|
|
|
namespace Vulkan::DynCheck {
|
|
namespace {
|
|
/// Per command buffer, without locks (a global mutex hid the race): open addressing on the
|
|
/// handle; a buffer is recorded by one thread at a time, which alone touches its entry.
|
|
struct Shadow {
|
|
std::atomic<VkCommandBuffer> key{VK_NULL_HANDLE};
|
|
Viewports viewports;
|
|
Scissors scissors;
|
|
const char* viewports_by = "nothing";
|
|
const char* scissors_by = "nothing";
|
|
};
|
|
constexpr u32 Slots = 4096;
|
|
Shadow shadows[Slots];
|
|
std::atomic<u64> draws{0}, reported{0};
|
|
Shadow* Find(vk::CommandBuffer cmd) {
|
|
const VkCommandBuffer key = VkCommandBuffer(cmd);
|
|
u32 at = u32((reinterpret_cast<uintptr_t>(key) * 0x9E3779B97F4A7C15ull) >> 52) & (Slots - 1);
|
|
for (u32 i = 0; i < Slots; ++i, at = (at + 1) & (Slots - 1)) {
|
|
VkCommandBuffer seen = shadows[at].key.load(std::memory_order_acquire);
|
|
if (seen == key) {
|
|
return &shadows[at];
|
|
}
|
|
if (seen == VK_NULL_HANDLE &&
|
|
shadows[at].key.compare_exchange_strong(seen, key, std::memory_order_acq_rel)) {
|
|
return &shadows[at];
|
|
}
|
|
if (seen == key) {
|
|
return &shadows[at];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
std::string Text(const Scissors& s) {
|
|
std::string out;
|
|
for (const auto& r : s) {
|
|
out += fmt::format("({},{} {}x{})", r.offset.x, r.offset.y, r.extent.width, r.extent.height);
|
|
}
|
|
return out.empty() ? std::string{"none"} : out;
|
|
}
|
|
std::string Text(const Viewports& v) {
|
|
std::string out;
|
|
for (const auto& r : v) {
|
|
out += fmt::format("({:g},{:g} {:g}x{:g})", r.x, r.y, r.width, r.height);
|
|
}
|
|
return out.empty() ? std::string{"none"} : out;
|
|
}
|
|
} // namespace
|
|
|
|
bool Enabled() {
|
|
static const bool on = bb_env_on("BB_DYNSTATE_CHECK");
|
|
return on;
|
|
}
|
|
|
|
void Begin(vk::CommandBuffer cmd) {
|
|
if (Shadow* s = Find(cmd)) {
|
|
s->viewports.clear();
|
|
s->scissors.clear();
|
|
s->viewports_by = s->scissors_by = "begin";
|
|
}
|
|
}
|
|
|
|
void Set(vk::CommandBuffer cmd, const Scissors& scissors, const char* who) {
|
|
if (Shadow* s = Find(cmd)) {
|
|
s->scissors = scissors;
|
|
s->scissors_by = who;
|
|
}
|
|
}
|
|
|
|
void SetViewports(vk::CommandBuffer cmd, const Viewports& viewports, const char* who) {
|
|
if (Shadow* s = Find(cmd)) {
|
|
s->viewports = viewports;
|
|
s->viewports_by = who;
|
|
}
|
|
}
|
|
|
|
void Draw(vk::CommandBuffer cmd, const Viewports& viewports, const Scissors& scissors, u64 vs,
|
|
u64 ps) {
|
|
const u64 n = draws.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
const Shadow* s = Find(cmd);
|
|
if (s && (!std::ranges::equal(s->viewports, viewports) ||
|
|
!std::ranges::equal(s->scissors, scissors))) {
|
|
if (reported.fetch_add(1, std::memory_order_relaxed) < 400) {
|
|
std::printf("Dynstate check: frame %llu draw vs %016llx ps %016llx expects vp %s sc %s, "
|
|
"command buffer has vp %s (by %s) sc %s (by %s)\n",
|
|
(unsigned long long)BbStats::frame_number.load(std::memory_order_relaxed),
|
|
(unsigned long long)vs, (unsigned long long)ps, Text(viewports).c_str(),
|
|
Text(scissors).c_str(), Text(s->viewports).c_str(), s->viewports_by,
|
|
Text(s->scissors).c_str(), s->scissors_by);
|
|
}
|
|
}
|
|
if (n % 1000000 == 0) {
|
|
std::printf("Dynstate check: %llu draws, %llu mismatches\n", (unsigned long long)n,
|
|
(unsigned long long)reported.load(std::memory_order_relaxed));
|
|
}
|
|
}
|
|
} // namespace Vulkan::DynCheck
|