Files
spice2x.github.io/src/spice2x/overlay/windows/generic_sub.cpp
T
bicarus f119d7c988 overlay: fix z-ordering of subscreen windows (#710)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Subscreen overlay window used to draw above the main game surface, but
below any other ImGui widgets. Fix this so that the subscreen image is
drawn at the same z-level as the sub window itself.

This has been a long-standing issue - at least since 2023 when spice2x
first forked...

## Testing
Tested TDJ in windowed mode.
2026-05-29 02:03:31 -07:00

263 lines
10 KiB
C++

#undef CINTERFACE
#include "generic_sub.h"
#include "avs/game.h"
#include "games/io.h"
#include "cfg/screen_resize.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "hooks/graphics/backends/d3d9/d3d9_device.h"
#include "hooks/graphics/graphics.h"
#include "util/logging.h"
#include "util/utils.h"
#include "touch/touch.h"
int GENERIC_SUB_WINDOW_X = 0;
int GENERIC_SUB_WINDOW_Y = 0;
int GENERIC_SUB_WINDOW_WIDTH = 0;
int GENERIC_SUB_WINDOW_HEIGHT = 0;
bool GENERIC_SUB_WINDOW_FULLSIZE = false;
// #define OVERLAYDBG 1
namespace overlay::windows {
const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f);
const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f);
GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) {
this->remove_window_padding = true;
// ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image
this->flags = ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBackground |
ImGuiWindowFlags_NoDocking;
this->size_max = ImVec2(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y);
this->size_min = ImVec2(240, 135 + ImGui::GetFrameHeight());
this->init_size = size_min;
this->resize_callback = keep_16_by_9;
this->toggle_button = games::OverlayButtons::ToggleSubScreen;
this->texture_width = 0;
this->texture_height = 0;
overlay->set_subscreen_mouse_handler([this](LONG *x, LONG *y) -> bool {
// convert to normalized form (relative window coordinates 0.f-1.f)
ImVec2 xy;
// log_misc("sub::overlay", "mouse handler {} {}", to_string(*x), to_string(*y));
// log_misc("sub::overlay", "spicetouch coords {} {} {} {}", to_string(SPICETOUCH_TOUCH_X), to_string(SPICETOUCH_TOUCH_Y), to_string(SPICETOUCH_TOUCH_WIDTH), to_string(SPICETOUCH_TOUCH_HEIGHT));
float ratio_x, ratio_y;
if (GRAPHICS_WINDOWED) {
// input coords are relative to spicetouch wnd
ratio_x = (float)*x / SPICETOUCH_TOUCH_WIDTH;
ratio_y = (float)*y / SPICETOUCH_TOUCH_HEIGHT;
} else {
// inputs coords are relative to (0,0) for non-windowed mode
ratio_x = (float)*x / ImGui::GetIO().DisplaySize.x;
ratio_y = (float)*y / ImGui::GetIO().DisplaySize.y;
}
// log_misc("sub::overlay", "game coords {} {}", to_string(ratio_x), to_string(ratio_y));
// transform to subscreen overlay coords
if (!GENERIC_SUB_WINDOW_FULLSIZE) {
ratio_x = (ratio_x * ImGui::GetIO().DisplaySize.x - GENERIC_SUB_WINDOW_X) / GENERIC_SUB_WINDOW_WIDTH;
ratio_y = (ratio_y * ImGui::GetIO().DisplaySize.y - GENERIC_SUB_WINDOW_Y) / GENERIC_SUB_WINDOW_HEIGHT;
// log_misc("sub::overlay", "overlay coords {} {} {} {}", to_string(GENERIC_SUB_WINDOW_X), to_string(GENERIC_SUB_WINDOW_Y), to_string(GENERIC_SUB_WINDOW_WIDTH), to_string(GENERIC_SUB_WINDOW_HEIGHT));
}
xy.x = ratio_x;
xy.y = ratio_y;
// log_misc("sub::overlay", "ratio {} {}", to_string(xy.x), to_string(xy.y));
// x/y can be outside of window
if (xy.x < 0.f || 1.f < xy.x || xy.y < 0.f || 1.f < xy.y) {
return false;
}
// call into child
this->touch_transform(xy, x, y);
return true;
});
}
void GenericSubScreen::touch_transform(const ImVec2 xy_in, LONG *x_out, LONG *y_out) {}
void GenericSubScreen::check_for_errors() {}
void GenericSubScreen::build_content() {
this->flags |= ImGuiWindowFlags_NoBackground;
check_for_errors();
if (this->disabled_message.has_value()) {
this->draws_window = true;
this->flags &= ~ImGuiWindowFlags_NoBackground;
ImGui::TextColored(YELLOW, "%s", this->disabled_message.value().c_str());
ImGui::TextUnformatted("");
if (ImGui::Button("Close")) {
this->set_active(false);
}
return;
}
this->draw_texture();
if (!this->texture) {
this->draws_window = true;
this->flags &= ~ImGuiWindowFlags_NoBackground;
ImGui::TextColored(YELLOW, "Failed to acquire surface texture for subscreen.");
if (avs::game::is_model("LDJ")) {
ImGui::TextColored(
YELLOW,
"Ensure that you are using the correct DLL type, \n"
"or patch the DLL to properly enable TDJ mode.");
} else if (avs::game::is_model("KFC")) {
ImGui::TextColored(
YELLOW,
"Ensure that you did not accidentally enable a patch \n"
"that turns off subscreen rendering.");
}
ImGui::TextUnformatted("");
if (ImGui::Button("Close")) {
this->set_active(false);
}
}
#if OVERLAYDBG
if (this->status_message.has_value()) {
log_warning("sub::overlay", "{}", this->status_message.value().c_str());
}
if (this->status_message.has_value()) {
ImGui::TextColored(YELLOW, "%s", this->status_message.value().c_str());
} else if (this->texture) {
ImGui::TextColored(WHITE, "Successfully acquired surface texture");
} else {
ImGui::TextColored(YELLOW, "Failed to acquire surface texture");
}
#endif
}
bool GenericSubScreen::build_texture(IDirect3DSurface9 *surface, UINT width, UINT height) {
HRESULT hr;
D3DSURFACE_DESC desc {};
hr = surface->GetDesc(&desc);
if (FAILED(hr)) {
this->status_message = fmt::format("Failed to get surface descriptor, hr={}", FMT_HRESULT(hr));
return false;
}
hr = this->device->CreateTexture(width, height, 0, desc.Usage, desc.Format,
desc.Pool, &this->texture, nullptr);
if (FAILED(hr)) {
this->status_message = fmt::format("Failed to create render target, hr={}", FMT_HRESULT(hr));
return false;
}
this->texture_width = width;
this->texture_height = height;
return true;
}
void GenericSubScreen::draw_texture() {
HRESULT hr;
auto surface = graphics_d3d9_ldj_get_sub_screen();
if (surface == nullptr) {
return;
}
if (this->draws_window) {
// calculate the **content** location and size.
// GetContentRegionAvail returns the correct dimension, up to (and including) the resize handle
// don't be tempted to change it to some other ImGui routine that accomplishes similar things!
overlay_content_top_left = ImGui::GetCursorScreenPos();
overlay_content_size = ImGui::GetContentRegionAvail();
} else {
// no window, full screen
overlay_content_top_left = ImVec2(0, 0);
overlay_content_size = ImGui::GetIO().DisplaySize;
}
GENERIC_SUB_WINDOW_X = overlay_content_top_left.x;
GENERIC_SUB_WINDOW_Y = overlay_content_top_left.y;
GENERIC_SUB_WINDOW_WIDTH = overlay_content_size.x;
GENERIC_SUB_WINDOW_HEIGHT = overlay_content_size.y;
if (this->draws_window &&
this->texture &&
((UINT)overlay_content_size.x != this->texture_width)) {
#if OVERLAYDBG
log_info("sub::overlay", "resize {} != {} ", overlay_content_size.x, this->texture_width);
#endif
// hack needed for SDVX; resizing the texture results in darker image, so allocate texture
// again with the new size when the window is resized
this->texture->Release();
this->texture = nullptr;
this->texture_width = 0;
this->texture_height = 0;
}
if (this->texture == nullptr) {
if (!this->build_texture(surface, overlay_content_size.x, overlay_content_size.y)) {
this->texture = nullptr;
this->texture_width = 0;
this->texture_height = 0;
surface->Release();
return;
}
}
IDirect3DSurface9 *texture_surface = nullptr;
hr = this->texture->GetSurfaceLevel(0, &texture_surface);
if (FAILED(hr)) {
this->status_message = fmt::format("Failed to get texture surface, hr={}", FMT_HRESULT(hr));
surface->Release();
return;
}
hr = this->device->StretchRect(surface, nullptr, texture_surface, nullptr, D3DTEXF_LINEAR);
if (FAILED(hr)) {
this->status_message = fmt::format("Failed to copy back buffer contents, hr={}", FMT_HRESULT(hr));
surface->Release();
texture_surface->Release();
return;
}
surface->Release();
texture_surface->Release();
// draw the subscreen into the current window's draw list so it participates in
// normal ImGui z-ordering (i.e. other ImGui windows can appear above it)
auto bottom_right = overlay_content_size;
bottom_right.x += overlay_content_top_left.x;
bottom_right.y += overlay_content_top_left.y;
auto draw_list = this->draws_window
? ImGui::GetWindowDrawList()
: ImGui::GetBackgroundDrawList();
draw_list->AddImage(
reinterpret_cast<ImTextureID>(this->texture),
overlay_content_top_left,
bottom_right);
if (this->draws_window) {
// draw an invisible button so that it swallows mouse input
// this is needed to prevent the window from being dragged around
// (alternatively io.ConfigWindowsMoveFromTitleBarOnly can be set but that is global)
ImGui::InvisibleButton(
(this->title + "__InvisibleButton").c_str(),
overlay_content_size,
ImGuiButtonFlags_None);
}
#if OVERLAYDBG
log_info("sub::overlay", "{} / {} = {}", overlay_content_size.x, overlay_content_size.y, overlay_content_size.x / overlay_content_size.y);
#endif
}
}