diff --git a/src/addons/devkit/addon.cpp b/src/addons/devkit/addon.cpp index d01c6c825..30cc569ab 100644 --- a/src/addons/devkit/addon.cpp +++ b/src/addons/devkit/addon.cpp @@ -700,7 +700,34 @@ std::string setting_device_proxy_compile_error = {}; bool g_device_proxy_dx9ex_bootstrap_pending = false; bool g_device_proxy_disable_flip_bootstrap_pending = false; -int pending_draw_index_focus = -1; +// Tracks pending scroll-to-shader navigation in the snapshot pane. +struct SnapshotNavigation { + int target_draw_index = -1; + uint32_t target_shader_hash = 0; // 0 = draw-level nav + int source_model_row = -1; // snapshot_rows index of the row that was clicked + float source_scroll_y = 0.f; // GetScrollY() at click time + + void Clear() { + target_draw_index = -1; + target_shader_hash = 0; + source_model_row = -1; + source_scroll_y = 0.f; + } + + [[nodiscard]] bool IsPending() const { return target_draw_index >= 0; } + [[nodiscard]] bool HasShaderTarget() const { return target_shader_hash != 0; } + + void ScrollToRow(int target_model_row, float row_height) { + if (source_model_row >= 0 && target_model_row >= 0) { + int row_delta = target_model_row - source_model_row; + ImGui::SetScrollY(source_scroll_y + (static_cast(row_delta) * row_height)); + } else { + ImGui::SetScrollHereY(0.5f); + } + + Clear(); + } +} snapshot_nav; struct SettingSelection { uint32_t shader_hash = 0; @@ -3787,7 +3814,10 @@ void RenderCapturePane(reshade::api::device* device, DeviceData* data) { ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); - const float snapshot_row_height = ImGui::GetFrameHeight(); + // Row height must include CellPadding.y on both sides. Using the wrong + // value causes the ImGuiListClipper to underreport total content height, + // clipping the bottom rows and miscalculating scroll-to-row offsets. + const float snapshot_row_height = ImGui::GetFrameHeight() + (ImGui::GetStyle().CellPadding.y * 2.0f); const uint32_t snapshot_row_layout_key = (snapshot_pane_show_vertex_shaders ? 1u << 0u : 0u) | (snapshot_pane_show_pixel_shaders ? 1u << 1u : 0u) @@ -3795,11 +3825,26 @@ void RenderCapturePane(reshade::api::device* device, DeviceData* data) { | (snapshot_pane_filter_resources_by_shader_use ? 1u << 3u : 0u) | (snapshot_pane_expand_all_nodes ? 1u << 4u : 0u); + int current_model_row = -1; + + // Called from render_draw_row after the tree node. For draw-level nav, + // scrolls here. For shader-level nav, defers to focus_pending_shader. const auto focus_pending_draw = [&](int draw_index) { - if (draw_index == pending_draw_index_focus) { + if (draw_index == snapshot_nav.target_draw_index) { + if (snapshot_nav.HasShaderTarget()) return; + ImGui::SetItemDefaultFocus(); - ImGui::SetScrollHereY(); - pending_draw_index_focus = -1; + snapshot_nav.ScrollToRow(current_model_row, snapshot_row_height); + } + }; + + // Called from render_shader_row. Matches on draw index + shader hash + // to scroll to the exact shader row, not just the parent draw. + const auto focus_pending_shader = [&](const SnapshotRow& row) { + if (snapshot_nav.HasShaderTarget() + && row.draw_index == snapshot_nav.target_draw_index + && row.shader_hash == snapshot_nav.target_shader_hash) { + snapshot_nav.ScrollToRow(current_model_row, snapshot_row_height); } }; @@ -3930,13 +3975,17 @@ void RenderCapturePane(reshade::api::device* device, DeviceData* data) { ImGui::EndDisabled(); }; - ImGui::PushID(row.draw_index); + ImGui::PushID(row.id_seed); render_nav_button("##prev", !has_prev, has_prev ? *prev_it : -1); render_nav_button("##next", !has_next, has_next ? *next_it : -1); ImGui::PopID(); if (new_index != -1) { - pending_draw_index_focus = new_index; + snapshot_nav.target_draw_index = new_index; + snapshot_nav.target_shader_hash = row.shader_hash; + snapshot_nav.source_model_row = current_model_row; + snapshot_nav.source_scroll_y = ImGui::GetScrollY(); + MakeSelectionCurrent(selection); } } @@ -3961,7 +4010,7 @@ void RenderCapturePane(reshade::api::device* device, DeviceData* data) { ImGui::TreeNodeEx("", bullet_flags, "%s", s.str().c_str()); } ImGui::PopID(); - if (pending_draw_index_focus == -1) { + if (!snapshot_nav.IsPending()) { if (ImGui::IsItemClicked()) { MakeSelectionCurrent(selection); ImGui::SetItemDefaultFocus(); @@ -3984,6 +4033,8 @@ void RenderCapturePane(reshade::api::device* device, DeviceData* data) { ImGui::TextUnformatted(pipeline_details_ptr->tag->c_str()); } } + + focus_pending_shader(row); }; const auto render_srv_row = [&](const SnapshotRow& row, bool& next_tree_open) { @@ -4763,6 +4814,7 @@ void RenderCapturePane(reshade::api::device* device, DeviceData* data) { sync_snapshot_tree_stack(ancestor_chain); ImGui::TableNextRow(ImGuiTableRowFlags_None, snapshot_row_height); ++rendered_row_count; + current_model_row = row_model_index; bool next_tree_open = false; @@ -5170,17 +5222,59 @@ void RenderCapturePane(reshade::api::device* device, DeviceData* data) { data->snapshot_row_layout_key = snapshot_row_layout_key; data->snapshot_rows_valid = true; }; + + // Shader rows only exist in the model when their parent draw's tree is + // open. Force the target draw open so rebuild_snapshot_rows includes the + // shader children we need to scroll to. + if (snapshot_nav.IsPending() && snapshot_nav.HasShaderTarget()) { + ImGui::PushID(snapshot_nav.target_draw_index); + ImGui::GetStateStorage()->SetInt(ImGui::GetID(""), 1); + ImGui::PopID(); + data->snapshot_rows_valid = false; + } if (!data->snapshot_rows_valid || data->snapshot_row_layout_key != snapshot_row_layout_key) { rebuild_snapshot_rows(); } int rendered_row_count = 0; - if (pending_draw_index_focus != -1 || data->snapshot_rows.empty()) { - rendered_row_count = render_capture_pane_rows( - 0, - static_cast(data->snapshot_rows.size())); - } else { + { ImGuiListClipper clipper; clipper.Begin(static_cast(data->snapshot_rows.size()), snapshot_row_height); + if (snapshot_nav.IsPending()) { + int draw_row_idx = -1; + int shader_row_idx = -1; + + for (int i = 0; i < static_cast(data->snapshot_rows.size()); ++i) { + const auto& r = data->snapshot_rows[static_cast(i)]; + + if (draw_row_idx < 0 + && r.kind == SnapshotRow::Kind::DRAW + && r.draw_index == snapshot_nav.target_draw_index) { + draw_row_idx = i; + } + + if (snapshot_nav.HasShaderTarget() + && r.kind == SnapshotRow::Kind::SHADER + && r.draw_index == snapshot_nav.target_draw_index + && r.shader_hash == snapshot_nav.target_shader_hash) { + shader_row_idx = i; + } + + if (draw_row_idx >= 0 && (snapshot_nav.target_shader_hash == 0 || shader_row_idx >= 0)) break; + } + + if (draw_row_idx < 0) { + snapshot_nav = {}; + } else { + // If we searched and the shader row doesn't exist, fall back to + // draw-level scroll. + if (snapshot_nav.HasShaderTarget() && shader_row_idx < 0) { + snapshot_nav.target_shader_hash = 0; + } + + clipper.IncludeItemByIndex(draw_row_idx); + if (shader_row_idx >= 0) clipper.IncludeItemByIndex(shader_row_idx); + } + } while (clipper.Step()) { rendered_row_count += render_capture_pane_rows( clipper.DisplayStart, @@ -5208,8 +5302,9 @@ inline void CreateDrawIndexLink(const std::string& label, int draw_index, const ImGui::PushStyleColor(ImGuiCol_Text, *color); } if (ImGui::TextLink(label.c_str())) { - setting_nav_item = 0; // Snapshot is the first nav item - pending_draw_index_focus = draw_index; + setting_nav_item = 0; + snapshot_nav.Clear(); + snapshot_nav.target_draw_index = draw_index; } if (color != nullptr) { ImGui::PopStyleColor();