diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 2c48305b..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - groups: - github-actions: - patterns: ["*"] - schedule: - interval: "weekly" - cooldown: - default-days: 7 diff --git a/.github/workflows/pr-summary-eval-reusable.yml b/.github/workflows/pr-summary-eval-reusable.yml index aebdba4f..0049162d 100644 --- a/.github/workflows/pr-summary-eval-reusable.yml +++ b/.github/workflows/pr-summary-eval-reusable.yml @@ -15,15 +15,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@v4 with: node-version: '20' - name: Fetch current PR body - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@v7 env: PR_NUMBER: ${{ inputs.pr-number }} with: @@ -44,7 +44,7 @@ jobs: echo "Eval exit code: $?" - name: Post evaluation comment - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@v7 env: PR_NUMBER: ${{ inputs.pr-number }} with: diff --git a/.github/workflows/pr-summary-generate.yml b/.github/workflows/pr-summary-generate.yml index b249f4ff..794939dc 100644 --- a/.github/workflows/pr-summary-generate.yml +++ b/.github/workflows/pr-summary-generate.yml @@ -37,13 +37,13 @@ jobs: fi - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: React to comment if: github.event_name == 'issue_comment' - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@v7 with: script: | await github.rest.reactions.createForIssueComment({ @@ -71,7 +71,7 @@ jobs: - name: Generate summary with GitHub Models id: generate - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@v7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -156,7 +156,7 @@ jobs: - name: Update PR title and description if: steps.generate.outputs.summary != '' - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@v7 env: PR_TITLE: ${{ steps.generate.outputs.title }} PR_NUMBER: ${{ steps.pr-number.outputs.number }} diff --git a/Kits/ATGTK/imgui/imgui_atg.cpp b/Kits/ATGTK/imgui/imgui_atg.cpp index 47fe07c9..1a075d1c 100644 --- a/Kits/ATGTK/imgui/imgui_atg.cpp +++ b/Kits/ATGTK/imgui/imgui_atg.cpp @@ -16,12 +16,122 @@ #include #pragma comment(lib, "shcore.lib") // GetDpiForMonitor #endif +#include #include #include #include +#include namespace ImGuiAtg { + //-------------------------------------------------------------------------------------- + // Input helpers + //-------------------------------------------------------------------------------------- + + namespace + { + std::vector g_navigationGroups; + std::string g_currentNavigationGroup; + std::string g_requestedNavigationGroup; + int g_navigationGroupDepth = 0; + bool g_navigationGroupInputActive = false; + } + + void BeginNavigationGroup(const char* id) + { + IM_ASSERT(id != nullptr && id[0] != '\0'); + IM_ASSERT(g_navigationGroupDepth == 0); + IM_ASSERT(std::find(g_navigationGroups.cbegin(), g_navigationGroups.cend(), id) == g_navigationGroups.cend()); + g_navigationGroupDepth++; + g_navigationGroups.emplace_back(id); + + if (g_requestedNavigationGroup == id) + { + ImGui::SetNavCursorVisible(true); + ImGui::SetKeyboardFocusHere(); + g_currentNavigationGroup = id; + g_requestedNavigationGroup.clear(); + } + } + + void EndNavigationGroup() + { + IM_ASSERT(g_navigationGroupDepth > 0); + g_navigationGroupDepth--; + } + + void HandleStandardInput() + { + IM_ASSERT(g_navigationGroupDepth == 0); + + if ((!ImGui::GetIO().WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F2, false)) || + (ImGui::IsKeyDown(ImGuiKey_GamepadL1) && + ImGui::IsKeyDown(ImGuiKey_GamepadR1) && + ImGui::IsKeyPressed(ImGuiKey_GamepadFaceUp, false))) + { + ToggleTheme(); + } + + if (ImGui::IsKeyDown(ImGuiKey_GamepadL1) && + ImGui::IsKeyDown(ImGuiKey_GamepadR1) && + ImGui::IsKeyDown(ImGuiKey_GamepadBack) && + ImGui::IsKeyDown(ImGuiKey_GamepadStart)) + { + PostQuitMessage(0); + } + + const bool groupNavigationChord = ImGui::IsKeyDown(ImGuiKey_GamepadL1) + && ImGui::IsKeyDown(ImGuiKey_GamepadR1); + const bool previousGroupPressed = groupNavigationChord + && ImGui::IsKeyPressed(ImGuiKey_GamepadDpadUp, false); + const bool nextGroupPressed = groupNavigationChord + && ImGui::IsKeyPressed(ImGuiKey_GamepadDpadDown, false); + const bool groupNavigationInputDown = groupNavigationChord + && (ImGui::IsKeyDown(ImGuiKey_GamepadDpadUp) + || ImGui::IsKeyDown(ImGuiKey_GamepadDpadDown)); + + if (!g_requestedNavigationGroup.empty() + && std::find( + g_navigationGroups.cbegin(), + g_navigationGroups.cend(), + g_requestedNavigationGroup) == g_navigationGroups.cend()) + { + g_requestedNavigationGroup.clear(); + } + + if (!g_navigationGroups.empty() && (previousGroupPressed || nextGroupPressed)) + { + auto currentGroup = std::find( + g_navigationGroups.cbegin(), + g_navigationGroups.cend(), + g_currentNavigationGroup); + + size_t requestedGroupIndex = 0; + if (currentGroup == g_navigationGroups.cend()) + { + requestedGroupIndex = previousGroupPressed ? g_navigationGroups.size() - 1 : 0; + } + else + { + const size_t currentGroupIndex = static_cast(currentGroup - g_navigationGroups.cbegin()); + requestedGroupIndex = previousGroupPressed + ? (currentGroupIndex + g_navigationGroups.size() - 1) % g_navigationGroups.size() + : (currentGroupIndex + 1) % g_navigationGroups.size(); + } + + g_requestedNavigationGroup = g_navigationGroups[requestedGroupIndex]; + g_navigationGroupInputActive = true; + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad; + } + else if (g_navigationGroupInputActive && !groupNavigationInputDown) + { + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + g_navigationGroupInputActive = false; + } + + g_navigationGroups.clear(); + } + //-------------------------------------------------------------------------------------- // Style / Font helpers //-------------------------------------------------------------------------------------- @@ -227,14 +337,14 @@ namespace ImGuiAtg // Fullscreen layout -- a single borderless window filling the framebuffer. //-------------------------------------------------------------------------------------- - void BeginFullscreenLayout(std::optional titleSafe) + void BeginFullscreenLayout() { // Resolve the title-safe choice: explicit caller value wins, otherwise default // to "on" for console builds and "off" for PC. #ifdef _GAMING_XBOX - const bool applyTitleSafe = titleSafe.value_or(true); + const bool applyTitleSafe = true; #else - const bool applyTitleSafe = titleSafe.value_or(false); + const bool applyTitleSafe = false; #endif ImVec2 displaySize = ImGui::GetIO().DisplaySize; @@ -276,6 +386,12 @@ namespace ImGuiAtg } void DrawFooter() + { + BeginFooter(); + EndFooter(); + } + + void BeginFooter() { // Collapse vertical spacing so the footer takes the minimum height possible const ImVec2 spacing = ImGui::GetStyle().ItemSpacing; @@ -283,82 +399,34 @@ namespace ImGuiAtg ImGui::Separator(); - // Exit hint -- click to quit. ControllerText wraps its output in a Group, - // so the IsItem* queries here apply to the entire glyph+text run. - ControllerText("[Alt]+[F4] / [LB]+[RB]+[View]+[Menu] Exit"); - if (ImGui::IsItemHovered()) - { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - PostQuitMessage(0); - } - - ImGui::SameLine(0, Scaled(80.0f)); + // Exit hint -- click to quit. + if (FooterItem("[Alt]+[F4] / [LB]+[RB]+[View]+[Menu] Exit")) + PostQuitMessage(0); // Theme toggle hint -- click to flip light/dark. - ControllerText("[F2] / [LB]+[RB]+[Y] Toggle Light/Dark"); - if (ImGui::IsItemHovered()) - { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - ToggleTheme(); - } + if (FooterItem("[F2] / [LB]+[RB]+[Y] Toggle Light/Dark")) + ToggleTheme(); - ImGui::PopStyleVar(); + // Cursor is left on the footer line. A sample may append its own hints here + // with FooterItem() before calling EndFooter(). } - void HandleStandardInput() + bool FooterItem(const char* text, bool clickable) { - // Theme toggle. Exit via Alt+F4 is handled by Windows (DefWindowProc), so we - // only wire up F2 here. Suppressed while ImGui is capturing keyboard input - // for a text widget so typing in a text box doesn't flip the theme. - if (!ImGui::GetIO().WantTextInput) - { - if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) - ToggleTheme(); - } - - using Microsoft::WRL::ComPtr; - static ComPtr s_gameInput; - static GameInputGamepadState s_prevGamepadState{}; - - if (!s_gameInput) - { - if (FAILED(::GameInputCreate(&s_gameInput))) - return; - } - - ComPtr<::IGameInputReading> reading; - if (FAILED(s_gameInput->GetCurrentReading(::GameInputKindGamepad, nullptr, &reading))) - return; - - ::GameInputGamepadState curr{}; - if (!reading->GetGamepadState(&curr)) - return; - - const auto buttons = curr.buttons; - const auto prevButtons = s_prevGamepadState.buttons; - - // Exit combo: LB + RB + View + Menu - if ((buttons & ::GameInputGamepadLeftShoulder) && - (buttons & ::GameInputGamepadRightShoulder) && - (buttons & ::GameInputGamepadMenu) && - (buttons & ::GameInputGamepadView)) + ControllerText(text); + ImGui::SameLine(0, Scaled(40.0f)); + if (clickable && ImGui::IsItemHovered()) { - PostQuitMessage(0); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + return true; } + return false; + } - // Theme toggle combo: LB + RB + Y (edge-triggered to fire once per press) - const bool themeCombo = (buttons & ::GameInputGamepadLeftShoulder) && - (buttons & ::GameInputGamepadRightShoulder) && - (buttons & ::GameInputGamepadY); - const bool wasThemeCombo = (prevButtons & ::GameInputGamepadLeftShoulder) && - (prevButtons & ::GameInputGamepadRightShoulder) && - (prevButtons & ::GameInputGamepadY); - if (themeCombo && !wasThemeCombo) - ToggleTheme(); - - s_prevGamepadState = curr; + void EndFooter() + { + ImGui::PopStyleVar(); } //-------------------------------------------------------------------------------------- @@ -458,26 +526,6 @@ namespace ImGuiAtg // Controller string -- mixed text and glyph font rendering //-------------------------------------------------------------------------------------- - // Encode a Unicode codepoint (BMP) to a 4-byte UTF-8 buffer. Returns length written. - static int EncodeUtf8(uint32_t cp, char* buf) - { - if (cp < 0x80) - { - buf[0] = static_cast(cp); - return 1; - } - if (cp < 0x800) - { - buf[0] = static_cast(0xC0 | (cp >> 6)); - buf[1] = static_cast(0x80 | (cp & 0x3F)); - return 2; - } - buf[0] = static_cast(0xE0 | (cp >> 12)); - buf[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); - buf[2] = static_cast(0x80 | (cp & 0x3F)); - return 3; - } - // Case-insensitive comparator for glyph tag lookup struct CaseInsensitiveLess { @@ -708,8 +756,8 @@ namespace ImGuiAtg { if (!first) ImGui::SameLine(0.0f, 0.0f); ImGui::SetCursorPosY(rowY); - char utf8[4]; - int len = EncodeUtf8(static_cast(glyph), utf8); + char utf8[5]; + int len = ImTextCharToUtf8(utf8, static_cast(glyph)); ImGui::PushFont(glyphFont, glyphSizeBase); // base size; ImGui scales by FontScaleDpi ImGui::TextUnformatted(utf8, utf8 + len); ImGui::PopFont(); @@ -736,21 +784,27 @@ namespace ImGuiAtg ImFont* glyphFont = s_glyphFont; if (static_cast(glyph) != 0 && glyphFont) { - char utf8[4]; - int len = EncodeUtf8(static_cast(glyph), utf8); + char utf8[5]; + int len = ImTextCharToUtf8(utf8, static_cast(glyph)); ImGui::PushFont(glyphFont, size); ImGui::TextUnformatted(utf8, utf8 + len); ImGui::PopFont(); } else if (fallback) { - // Vertically center fallback text within the glyph-sized row - float textHeight = ImGui::GetFontSize(); - float rowHeight = size * GetCurrentScale(); - float offsetY = (rowHeight - textHeight) * 0.5f; - if (offsetY > 0.0f) - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + offsetY); - ImGui::TextUnformatted(fallback); + // Vertically center fallback text within the glyph-sized row. The item must + // reserve the full row height (like a real glyph) so it aligns consistently + // whether or not a glyph precedes it on the line -- otherwise the first item + // on an all-fallback row sits higher than its SameLine siblings. We draw the + // text manually at the centered offset and reserve the height with a Dummy. + const float textHeight = ImGui::GetFontSize(); + const float rowHeight = ImMax(size * GetCurrentScale(), textHeight); + const float offsetY = (rowHeight - textHeight) * 0.5f; + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const ImVec2 textSize = ImGui::CalcTextSize(fallback); + ImGui::GetWindowDrawList()->AddText(ImVec2(pos.x, pos.y + offsetY), + ImGui::GetColorU32(ImGuiCol_Text), fallback); + ImGui::Dummy(ImVec2(textSize.x, rowHeight)); } } diff --git a/Kits/ATGTK/imgui/imgui_atg.h b/Kits/ATGTK/imgui/imgui_atg.h index acd56875..9137b368 100644 --- a/Kits/ATGTK/imgui/imgui_atg.h +++ b/Kits/ATGTK/imgui/imgui_atg.h @@ -48,24 +48,12 @@ namespace ImGuiAtg // Use for fixed widget sizes: ImGuiAtg::Scaled(180) instead of 180 * scale. inline float Scaled(float value) { return value * GetCurrentScale(); } - // Full-screen layout -- creates a single borderless ImGui window filling the framebuffer. - // - // titleSafe controls the TV title-safe inset (a 5%-per-edge margin that protects - // content from being cropped by TV overscan): - // std::nullopt (default) -- automatic: enabled on Xbox console builds (_GAMING_XBOX), - // disabled on PC builds. - // true -- force the inset on, regardless of platform. - // false -- force the inset off, regardless of platform. - void BeginFullscreenLayout(std::optional titleSafe = std::nullopt); + // Full-screen layout -- creates a single borderless ImGui window filling the framebuffer, title-safe on console + void BeginFullscreenLayout(); void EndFullscreenLayout(); - // Standard sample footer -- a single-line strip with exit and theme toggle hints: - // [Alt]+[F4] / [LB]+[RB]+[View]+[Menu] Exit [F2] / [LB]+[RB]+[Y] Toggle Light/Dark - // - // DrawFooter() renders the visual footer only. The matching keyboard/gamepad - // shortcuts are processed by ImGuiAtg::HandleStandardInput(), which the sample - // Main.cpp calls automatically once per frame. - // + // Standard sample footer -- a single-line strip with exit and theme toggle hints + // // Use GetFooterHeight() to reserve space when laying out scrollable content above // the footer, e.g.: // ImGui::BeginChild("##Content", ImVec2(0, ImGui::GetContentRegionAvail().y - ImGuiAtg::GetFooterHeight())); @@ -73,23 +61,40 @@ namespace ImGuiAtg // ImGui::EndChild(); // ImGuiAtg::DrawFooter(); float GetFooterHeight(); + + // DrawFooter() renders the standard footer with no custom items. Equivalent to + // calling BeginFooter() immediately followed by EndFooter(). void DrawFooter(); + // BeginFooter()/EndFooter() let a sample append its own hints after the standard + // ones. Example: + // ImGuiAtg::BeginFooter(); + // if (ImGuiAtg::FooterItem("[F5] Reset")) + // Reset(); + // ImGuiAtg::EndFooter(); + void BeginFooter(); + void EndFooter(); + + // FooterItem() renders one footer hint between BeginFooter() and EndFooter(). + bool FooterItem(const char* text, bool clickable = true); + // Handles standard sample input -- the same shortcuts that DrawFooter() advertises: // // Alt+F4 / LB+RB+View+Menu -- exit the app (Alt+F4 is handled by Windows) // F2 / LB+RB+Y -- toggle light/dark theme - // - // Call once per frame from the main loop between ImGui::NewFrame() and Render - // (the sample Main.cpp does this for you). Maintains its own previous-frame - // gamepad state for edge detection. Keyboard shortcuts are suppressed while - // ImGui is capturing keyboard input for a text widget. Gamepad handling uses - // the v0 GameInput API (always available via the Windows SDK or the - // Microsoft.GameInput NuGet package) so this works in any sample regardless of - // which GameInput API version the sample itself targets. The kit creates its - // own IGameInput instance on first use. void HandleStandardInput(); + // Ordered gamepad navigation groups. LB+RB+D-pad up/down moves between the groups + // submitted during Draw(), in submission order. When a group is selected, its first + // navigable item receives focus. + // + // Group IDs must be unique within a frame. Groups cannot be nested, and every + // BeginNavigationGroup() must have a matching EndNavigationGroup(). Conditional groups + // participate only on frames where they are submitted. The standard frame loop must call + // HandleStandardInput() once after ImGui::NewFrame() and before submitting groups. + void BeginNavigationGroup(const char* id); + void EndNavigationGroup(); + // Splitter -- splits the current region into two resizable panels. // // Usage (horizontal -- top/bottom): diff --git a/Kits/OpenSource/imgui/backends/imgui_impl_win32.cpp b/Kits/OpenSource/imgui/backends/imgui_impl_win32.cpp index 8376e14d..03e3e3a5 100644 --- a/Kits/OpenSource/imgui/backends/imgui_impl_win32.cpp +++ b/Kits/OpenSource/imgui/backends/imgui_impl_win32.cpp @@ -99,6 +99,8 @@ #include #include using Microsoft::WRL::ComPtr; +#include +#include #include #if GAMEINPUT_API_VERSION == 1 @@ -124,21 +126,25 @@ using namespace GameInput::v3; struct ImGui_ImplWin32_Data { - HWND hWnd; - HWND MouseHwnd; - int MouseTrackedArea; // 0: not tracked, 1: client area, 2: non-client area - int MouseButtonsDown; - INT64 Time; - INT64 TicksPerSecond; - ImGuiMouseCursor LastMouseCursor; - UINT32 KeyboardCodePage; + HWND hWnd = nullptr; + HWND MouseHwnd = nullptr; + int MouseTrackedArea = 0; // 0: not tracked, 1: client area, 2: non-client area + int MouseButtonsDown = 0; + INT64 Time = 0; + INT64 TicksPerSecond = 0; + ImGuiMouseCursor LastMouseCursor = ImGuiMouseCursor_Arrow; + UINT32 KeyboardCodePage = 0; #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD - bool HasGamepad; - ComPtr GameInput; + bool HasGamepad = false; + GameInputCallbackToken DeviceCallbackToken = 0; + + ComPtr GameInput{}; + std::vector> Gamepads{}; + std::mutex GamepadsMutex{}; #endif - ImGui_ImplWin32_Data() { memset((void*)this, 0, sizeof(*this)); } + ImGui_ImplWin32_Data() = default; }; // Backend data stored in io.BackendPlatformUserData to allow support for multiple Dear ImGui contexts @@ -169,6 +175,39 @@ static void ImGui_ImplWin32_UpdateKeyboardCodePage(ImGuiIO& io) #endif } +static void CALLBACK ImGui_ImplWin32_DeviceCallback( + _In_ GameInputCallbackToken, + _In_ void* context, + _In_ IGameInputDevice* device, + _In_ uint64_t, + _In_ GameInputDeviceStatus current, + _In_ GameInputDeviceStatus previous) noexcept +{ + auto* bd = static_cast(context); + + const bool wasConnected = (previous & GameInputDeviceConnected) != 0; + const bool isConnected = (current & GameInputDeviceConnected) != 0; + + std::lock_guard lock(bd->GamepadsMutex); + + // newly connected device, add to our list + if (isConnected && !wasConnected) + { + bd->Gamepads.emplace_back(device); + } + + // newly disconnected device, remove from our list + else if (wasConnected && !isConnected) + { + bd->Gamepads.erase( + std::remove_if(bd->Gamepads.begin(), bd->Gamepads.end(), [device](ComPtr const& entry) + { + return entry.Get() == device; + }), bd->Gamepads.end() + ); + } +} + static bool ImGui_ImplWin32_InitEx(void* hwnd, bool platform_has_own_dc) { ImGuiIO& io = ImGui::GetIO(); @@ -181,17 +220,44 @@ static bool ImGui_ImplWin32_InitEx(void* hwnd, bool platform_has_own_dc) if (!::QueryPerformanceCounter((LARGE_INTEGER*)&perf_counter)) return false; - // Setup backend capabilities flags ImGui_ImplWin32_Data* bd = IM_NEW(ImGui_ImplWin32_Data)(); + bd->hWnd = (HWND)hwnd; + bd->TicksPerSecond = perf_frequency; + bd->Time = perf_counter; + bd->LastMouseCursor = ImGuiMouseCursor_COUNT; + + // Setup GameInput +#ifndef IMGUI_IMPL_GDK_DISABLE_GAMEPAD + HRESULT hr = GameInputCreate(&bd->GameInput); + if (FAILED(hr)) + { + IM_DELETE(bd); + return false; + } + + hr = bd->GameInput->RegisterDeviceCallback( + nullptr, + GameInputKindGamepad, + GameInputDeviceConnected, + GameInputBlockingEnumeration, + bd, + ImGui_ImplWin32_DeviceCallback, + &bd->DeviceCallbackToken); + + if (FAILED(hr)) + { + IM_DELETE(bd); + return false; + } + +#endif // IMGUI_IMPL_GDK_DISABLE_GAMEPAD + + // Setup backend capabilities flags io.BackendPlatformUserData = (void*)bd; io.BackendPlatformName = "imgui_impl_win32"; io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used) - bd->hWnd = (HWND)hwnd; - bd->TicksPerSecond = perf_frequency; - bd->Time = perf_counter; - bd->LastMouseCursor = ImGuiMouseCursor_COUNT; ImGui_ImplWin32_UpdateKeyboardCodePage(io); // Set platform dependent data in viewport @@ -199,12 +265,6 @@ static bool ImGui_ImplWin32_InitEx(void* hwnd, bool platform_has_own_dc) main_viewport->PlatformHandle = main_viewport->PlatformHandleRaw = (void*)bd->hWnd; IM_UNUSED(platform_has_own_dc); // Used in 'docking' branch - // Setup GameInput -#ifndef IMGUI_IMPL_GDK_DISABLE_GAMEPAD - if(GameInputCreate(&bd->GameInput) != S_OK) - return false; -#endif // IMGUI_IMPL_GDK_DISABLE_GAMEPAD - return true; } @@ -226,6 +286,18 @@ void ImGui_ImplWin32_Shutdown() ImGuiIO& io = ImGui::GetIO(); ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); +#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD + if (bd->GameInput != nullptr && bd->DeviceCallbackToken != 0) + { +#if defined(GAMEINPUT_API_VERSION) && (GAMEINPUT_API_VERSION >= 1) + bd->GameInput->UnregisterCallback(bd->DeviceCallbackToken); +#else + bd->GameInput->UnregisterCallback(bd->DeviceCallbackToken, UINT64_MAX); +#endif + bd->DeviceCallbackToken = 0; + } +#endif + io.BackendPlatformName = nullptr; io.BackendPlatformUserData = nullptr; io.BackendFlags &= ~(ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_HasSetMousePos | ImGuiBackendFlags_HasGamepad); @@ -344,8 +416,14 @@ static void ImGui_ImplWin32_UpdateGamepads(ImGuiIO& io) #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io); - if(bd->GameInput == nullptr) + std::lock_guard lock(bd->GamepadsMutex); + + if(bd->GameInput == nullptr || bd->Gamepads.empty() || bd->Gamepads.back() == nullptr) + { + io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; + bd->HasGamepad = false; return; + } #if GAMEINPUT_API_VERSION == 1 ComPtr reading; @@ -357,7 +435,7 @@ static void ImGui_ImplWin32_UpdateGamepads(ImGuiIO& io) ComPtr reading; #endif - HRESULT hr = bd->GameInput->GetCurrentReading(GameInputKindGamepad, nullptr, &reading); + HRESULT hr = bd->GameInput->GetCurrentReading(GameInputKindGamepad, bd->Gamepads.back().Get(), &reading); bd->HasGamepad = hr == S_OK; io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; diff --git a/Samples/Graphics/FramePacing/Assets/LOGO_ATG.png b/Samples/Graphics/FramePacing/Assets/LOGO_ATG.png new file mode 100644 index 00000000..bd455669 Binary files /dev/null and b/Samples/Graphics/FramePacing/Assets/LOGO_ATG.png differ diff --git a/Samples/Graphics/FramePacing/Assets/LargeLogo.png b/Samples/Graphics/FramePacing/Assets/LargeLogo.png new file mode 100644 index 00000000..ed4a935e Binary files /dev/null and b/Samples/Graphics/FramePacing/Assets/LargeLogo.png differ diff --git a/Samples/Graphics/FramePacing/Assets/Logo.png b/Samples/Graphics/FramePacing/Assets/Logo.png new file mode 100644 index 00000000..2ca6c2e9 Binary files /dev/null and b/Samples/Graphics/FramePacing/Assets/Logo.png differ diff --git a/Samples/Graphics/FramePacing/Assets/SmallLogo.png b/Samples/Graphics/FramePacing/Assets/SmallLogo.png new file mode 100644 index 00000000..40dba78d Binary files /dev/null and b/Samples/Graphics/FramePacing/Assets/SmallLogo.png differ diff --git a/Samples/Graphics/FramePacing/Assets/SplashScreen.png b/Samples/Graphics/FramePacing/Assets/SplashScreen.png new file mode 100644 index 00000000..4d4d8d33 Binary files /dev/null and b/Samples/Graphics/FramePacing/Assets/SplashScreen.png differ diff --git a/Samples/Graphics/FramePacing/Assets/StoreLogo.png b/Samples/Graphics/FramePacing/Assets/StoreLogo.png new file mode 100644 index 00000000..62cd3f2b Binary files /dev/null and b/Samples/Graphics/FramePacing/Assets/StoreLogo.png differ diff --git a/Samples/Graphics/FramePacing/Assets/UILayout.json b/Samples/Graphics/FramePacing/Assets/UILayout.json new file mode 100644 index 00000000..6f69afd3 --- /dev/null +++ b/Samples/Graphics/FramePacing/Assets/UILayout.json @@ -0,0 +1,509 @@ +{ + "styles": [ + { + "classId": "SpriteStyle", + "id": "background_panel_gray", + "colorUsage": "override", + "color": [ 25, 25, 25, 0.75 ] + }, + { + "classId": "SpriteStyle", + "id": "info_panel_gray", + "colorUsage": "override", + "color": [ 25, 25, 25, 0.80 ] + }, + { + "classId": "SpriteStyle", + "id": "legend_panel_gray", + "colorUsage": "override", + "color": [ 25, 25, 25, 0.85 ] + }, + { + "classId": "SpriteStyle", + "id": "graph_panel_gray", + "colorUsage": "override", + "color": [ 25, 25, 25, 0.95 ] + }, + { + "classId": "TextStyle", + "id": "label_text", + "colorUsage": "override", + "color": [ 140, 198, 199, 1.0 ], + "verticalAlign": "middle" + }, + { + "classId": "TextStyle", + "id": "highlight_label_text", + "colorUsage": "override", + "color": [ 219, 73, 28, 1.0 ], + "verticalAlign": "middle" + }, + { + "classId": "TextStyle", + "id": "legend_text", + "colorUsage": "override", + "color": [ 140, 198, 199, 1.0 ], + "verticalAlign": "middle", + "horizontalAlign": "center", + "legendFont": "Assets/Fonts/XboxOneControllerLegendSmall" + } + ], + "layout": { + "classId": "Panel", + "id": "root", + "positioningAnchor": [ "left", "top" ], + "position": [ 0, 0 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 1920, 1080 ], + "style": { + "classId": "SpriteStyle", + "colorUsage": "override", + "color": [ 0, 0, 0, 0.0 ] + }, + "childElements": [ + { + "classId": "Panel", + "id": "stats_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 785, 0 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 946, 460 ], + "styleId": "background_panel_gray", + "childElements": [ + { + "classId": "Panel", + "id": "frame_time_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 14, 14 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 570, 134 ], + "styleId": "graph_panel_gray" + }, + { + "classId": "Panel", + "id": "fps_label_background", + "positioningAnchor": [ "right", "top" ], + "position": [ -199, 14 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 149, 134 ], + "styleId": "info_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "fps_label", + "positioningAnchor": [ "right", "top" ], + "position": [ -10, 5 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 100, 124 ], + "text": "average\nfps", + "style": { + "classId": "TextStyle", + "inheritsFromId": "label_text", + "horizontalAlign": "right" + } + } + ] + }, + { + "classId": "Panel", + "id": "fps_num_background", + "positioningAnchor": [ "right", "top" ], + "position": [ 0, 14 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 199, 134 ], + "styleId": "graph_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "fps_num", + "positioningAnchor": [ "left", "top" ], + "position": [ 10, 5 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 90, 124 ], + "text": "", + "style": { + "classId": "TextStyle", + "inheritsFromId": "highlight_label_text", + "size": 48 + } + } + ] + }, + { + "classId": "Panel", + "id": "latency_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 14, 162 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 570, 134 ], + "styleId": "graph_panel_gray" + }, + { + "classId": "Panel", + "id": "latency_label_background", + "positioningAnchor": [ "right", "top" ], + "position": [ -199, 162 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 149, 134 ], + "styleId": "info_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "latency_label", + "positioningAnchor": [ "right", "top" ], + "position": [ -10, 5 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 100, 124 ], + "text": "ideal\nlatency (ms)", + "style": { + "classId": "TextStyle", + "inheritsFromId": "label_text", + "horizontalAlign": "right" + } + } + ] + }, + { + "classId": "Panel", + "id": "latency_num_background", + "positioningAnchor": [ "right", "top" ], + "position": [ 0, 162 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 199, 134 ], + "styleId": "graph_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "latency_num", + "positioningAnchor": [ "left", "top" ], + "position": [ 10, 5 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 90, 124 ], + "text": "", + "style": { + "classId": "TextStyle", + "inheritsFromId": "highlight_label_text", + "size": 48 + } + } + ] + }, + { + "classId": "Panel", + "id": "margin_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 14, 308 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 570, 134 ], + "styleId": "graph_panel_gray" + }, + { + "classId": "Panel", + "id": "margin_label_background", + "positioningAnchor": [ "right", "top" ], + "position": [ -199, 308 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 149, 134 ], + "styleId": "info_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "margin_label", + "positioningAnchor": [ "right", "top" ], + "position": [ -10, 5 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 100, 124 ], + "text": "latency\nmargin (ms)", + "style": { + "classId": "TextStyle", + "inheritsFromId": "label_text", + "horizontalAlign": "right" + } + } + ] + }, + { + "classId": "Panel", + "id": "margin_num_background", + "positioningAnchor": [ "right", "top" ], + "position": [ 0, 308 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 199, 134 ], + "styleId": "graph_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "margin_num", + "positioningAnchor": [ "left", "top" ], + "position": [ 10, 5 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 90, 124 ], + "text": "", + "style": { + "classId": "TextStyle", + "inheritsFromId": "highlight_label_text", + "size": 48 + } + } + ] + } + ] + }, + { + "classId": "Panel", + "id": "timelines_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 15, 480 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 1700, 515 ], + "styleId": "background_panel_gray", + "childElements": [ + { + "classId": "Panel", + "id": "cpu_update_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 15, 15 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 1670, 85 ], + "styleId": "graph_panel_gray" + }, + { + "classId": "Panel", + "id": "cpu_render_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 15, 115 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 1670, 85 ], + "styleId": "graph_panel_gray" + }, + { + "classId": "Panel", + "id": "gpu_graphics_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 15, 215 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 1670, 85 ], + "styleId": "graph_panel_gray" + }, + { + "classId": "Panel", + "id": "gpu_compute_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 15, 315 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 1670, 85 ], + "styleId": "graph_panel_gray" + }, + { + "classId": "Panel", + "id": "flip_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 15, 415 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 1670, 85 ], + "styleId": "graph_panel_gray" + } + ] + }, + { + "classId": "Panel", + "id": "control_panel", + "positioningAnchor": [ "left", "top" ], + "position": [ 0, 0 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 770, 460 ], + "styleId": "background_panel_gray", + "childElements": [ + { + "classId": "Panel", + "id": "x_legend", + "positioningAnchor": [ "left", "top" ], + "position": [ 15, 100 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 175, 145 ], + "styleId": "legend_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "x_legend_text", + "positioningAnchor": [ "left", "top" ], + "position": [ 0, 0 ], + "text": "Frame load\nauto/manual\n[X]", + "isLegend": true, + "styleId": "legend_text" + }, + { + "classId": "Panel", + "id": "x_legend_border", + "positioningAnchor": [ "right", "top" ], + "position": [ 0, 0 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 3, 145 ], + "style": { + "classId": "SpriteStyle", + "colorUsage": "override", + "color": [ 122, 104, 238, 1.0 ] + } + } + ] + }, + { + "classId": "Panel", + "id": "a_legend", + "positioningAnchor": [ "left", "top" ], + "position": [ 15, 250 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 175, 145 ], + "styleId": "legend_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "a_legend_text", + "positioningAnchor": [ "left", "top" ], + "position": [ 0, 0 ], + "text": "Frame pacing\nauto/manual\n[A]", + "isLegend": true, + "styleId": "legend_text" + }, + { + "classId": "Panel", + "id": "a_legend_border", + "positioningAnchor": [ "right", "top" ], + "position": [ 0, 0 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 3, 145 ], + "style": { + "classId": "SpriteStyle", + "colorUsage": "override", + "color": [ 34, 139, 34, 1.0 ] + } + } + ] + }, + { + "classId": "Panel", + "id": "menu_panel", + "positioningAnchor": [ "right", "bottom" ], + "position": [ -15, -15 ], + "sizingAnchor": [ "right", "bottom" ], + "size": [ 550, 380 ], + "styleId": "legend_panel_gray" + }, + { + "classId": "StaticText", + "id": "navigate_legend_text", + "positioningAnchor": [ "right", "top" ], + "position": [ -15, 10 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 550, 50 ], + "text": "[LThumb] Navigate", + "isLegend": true, + "styleId": "legend_text" + } + ] + }, + { + "classId": "Panel", + "id": "rs_legend", + "positioningAnchor": [ "left", "top" ], + "position": [ 593, 1007 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 224, 52 ], + "styleId": "legend_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "rs_legend_text", + "positioningAnchor": [ "left", "top" ], + "position": [ 0, 0 ], + "text": "[RThumb] Pan left/right", + "isLegend": true, + "styleId": "legend_text" + } + ] + }, + { + "classId": "Panel", + "id": "ltrt_legend", + "positioningAnchor": [ "left", "top" ], + "position": [ 905, 1007 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 224, 52 ], + "styleId": "legend_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "ltrt_legend_text", + "positioningAnchor": [ "left", "top" ], + "position": [ 0, 0 ], + "text": "[LT]/[RT] Zoom out/in", + "isLegend": true, + "styleId": "legend_text" + } + ] + }, + { + "classId": "Panel", + "id": "b_legend", + "positioningAnchor": [ "left", "top" ], + "position": [ 1217, 1007 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 224, 52 ], + "styleId": "legend_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "b_legend_text", + "positioningAnchor": [ "left", "top" ], + "position": [ 0, 0 ], + "text": "[B] Pause/Resume", + "isLegend": true, + "styleId": "legend_text" + } + ] + }, + { + "classId": "Panel", + "id": "exit_legend", + "positioningAnchor": [ "left", "top" ], + "position": [ 1529, 1007 ], + "sizingAnchor": [ "left", "top" ], + "size": [ 112, 52 ], + "styleId": "legend_panel_gray", + "childElements": [ + { + "classId": "StaticText", + "id": "exit_legend_text", + "positioningAnchor": [ "left", "top" ], + "position": [ 0, 0 ], + "text": "[View] Exit", + "isLegend": true, + "styleId": "legend_text" + } + ] + }, + { + "classId": "Panel", + "id": "tear_indicator_background", + "positioningAnchor": [ "right", "top" ], + "position": [ 0, 0 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 189, 1080 ], + "styleId": "background_panel_gray" + }, + { + "classId": "Panel", + "id": "tear_indicator_border", + "positioningAnchor": [ "right", "top" ], + "position": [ -189, 0 ], + "sizingAnchor": [ "right", "top" ], + "size": [ 3, 1080 ], + "style": { + "classId": "SpriteStyle", + "colorUsage": "override", + "color": [ 140, 198, 199, 1.0 ] + } + } + ] + } +} \ No newline at end of file diff --git a/Samples/Graphics/FramePacing/EnforceGpuTime.hlsl b/Samples/Graphics/FramePacing/EnforceGpuTime.hlsl new file mode 100644 index 00000000..8f907785 --- /dev/null +++ b/Samples/Graphics/FramePacing/EnforceGpuTime.hlsl @@ -0,0 +1,41 @@ +//-------------------------------------------------------------------------------------- +// File: EnforceGpuTime.hlsl +// +// This compute shader creates a repeatable GPU workload by spinning until a specified +// time has passed. FramePacing uses it to demonstrate changing graphics and compute load. +// +// This should be compiled with /Od, or subsequent calls to __XB_MemTime() (in +// GetTimeStamp()) may be optimized away. +// +// Copyright (c) Microsoft Corporation. All rights reserved. +//-------------------------------------------------------------------------------------- + +#include "Time.hlsli" + +#define RS \ +[\ + RootSignature\ + (\ + "RootConstants(num32BitConstants=2, b0),\ + UAV(u0)"\ + )\ +] + +cbuffer cb0 +{ + uint64_t g_desiredFrameTime; +} + +RWStructuredBuffer