Compare commits

...

80 Commits

Author SHA1 Message Date
bicarus cf86fbd238 graphics: remove static import of d3d9 (#780)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #779
Regressed by #720 

## Description of change
#720 introduced a static dependency on DX9 which caused `d3d9.dll` to be
loaded at boot from `system32`. Some third party hooks (like
ifs_layeredfs and dxvk) rely on supplying a custom `d3d9.dll` in the DLL
search path (usually in modules) but this change caused Windows to skip
that check.

Remove the hard dependency on d3d9 and add a CMake check to ensure that
compiled binaries do not accidentally introduce new static imports in
the future.

## Testing
Confirmed that dxvk runs again.
2026-06-27 15:23:59 -07:00
bicarus 3fdb369128 sdvx: option to disable Live2D (#777)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds an option to disable Live2D. Can be disabled everywhere, or just
during a song.

## Testing
Tested EG final and recent Nabla.
2026-06-27 02:34:19 -07:00
bicarus db7defff5a overlay: fix OS cursor showing up in some games (#776)
## Link to GitHub Issue or related Pull Request, if one exists
regressed by #766, fixes #775 

## Description of change
Some games like DDR leave `ShowCursor` ref count at non-negative number,
so when spice handles `WM_SETCURSOR` to change the cursor shape, it ends
up showing the OS cursor.

## Testing
2026-06-25 15:30:59 -07:00
bicarus 451cbec0b9 overlay: OBS control window via WebSocket API (#773)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds an overlay window widget for controlling OBS over WebSocket.

Add new lines to the FPS widget that shows recording / streaming timers.

Show notifications for major state changes (stream started/stopped,
recording started/paused/resumed/stopped)

## Testing
2026-06-24 02:44:36 -07:00
bicarus 5c69e295ab ccj: fix mouse trackball cursor wrap (#774)
I don't have a repro but a user reported that the cursor doesn't wrap
around on one side. Correctly account for windows bleeding over to other
monitors.
2026-06-21 17:03:44 -07:00
bicarus 5065a92d55 launcher: don't warn about hook option conflicts (#771)
DLL hook options (-k and -z) allow multiple flags being specified in
both spicecfg and via command line; they never conflict as they are
additive. Remove the warning about option conflicts.
2026-06-20 15:46:45 -07:00
bicarus 5483e8e2c0 ponp: make Force Sub Redraw an option (#770)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Similar to how it is on SDVX, turning this on can cause graphical
glitches depending on the GPU, so it needs to be an option that users
can toggle.

## Testing
2026-06-19 22:30:08 -07:00
bicarus e8949a2612 overlay: fix highlights, alignment in presets table (#769)
* Fix table row highlight hover detection in Options tab when row has
two lines of text
* Fix text alignment in controller presets table
2026-06-19 00:40:54 -07:00
bicarus db71a0b24d cfg: center window on launch (#768) 2026-06-18 23:02:23 -07:00
bicarus d3d5422768 cfg: enforce minimum window size (#767) 2026-06-18 21:10:42 -07:00
bicarus-dev 5185f753e5 fix truncated text 2026-06-18 17:32:59 -07:00
bicarus eb897d0f1b overlay: fix cursor visibility (#766)
After #739 there was a regression where the cursor visibility became
inconsistent if the FPS display was shown and then another overlay
window was toggled. Fix the imgui cursor toggle logic to only check for
the overlay layer visibility.
2026-06-17 20:12:56 -07:00
bicarus 50fc80a1c3 graphics: respect Windows dark mode (#765)
Only affects the caption bar.

Should work for:

- [x] standalone configurator
- [x] dx9 games, including multi-window games like gitadora
- [x] Unity games
2026-06-17 11:51:31 -07:00
bicarus 301e15c44d overlay: make tabs bigger (#764) 2026-06-17 01:14:50 -07:00
bicarus-dev b6f886a7c5 fix option text for -iidxtdjsubsize 2026-06-16 01:47:47 -07:00
bicarus 0b085286d3 cfg: ignore nolegacy (#763)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #762

## Description of change
Fix -nolegacy causing input to be not processed in spicecfg

Need to fix `run_cfg` and `CONFIGURATOR_STANDALONE` for real soon.

## Testing
2026-06-15 14:48:18 -07:00
bicarus 29d3d9bb52 overlay: add sorting to patches table (#761) 2026-06-15 14:47:15 -07:00
bicarus-dev bf05a3ef30 update crash message 2026-06-15 12:55:15 -07:00
bicarus 6280ac3b0a imgui: update imgui to head of docking branch (v1.92.8) (#756)
## Tested:

- [x] LDJ with sub
- [x] sdvx
- [x] dx11 game
2026-06-15 10:40:38 -07:00
bicarus-dev cc9df27569 change ea url template 2026-06-15 10:33:34 -07:00
bicarus ac51acdafe cfg: fix config save atomicity (#760)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fully flush `tmp` file and then use `MoveFileExW` instead of `CopyFileW`
which guarantees atomic operation on NTFS.

## Testing
2026-06-15 02:06:04 -07:00
bicarus 6fec169347 overlay: toggle main menu independently (#759)
Recently there was a change to make the main menu also toggle the
overlay; revert that.
2026-06-15 01:45:02 -07:00
bicarus 4af006e869 wasapi: address buffer overflow when using -wasapishared (#758)
## Link to GitHub Issue or related Pull Request, if one exists
Building on #745 

## Description of change
With `-wasapishared`, some users see `AUDCLNT_E_BUFFER_TOO_LARGE`
(`0x88890006`).

The existing one-device-period buffer clamp wasn't enough on endpoints
with a small shared buffer; the shared buffer is double-buffered, so a
full-buffer write often exceeds the free space.

### Fix

Added a FIFO bridge to `SharedRedirect` that decouples the game's
per-event writes from the shared engine's clock:

- **`GetBuffer`** hands the game a pointer into the FIFO tail to write
in place.
- **`ReleaseBuffer`** commits the write and drains `min(pending,
device_free)` frames to the device - so a write can never exceed what
the device accepts, structurally preventing the overflow on any
endpoint.
- **`GetCurrentPadding`** reports the FIFO fill level (capped to the
reported buffer size) so poll/timer-driven games pace correctly against
the virtual buffer.

## Testing
2026-06-14 19:57:28 -07:00
bicarus-dev 256ef4d341 update -wasapishared option text 2026-06-14 17:53:01 -07:00
bicarus 698f2ffc2c overlay: add vertical row padding (#757)
Slightly decrease information density by adding a couple pixels of
vertical padding to each table row in the configurator.
2026-06-14 12:29:32 -07:00
bicarus 4879a35eb8 overlay: remove keyboard / gamepad tab navigation (#755)
Tired of constantly fighting unintentional keyboard navigation behavior.

Also, remove `ImGuiConfigFlags_IsTouchScreen` which literally does
nothing.
2026-06-12 21:28:34 -07:00
bicarus fd7e850789 ccj: fix mouse wheel input in overlay (#754)
## Link to GitHub Issue or related Pull Request, if one exists
#251 

## Description of change
CCJ registers for rawinput mouse. We allowed the rawinput registration
to go through to the OS, which prevents spice's rawinput stack from
using the mouse. This caused the overlay to not accept mouse input. Fix
it by not letting the rawinput registration through. Luckily, mouse
input continues to work properly in CCJ (for touch).

Also, fix a bug in DX11 overlay when toggling the main menu.

## Testing
2026-06-12 03:42:59 -07:00
bicarus 0cf07c38da overlay: left nav for cards tab (#753)
Enable left nav for Card tab, splitting the single page into multiple
sub tabs.

Card Manager is now embedded in this UI (but can still be launched as a
separate window)
2026-06-12 00:57:52 -07:00
bicarus 1f073b2d75 overlay: combine buttons/analogs/overlay/lights (#752)
Same idea as #747 except for input/output binding.
2026-06-11 23:38:09 -07:00
jessemarthin 3037214932 Fix capture.get_jpg blocking during game load for Companion Mirror (#750)
Add a timeout and skip signalling to the D3D9 capture path so the API
thread no longer waits indefinitely when Present stops during loading.
Return the last successful JPEG frame as a fallback.

## Link to GitHub Issue or related Pull Request, if one exists
#746

## Description of change
* Fix `capture.get_jpg` blocking indefinitely when D3D9 screen capture
cannot
  complete during game loading (e.g. no Present calls).
* Add a 2-second timeout to `graphics_capture_receive_jpeg()` and cancel
  pending capture requests on timeout.
* Signal capture skip from D3D9 failure paths via
`graphics_capture_skip()`.
* Cache the last successful JPEG per screen and return it as a fallback
  when a new capture fails.
Tested with **stock iOS Spice Companion** only. I have not tested
Android or
other Companion clients. The iOS client implementation differs from the
others; this fix is server-side only.
No config file, CLI option, or API schema changes.

## Testing
* Built locally with llvm-mingw cross-compiler (WSL).
* Docker build (`src/spice2x/build_docker.sh`): Pending
* Tested with stock iOS Spice Companion on iPad: Mirror remains
connected
  during game loading instead of returning to KeyPad.
2026-06-11 19:14:47 -07:00
drmext 18d6d9783a overlay: fix Options tab contents jitter (#751)
## Link to GitHub Issue or related Pull Request, if one exists
#747

## Description of change
The old approach called ImGui’s `SetScrollHereY()` while the options
list was still being laid out. That caused one-frame scroll corrections
and edge-snap jitter near scroll boundaries.

The new approach in `build_options_tab()`:

1. **On click** - set a flag (`options_scroll_pending` for categories,
`options_scroll_top` for group headers).
2. **During layout** - when building the target category, record its
content position with `GetCursorPosY()` (don’t scroll yet).
3. **After all options are built** - apply scroll once by setting
`window->Scroll.y` directly, and only if the position actually needs to
change (0.5px threshold).

That deferred, single-pass scroll avoids the shake while still
supporting scroll-up, scroll-down, and re-click-to-recenter.


## Testing
In spicecfg.exe and in-game overlay, Options tab:

Open groups with many categories

Scroll content down manually, then click a higher category in the left
nav - content scrolls up to that section

Re-click the same category after manual scroll - jumps back without
shake

Rapid re-clicks when already aligned - no jitter

Switch group headers - still scrolls to top; re-click at top - no shake
2026-06-11 14:03:33 -07:00
bicarus 267add3227 iidx: set SOUND_OUTPUT_DEVICE even when -iidx is not enabled (#749)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
For cab usage, when `-exec bm2dx.dll` is used without `-iidx` then
SOUND_OUTPUT_DEVICE never gets set.

Change this so that `SOUND_OUTPUT_DEVICE` gets set unconditionally if
`-iidxsounddevice` is set.

## Testing
2026-06-11 00:40:53 -07:00
bicarus a170627f52 overlay: redesign Options tab(s) (#747)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Combine API/Options/Advanced/Development/Search tabs into one Options
tab and add a left nav for navigation categories.

Reduce information density for options table by making each row double
the height and slightly increasing widget sizes.

## Testing
2026-06-11 00:17:54 -07:00
drmext 5c244eaca7 improve E004 card generation and placeholder pcbid (#748)
## Link to GitHub Issue or related Pull Request, if one exists
#73

## Description of change
Generated cards now start with E0040100 to fully match real cards. The
placeholder example `E004010000000000` is replaced with
`E0040100FFFFFFFF`.

The invalid placeholder/fallback PCBID `04040000000000000000` is
replaced with `01201000000000010101`, which contains a valid
header/checksum, making it closer resemble a real PCBID while still
clearly being an example.
2026-06-10 23:24:43 -07:00
bicarus 6bcadccf09 audio: WASAPI Force Shared Mode option (#745)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
This new option, `-wasapishared`, detects when the game opens WASAPI
Exclusive stream and forcibly opens a shared mode stream instead on the
real device.

#### Benefits of this:

1. No need for `force wasapi shared` patches
2. No need to change sample rate of audio devices before starting up
games
3. Adds ability to boot in shared mode even for games that do not have
shared wasapi changes (old GITADORA, popn HC)

As a result, this option is strictly better than `shared wasapi` patches
available for many games.

#### Downsides:
1. OS resampling will add a small latency, but not big enough to be
noticeable.
2. Using shared mode instead of exclusive mode adds latency, of course.

(If you cared about latency, you would use the low latency option, or
use exclusive or asio)

Basically it's the "make things work" button for audio that should work
for almost all games that we support.

The option has no effect if the game opens in shared mode.

## Testing
SDVX7 - ok
GITADORA GW - ok
popn HC - ok
IIDX - ok
2026-06-10 00:20:59 -07:00
bicarus-dev ccb3bac48c defer error for -exec 2026-06-09 00:09:00 -07:00
bicarus 0a7ecf82a9 build: produce shared objects for spice.exe / spice_laa.exe, disable 64-bit winxp builds (#744)
to speed up clean builds. GitHub CI time went from 20 minutes -> 10
minutes.
2026-06-08 17:42:41 -07:00
bicarus-dev 1c6929dd84 Move full screen Monitor options to Graphics (Full Screen) 2026-06-08 15:53:54 -07:00
bicarus 1d3d9040f2 Update bug_report.md 2026-06-08 00:51:48 -07:00
bicarus 8f1069628e Update bug_report.md 2026-06-08 00:50:33 -07:00
bicarus-dev f8c585a9d1 update main menu layout 2026-06-08 00:27:42 -07:00
bicarus 9651051990 overlay: make overlay toggle behavior more consistent (#743)
Clean up how various overlay toggle shortcuts work with the main menu
visible.
2026-06-07 21:56:06 -07:00
bicarus a54a62d0d7 overlay: fix dummy marker spacing (#741)
Fix alignment in patches tab
2026-06-07 19:14:43 -07:00
bicarus 100caa0f5c overlay: refactor overlay layering (#739)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Create three distinct layers for the overlay:

1. Bottommost persistent layer - non-interactable layer that is always
on. This was only for notifications, but now the FPS widget lives here
as well.
2. Overlay windows layer - most interactable windows go here.
3. Topmost main menu - this is reserved for the main menu (escape key)
and this is a modal dialog that occludes the layers below.

Why? 

- `toggle overlay` behavior with FPS widget *also* toggling on/off was a
bit confusing (now they're two separate keys)
- FPS widget is popular, but it caused the entire overlay to be active,
which affects how input is handled
- the main menu being a standalone window was a little awkward (now it's
a modal)

## Testing
2026-06-07 17:23:08 -07:00
bicarus-dev bb87aa2944 add placeholder text for ea url option 2026-06-07 01:48:52 -07:00
bicarus 2e2968d1bc overlay: remove dependency on d3dcompiler (#738)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #737, regressed by #707

## Description of change
ImGui backend for DX11 pulled in dependency on a specific version of
`d3dcompiler_47.dll` which is not present on stock Win7 OS image.

Change this to a runtime load of the DLL and disable the overlay if we
can't find one.

## Testing
- [x] Win11, DX11 overlay
- [x] Win11, DX9 overlay
- [x] Win7, DX9 overlay
2026-06-06 14:06:37 -07:00
bicarus 0457262472 audio: asio "downmix" 7.1 to stereo (#736)
## Link to GitHub Issue or related Pull Request, if one exists
#730 

## Description of change
Add an option that extracts channels from 7.1 ASIO and presents to 2-ch
ASIO. Same idea as the SDVX 2ch fix except for gitadora we are
duplicating the center channel to front.

## Testing
Tested SDVX and GFDM Arena.
2026-06-06 03:11:40 -07:00
bicarus-dev f23c359114 minify patches json again 2026-06-05 18:05:48 -07:00
bicarus-dev 81e74dcb97 remove museca difficulty patches 2026-06-05 17:45:36 -07:00
bicarus-dev 8ead941c6c format patches json 2026-06-05 17:22:38 -07:00
bicarus ece03cabba cfg: asio driver selector must list both 32-bit and 64-bit drivers (#734)
## Link to GitHub Issue or related Pull Request, if one exists
#730 

## Description of change
spicecfg is a 32-bit application. When it uses the ASIO SDK it only saw
the 32-bit drivers.

There are some ASIO drivers that have different names under 32-bit and
64-bit (Xonar AE is one of them)... so we have to manually scan the
registry and surface both the WOW32 and WOW64 nodes.

## Testing
<img width="329" height="309" alt="image"
src="https://github.com/user-attachments/assets/7abecf76-6835-4df5-8c2e-e7b425130f4c"
/>

Checked both 32-bit and 64-bit configurator, identical results.
2026-06-05 02:05:03 -07:00
bicarus 4b7f68f920 gitadora: (arena model) flip realtek option (#733)
## Link to GitHub Issue or related Pull Request, if one exists
#730 

## Description of change
Flip the option - enable the Realtek hack by default, unless the user
chooses not to.

## Testing
Tested ASIO path and WASAPI path. WASAPI path should be unaffected as
the game just picks the default audio device.
2026-06-05 01:38:46 -07:00
bicarus 2dae86a6f2 gitadora: (arena model) simulate Realtek device, fix asio redirect hooks (#732)
## Link to GitHub Issue or related Pull Request, if one exists
#730, #718

## Description of change

Part 1)

When ASIO is in use, the game also looks for a Realtek device so that it
can open a WASAPI Exclusive mode stream for headphones.

Add an option to fake that, in case the user doesn't have a Realtek
device.

Part 2)

`-gdaasio` wasn't workign properly - fix the logic.

## Testing
Seems to work with FlexASIO, and Xonar with no real Realtek device.
2026-06-05 00:08:52 -07:00
bicarus 85058c2156 audio: create a wrapper for asio drivers (#731)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Create `WrappedAsio`, similar to how we wrap `IAudioEndpoint`

So far, the wrapper does these things:

1. logging (for diagnosis, since iidx and gfdm don't produce any logs
when asio succeeds)
2. iidx32+ hack to work around refcount mismatch issue
3. sdvx valk cab hack to force 2-channel audio ("downmixing" by taking
only the front channels)
4. honor volume boost

As a result of `#2` the mempatch was removed from `iidx.cpp` since we
can tackle it cleanly in the hook. `#3` also removes the need for manual
patches.

Real downmixing is hard & expensive on the CPU so it was not
implemented.

## Testing
Tested iidx/sdvx/gfdm with xonar and flexasio
2026-06-04 09:24:44 -07:00
bicarus ea2f4c5572 gitadora: (arena model) complain loudly about bad prop files (#729)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Refuse to load if `libaio.dll` exists but `<spec>` is set incorrectly
(almost certainly because the user copied old prop files from downlevel
version)

## Testing
2026-06-02 17:47:51 -07:00
bicarus 48033816a8 audio: WASAPI exclusive resampling, buffer size increase options (#727)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Resampler:
Implement resampler for exclusive mode streams, as we are seeing more
and more devices - not just laptops but onboard audio devices - that
only support 48khz and not 44.1khz. Should work with volume boost (gain
calculated inside resample) and also downmixer (hands off intermediate
scratch buffers).

Buffer size increase:
By default many of these games request a tiny buffer when in shared mode
(TDJ uses 3ms). On some audio setup this results in crackling due to
underflow. Add an option to forcibly increase the buffer size.

## Testing
With resampler set to 48kHz and buffer set to 20ms I can reliably boot
and play IIDX on my display port monitor's speakers; previously this
wasn't possible. IIDX is event-driven.

Tested SDVX7 as well at 48kHz, which opens timer-driven streams.
2026-06-02 01:55:50 -07:00
bicarus-dev b517ef3182 update options 2026-06-01 02:29:12 -07:00
bicarus 6fef16353e otoca: patch up game for printing holo cards (#726)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #154

## Description of change
Patch the game to skip over special holo printing logic and redirect to
the normal printer, resolving the soft lock + printer failure.

## Testing
Tested `NCG-2019012900`. Other versions might not work though since this
is patch-based not hook-based.
2026-05-31 23:08:17 -07:00
bicarus 84ea3f9e8e audio: stereo downmix, volume boost options (#722)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #717, fixes #647

## Description of change
Adds an option to downmix surround sound (5.1, 7.1, etc) down to stereo
(2 speakers). This can be used in most WASAPI games, including Gitadora
and FTT.

How it works: when the game tries to open surround format (say, 7.1) we
create a fake buffer and tell the game that it is supported. In reality
we open a 2-channel stream with the real sound card. When the stream
begins, we downmix the channels down to two (using one of many
algorithms) and output to the sound card.

While we're here, implement an option to boost the game volume by some
decibel value, which is a feature often requested by SDVX players. This
was needed since downmixing can sometimes result in quieter audio.

## Testing
Tested GW Delta and FTT.

Downmix doesn't work on IIDX (32 bits) but volume boost works.
2026-05-31 20:49:22 -07:00
Azalea 6bb6b0a301 [O] Filter GetIpAddrTable to selected network adapter (#724)
## Description of change
Filter GetIpAddrTable to selected network adapter. 

Fixes the IP address error (5-1506-0000) on gitadora gw delta when
multiple network adapters are present.

## Testing
Tested on Gitadora GW Delta with both WiFi and Tailscale adapters
present.
2026-05-31 16:17:47 -07:00
drmext 465d6c6c18 cfg: fix alt+f4 exit (#723)
## Link to GitHub Issue or related Pull Request, if one exists
#720

## Description of change
Pass `VK_F4` sys-key messages through to `DefWindowProc` before ImGui
ingestion, restoring the normal Alt+F4 -> `WM_CLOSE` ->
`launcher::shutdown()` path.

## Testing
- [x] Launch `spicecfg` (D3D9 default path) and press Alt+F4; window
closes and process exits
- [x] Launch `spicecfg` (software path) and press Alt+F4; window closes
and process exits
- [x] Close via title-bar X; still works
2026-05-30 23:59:54 -07:00
Selundine ef384e85dd Add GITADORA Arena windowed monitor layout support, allows the SMALL touch window to be resized (#716)
## Link to GitHub Issue or related Pull Request, if one exists
N/A

## Description of change
Adds GITADORA Arena windowed monitor layout options for the GITADORA,
LEFT, RIGHT, and SMALL windows.

This also applies the selected window border style to all four GITADORA
Arena windows, and allows the SMALL touch window to be resized in
windowed mode while keeping touch coordinates updated.

## Testing
- Manually tested GITADORA Arena windowed mode:
  - default, resizable, and borderless window styles
  - per-window monitor placement
  - SMALL resize
  - SMALL touch coordinate mapping after resize
2026-05-30 23:09:06 -07:00
bicarus 7ee04879e2 overlay: option to force software renderer in configurator (#721)
## Link to GitHub Issue or related Pull Request, if one exists
chicken bit for #720
2026-05-30 15:24:07 -07:00
drmext 4c73200f58 cfg: d3d9 standalone configurator (#720)
## Description of change
**Configurator (spicecfg.exe)**

- Attempts to create a D3D9 device on startup and uses the hardware
ImGui DX9 path when successful; falls back to the existing software
rasterizer automatically if D3D9 init fails.
- Rewrites the configurator window loop: refresh-rate-aware render
timer, minimize/resize/`WM_DISPLAYCHANGE` handling, direct Win32 ->
ImGui input (keyboard, mouse, wheel), and optimized software painting
via `SetDIBitsToDevice` instead of per-frame GDI `HBITMAP` allocation.

**Overlay / input (scoped to standalone configurator)**

- Skips expensive per-frame rawinput polling in
`ImGui_ImplSpice_NewFrame` when `CONFIGURATOR_STANDALONE` is set (Win32
messages drive input instead).
- Adds software-renderer idle-frame detection (`sw_pixels_dirty` +
draw-data hash) so spicecfg only repaints when pixels actually change.
- Adds `ImGuiWindowFlags_NoScrollWithMouse` on the configurator root
window to prevent scroll jolt on non-scrollable child widgets.
2026-05-30 12:52:35 -07:00
bicarus-dev 31aabe9786 fix option name 2026-05-30 01:22:09 -07:00
bicarus 4ea55a61b8 gitadora: (arena model) asio option (#718)
## Link to GitHub Issue or related Pull Request, if one exists
#717 

## Description of change
Add an option to override the ASIO device.

By default the game will try to use any ASIO driver that contains the
string `XONAR` in it.

## Testing
Tested with FlexASIO.
2026-05-30 00:03:06 -07:00
bicarus 0209b80a22 graphics: address perf issues with DX9 image resize logic (#712)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Move resize logic from `EndScene` to `Present`/`PresentEx` (resolves
Live2D frame drops in some extreme settings)

Plug small memory leak

Remove unnecessary surface Lock/Unlock

## Testing
Tested IIDX, SDVX, DDR.
2026-05-29 09:33:31 -07:00
drmext 0511dfb6ca rawinput: fix spicecfg close delay (#715)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Closing spicecfg was blocked for up to ~495 ms. Runtime behavior is
unchanged: same flush interval and same output path during normal
operation. Only shutdown teardown is faster.

## Testing
Opened and closed spicecfg.exe repeatedly; the window closes immediately
with no hang
2026-05-29 09:33:08 -07:00
drmext b558df3340 cfg: optimize saving config (#714)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Save config xml once instead of ~33 times

## Testing
xml is still saved properly
2026-05-29 09:09:02 -07:00
bicarus 89602cfcab misc: clean up eamuse_get_game (#713)
No need to allocate a new `std::string` every call...
2026-05-29 09:08:07 -07:00
drmext b4557993c9 cfg: optimize saving patches to file on disk (#711)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Saving patches to file used to rewrite the file repeatedly, now it loads
and saves only once. Original file timestamp is now preserved on the
backup.

## Testing
Saved a very large patch set that used to take 10+ seconds; now it is
instant. Confirmed hex diffs are still accurate.
2026-05-29 02:20:39 -07:00
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
bicarus b3d8d0aea9 overlay: implement dx11 backend (#707)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #134 

## Description of change
Add ImGui DX11 backend implementation, and integrate into the spice
overlay.

The tricky part is that some of the Unity games:

1. Launch multiple windows, and
2. Can be resized (even full screen games launch at a certain resolution
and then expand to fit desktop resolution)
3. lazy load DX11 DLLs

This PR also adds screenshot functionality, but screen resize is not
implemented.

## Testing

Seems to be working for most games. Need to do final tests for:

- [x] CCJ (attract movie plays as well)
- [x] Busou Shinki (title movie plays)
- [x] MFG
- [x] QuizKnock
- [x] Polaris Chord
- [x] check dx9 for regression
2026-05-29 00:47:28 -07:00
drmext a977cba772 overlay: prevent save button shifting patches text (#709)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
When opening the patches tab by pressing F4 in game, the Save button
doesn't appear until after initially selecting/deselecting a patch, then
it shifts all of the patch text below. To solve this annoyance, this
keeps the Save button in a fixed position (starts greyed out until a
selection) and no longer shifts the text.

## Testing
Confirmed saving selected patches still works properly in the overlay
and standalone cfg
2026-05-28 23:58:09 -07:00
bicarus 489d40d87d utils: dump memory info on crash (#708)
For diagnosing out of memory errors / malloc failures.
2026-05-28 23:57:34 -07:00
bicarus-dev 3a468a9a3b fix up option name 2026-05-27 21:06:05 -07:00
bicarus a2e220d3f4 gitadora: (arena model) unify window layout options (#706)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Combine the two layout options into one

## Testing

- [x] windowed, x4
- [x] windowed, x2
- [x] windowed, x1 with sub
- [x] fs
2026-05-27 21:04:23 -07:00
Azalea febc02b50b [+] Hide sides on gitadora 𝛿 (#705)
## Description of change
Added an option to only disable the left/right screens of gitadora gw
delta while exposing the touch panel screen.

## Testing
Tested by hand on CachyOS + hyprland + gamescope + wine + vnc + phone +
cat 🐈‍⬛

<img width="2560" height="1707" alt="image"
src="https://github.com/user-attachments/assets/a2ef0172-68e2-43c2-89c3-4863698f2f00"
/>
2026-05-27 18:57:10 -07:00
bicarus 311404f56b overlay: toast notifications (#704)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds toast notifications to the overlay.

Toast notifications are transient windows (shown for about 4 seconds).
These are drawn on top of the game even when the overlay is inactive
(i.e., when no window is open), and does not cause any input sink
behavior.

Add toasts for some common user-driven actions that could use a
notification, such as:

* screenshot
* card insert
* PIN macro
* API client connect/disconnect
* virtual printer
* screen resize toggle / scene switch

Add an option to change where the toasts are displayed, or to turn it
off entirely.

Add SDK function for it, and add it to the samples.

## Testing
Tested DDR (shown at bottom right, as is for most games) and RB (shown
top right by default).
2026-05-27 00:45:27 -07:00
bicarus-dev d929cdf7c8 clean up for release 2026-05-25 17:30:38 -07:00
137 changed files with 14450 additions and 2099 deletions
+5 -3
View File
@@ -11,9 +11,7 @@ assignees: ''
*name of game, version of game* *name of game, version of game*
## Version of spice2x ## Version of spice2x
*version of spice, this can be seen in the About tab, and in log.txt* *please remember to test the LATEST beta version before posting a bug about it*
*please remember to test the LATEST beta version before posting a bug about it**
## Describe the issue ## Describe the issue
*what's the issue?* *what's the issue?*
@@ -22,3 +20,7 @@ assignees: ''
> [!WARNING] > [!WARNING]
> Please make sure you remove any personally identifiable information from the log file. > Please make sure you remove any personally identifiable information from the log file.
> Please set `-loglevel` to `all` before launching the game for more verbose logs. > Please set `-loglevel` to `all` before launching the game for more verbose logs.
## A note on issue management
`Closed as not planned` means exactly that - it's closed, because it's not on our plans to address them any time soon. It doesn't mean your issue is invalid, it just means it's not a priority, and we can always revisit them later.
+100 -15
View File
@@ -235,6 +235,13 @@ add_compile_definitions(
_WIN32_IE=0x0400 _WIN32_IE=0x0400
) )
# SPICE_XP: set by build_all.sh for the WinXP-compat toolchains. Gates out
# the DX11 overlay backend
option(SPICE_XP "Build for the WinXP-compat toolchain" OFF)
if(SPICE_XP)
add_compile_definitions(SPICE_XP=1)
endif()
# acioemu log # acioemu log
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_LOG") #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_LOG")
@@ -401,6 +408,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/iidx/mf_wrappers.cpp games/iidx/mf_wrappers.cpp
games/sdvx/bi2x_hook.cpp games/sdvx/bi2x_hook.cpp
games/sdvx/sdvx.cpp games/sdvx/sdvx.cpp
games/sdvx/sdvx_live2d.cpp
games/sdvx/io.cpp games/sdvx/io.cpp
games/sdvx/camera.cpp games/sdvx/camera.cpp
games/jb/jb.cpp games/jb/jb.cpp
@@ -409,6 +417,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/nost/io.cpp games/nost/io.cpp
games/nost/poke.cpp games/nost/poke.cpp
games/gitadora/gitadora.cpp games/gitadora/gitadora.cpp
games/gitadora/asio.cpp
games/gitadora/io.cpp games/gitadora/io.cpp
games/gitadora/handle.cpp games/gitadora/handle.cpp
games/gitadora/j32d.cpp games/gitadora/j32d.cpp
@@ -486,6 +495,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
# hooks # hooks
hooks/audio/acm.cpp hooks/audio/acm.cpp
hooks/audio/audio.cpp hooks/audio/audio.cpp
hooks/audio/asio_driver_scan.cpp
hooks/audio/asio_proxy.cpp
hooks/audio/buffer.cpp hooks/audio/buffer.cpp
hooks/audio/mme.cpp hooks/audio/mme.cpp
hooks/audio/util.cpp hooks/audio/util.cpp
@@ -494,8 +505,13 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/audio/backends/mmdevice/device.cpp hooks/audio/backends/mmdevice/device.cpp
hooks/audio/backends/mmdevice/device_collection.cpp hooks/audio/backends/mmdevice/device_collection.cpp
hooks/audio/backends/mmdevice/device_enumerator.cpp hooks/audio/backends/mmdevice/device_enumerator.cpp
hooks/audio/backends/mmdevice/null_device.cpp
hooks/audio/backends/mmdevice/null_discard_backend.cpp
hooks/audio/backends/wasapi/audio_client.cpp hooks/audio/backends/wasapi/audio_client.cpp
hooks/audio/backends/wasapi/audio_render_client.cpp hooks/audio/backends/wasapi/audio_render_client.cpp
hooks/audio/backends/wasapi/downmix.cpp
hooks/audio/backends/wasapi/resample.cpp
hooks/audio/backends/wasapi/shared.cpp
hooks/audio/backends/wasapi/dummy_audio_client.cpp hooks/audio/backends/wasapi/dummy_audio_client.cpp
hooks/audio/backends/wasapi/dummy_audio_clock.cpp hooks/audio/backends/wasapi/dummy_audio_clock.cpp
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
@@ -514,9 +530,15 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/graphics/nvenc_hook.cpp hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.cpp hooks/graphics/backends/d3d9/d3d9_backend.cpp
hooks/graphics/backends/d3d9/d3d9_device.cpp hooks/graphics/backends/d3d9/d3d9_device.cpp
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_texture.cpp hooks/graphics/backends/d3d9/d3d9_texture.cpp
hooks/graphics/backends/d3d11/d3d11_backend.cpp
hooks/graphics/backends/d3d11/d3d11_swapchain.cpp
hooks/graphics/backends/d3d11/d3d11_factory.cpp
hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp
hooks/graphics/backends/d3d11/d3d11_screenshot.cpp
hooks/input/dinput8/fake_backend.cpp hooks/input/dinput8/fake_backend.cpp
hooks/input/dinput8/fake_device.cpp hooks/input/dinput8/fake_device.cpp
hooks/input/dinput8/hook.cpp hooks/input/dinput8/hook.cpp
@@ -558,6 +580,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
# overlay # overlay
overlay/overlay.cpp overlay/overlay.cpp
overlay/notifications.cpp
overlay/window.cpp overlay/window.cpp
overlay/imgui/extensions.cpp overlay/imgui/extensions.cpp
overlay/imgui/impl_spice.cpp overlay/imgui/impl_spice.cpp
@@ -584,10 +607,15 @@ set(SOURCE_FILES ${SOURCE_FILES}
overlay/windows/keypad.cpp overlay/windows/keypad.cpp
overlay/windows/log.cpp overlay/windows/log.cpp
overlay/windows/midi.cpp overlay/windows/midi.cpp
overlay/windows/obs.cpp
overlay/windows/obs_websocket.cpp
overlay/windows/patch_manager.cpp overlay/windows/patch_manager.cpp
overlay/windows/popn_sub.cpp overlay/windows/popn_sub.cpp
overlay/windows/wnd_manager.cpp overlay/windows/wnd_manager.cpp
# external
external/easywsclient/easywsclient.cpp
# rawinput # rawinput
rawinput/rawinput.cpp rawinput/rawinput.cpp
rawinput/sextet.cpp rawinput/sextet.cpp
@@ -650,37 +678,74 @@ set(SOURCE_FILES ${SOURCE_FILES}
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES}) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
# guard against statically importing DLLs that must always be loaded dynamically:
# * DLLs users override via the modules directory (e.g. DXVK's d3d9.dll) - a
# static import loads the system copy at startup and preempts the override
# (issue #779).
# * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks
# Unity games.
# the check runs objdump on each produced binary and fails the build if any
# forbidden DLL is imported.
set(SPICE_FORBIDDEN_STATIC_IMPORTS
d3d8.dll d3d9.dll d3d10core.dll d3d11.dll dxgi.dll opengl32.dll
mf.dll mfplat.dll mfreadwrite.dll)
function(spice_guard_dll_imports target)
if(MSVC)
# objdump-based parsing assumes a GNU/LLVM toolchain
return()
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DOBJDUMP=${CMAKE_OBJDUMP}
-DTARGET_FILE=$<TARGET_FILE:${target}>
"-DFORBIDDEN=${SPICE_FORBIDDEN_STATIC_IMPORTS}"
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_no_static_dll_imports.cmake
VERBATIM
COMMENT "Checking ${target} for forbidden static DLL imports")
endfunction()
# spice.exe / spice_laa.exe shared objects
###########################################
# spice.exe and spice_laa.exe are compiled identically; the only difference is
# the large-address-aware bit, which is set at link time. Compile the sources
# once into a shared OBJECT library so spice_laa.exe only costs an extra link
# instead of a full recompile.
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
target_link_libraries(spicetools_spice_objs
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
if(NOT MSVC)
set_target_properties(spicetools_spice_objs PROPERTIES COMPILE_FLAGS "-m32")
endif()
# spice.exe # spice.exe
########### ###########
set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/Win32D.rc)
add_executable(spicetools_spice ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_spice ${RESOURCE_FILES})
target_link_libraries(spicetools_spice target_link_libraries(spicetools_spice PRIVATE spicetools_spice_objs)
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_spice PUBLIC winscard)
set_target_properties(spicetools_spice PROPERTIES PREFIX "") set_target_properties(spicetools_spice PROPERTIES PREFIX "")
set_target_properties(spicetools_spice PROPERTIES OUTPUT_NAME "spice") set_target_properties(spicetools_spice PROPERTIES OUTPUT_NAME "spice")
IF(NOT MSVC) IF(NOT MSVC)
set_target_properties(spicetools_spice PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32") set_target_properties(spicetools_spice PROPERTIES LINK_FLAGS "-m32")
endif() endif()
# spice_laa.exe # spice_laa.exe
########### ###########
set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/Win32D.rc)
add_executable(spicetools_spice_laa ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_spice_laa ${RESOURCE_FILES})
target_link_libraries(spicetools_spice_laa target_link_libraries(spicetools_spice_laa PRIVATE spicetools_spice_objs)
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_spice_laa PUBLIC winscard)
set_target_properties(spicetools_spice_laa PROPERTIES PREFIX "") set_target_properties(spicetools_spice_laa PROPERTIES PREFIX "")
set_target_properties(spicetools_spice_laa PROPERTIES OUTPUT_NAME "spice_laa") set_target_properties(spicetools_spice_laa PROPERTIES OUTPUT_NAME "spice_laa")
target_compile_definitions(spicetools_spice_laa PRIVATE SPICE32_LARGE_ADDRESS_AWARE=1)
IF(NOT MSVC) IF(NOT MSVC)
set_target_properties(spicetools_spice_laa PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32 -Wl,--large-address-aware") set_target_properties(spicetools_spice_laa PROPERTIES LINK_FLAGS "-m32 -Wl,--large-address-aware")
endif() endif()
# spice_linux.exe # spice_linux.exe
@@ -713,6 +778,10 @@ target_link_libraries(spicetools_spice64 PUBLIC winscard)
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "") set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64") set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1) target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1)
if(NOT SPICE_XP)
target_link_libraries(spicetools_spice64 PUBLIC d3d11 dxgi)
target_compile_definitions(spicetools_spice64 PRIVATE SPICE_D3D11=1)
endif()
IF(NOT MSVC) IF(NOT MSVC)
set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64") set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
@@ -732,6 +801,9 @@ set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux") set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1) target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
target_compile_definitions(spicetools_spice64_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1) target_compile_definitions(spicetools_spice64_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
# spice64_linux is never built under the WinXP toolchain, so dx11 is always on.
target_link_libraries(spicetools_spice64_linux PUBLIC d3d11 dxgi)
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE_D3D11=1)
IF(NOT MSVC) IF(NOT MSVC)
set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64") set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
@@ -744,7 +816,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_cfg target_link_libraries(spicetools_cfg
PUBLIC ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_cfg PUBLIC winscard) target_link_libraries(spicetools_cfg PUBLIC winscard)
set_target_properties(spicetools_cfg PROPERTIES PREFIX "") set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
@@ -762,7 +834,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_cfg_linux target_link_libraries(spicetools_cfg_linux
PUBLIC ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "") set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux") set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
@@ -917,3 +989,16 @@ set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stu
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64" ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64")
# forbidden static DLL import guard
###################################
# apply the check to every executable and DLL produced by this project.
get_property(spice_all_targets DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS)
foreach(spice_target IN LISTS spice_all_targets)
get_target_property(spice_target_type ${spice_target} TYPE)
if(spice_target_type STREQUAL "EXECUTABLE"
OR spice_target_type STREQUAL "SHARED_LIBRARY"
OR spice_target_type STREQUAL "MODULE_LIBRARY")
spice_guard_dll_imports(${spice_target})
endif()
endforeach()
+8
View File
@@ -11,6 +11,8 @@
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
#include "overlay/notifications.h"
#include "module.h" #include "module.h"
#include "modules/analogs.h" #include "modules/analogs.h"
#include "modules/buttons.h" #include "modules/buttons.h"
@@ -197,6 +199,9 @@ void Controller::connection_handler(api::ClientState client_state) {
// log connection // log connection
log_info("api", "client connected: {}", client_address_str); log_info("api", "client connected: {}", client_address_str);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("API client connected ({})", client_address_str));
client_states_m.lock(); client_states_m.lock();
client_states.emplace_back(&client_state); client_states.emplace_back(&client_state);
client_states_m.unlock(); client_states_m.unlock();
@@ -277,6 +282,9 @@ void Controller::connection_handler(api::ClientState client_state) {
// log disconnect // log disconnect
log_info("api", "client disconnected: {}", client_address_str); log_info("api", "client disconnected: {}", client_address_str);
overlay::notifications::add(
overlay::notifications::Severity::Info,
fmt::format("API client disconnected ({})", client_address_str));
client_states_m.lock(); client_states_m.lock();
client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state)); client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
client_states_m.unlock(); client_states_m.unlock();
+59 -15
View File
@@ -1,5 +1,7 @@
#include "capture.h" #include "capture.h"
#include <functional> #include <functional>
#include <mutex>
#include <unordered_map>
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "util/crypt.h" #include "util/crypt.h"
@@ -14,6 +16,56 @@ namespace api::modules {
static thread_local std::vector<uint8_t> CAPTURE_BUFFER; static thread_local std::vector<uint8_t> CAPTURE_BUFFER;
struct CachedFrame {
std::vector<uint8_t> jpeg;
uint64_t timestamp = 0;
int width = 0;
int height = 0;
};
static std::mutex FRAME_CACHE_M;
static std::unordered_map<int, CachedFrame> FRAME_CACHE;
static void add_jpeg_response(
int screen,
uint64_t timestamp,
int width,
int height,
const std::vector<uint8_t> &jpeg,
Response &res) {
auto encoded = crypt::base64_encode(jpeg.data(), jpeg.size());
Value data;
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
res.add_data(timestamp);
res.add_data(width);
res.add_data(height);
res.add_data(data);
std::lock_guard<std::mutex> lock(FRAME_CACHE_M);
FRAME_CACHE[screen] = {jpeg, timestamp, width, height};
}
static bool try_cached_response(int screen, Response &res) {
std::lock_guard<std::mutex> lock(FRAME_CACHE_M);
const auto pos = FRAME_CACHE.find(screen);
if (pos == FRAME_CACHE.end() || pos->second.jpeg.empty()) {
return false;
}
const auto &cached = pos->second;
auto encoded = crypt::base64_encode(cached.jpeg.data(), cached.jpeg.size());
Value data;
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
res.add_data(cached.timestamp);
res.add_data(cached.width);
res.add_data(cached.height);
res.add_data(data);
return true;
}
Capture::Capture() : Module("capture") { Capture::Capture() : Module("capture") {
functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2); functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2);
functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2); functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2);
@@ -41,6 +93,7 @@ namespace api::modules {
* reduce: uint for dividing image size * reduce: uint for dividing image size
*/ */
void Capture::get_jpg(Request &req, Response &res) { void Capture::get_jpg(Request &req, Response &res) {
CAPTURE_BUFFER.clear();
CAPTURE_BUFFER.reserve(1024 * 128); CAPTURE_BUFFER.reserve(1024 * 128);
// settings // settings
@@ -71,24 +124,15 @@ namespace api::modules {
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) { bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
CAPTURE_BUFFER.push_back(byte); CAPTURE_BUFFER.push_back(byte);
}, true, quality, true, divide, &timestamp, &width, &height); }, true, quality, true, divide, &timestamp, &width, &height);
if (!success) {
if (success) {
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
CAPTURE_BUFFER.clear();
return; return;
} }
// encode to base64 // fall back to the last successful frame while the game is busy loading
auto encoded = crypt::base64_encode(
CAPTURE_BUFFER.data(),
CAPTURE_BUFFER.size());
// clear buffer
CAPTURE_BUFFER.clear(); CAPTURE_BUFFER.clear();
try_cached_response(screen, res);
// add data to response
Value data;
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
res.add_data(timestamp);
res.add_data(width);
res.add_data(height);
res.add_data(data);
} }
} }
+7
View File
@@ -5,6 +5,7 @@
#include "util/utils.h" #include "util/utils.h"
#include "util/rc4.h" #include "util/rc4.h"
#include "util/logging.h" #include "util/logging.h"
#include "overlay/notifications.h"
#include "controller.h" #include "controller.h"
using namespace headsocket; using namespace headsocket;
@@ -91,12 +92,18 @@ namespace api {
// log connection // log connection
log_info("api::websocket", "client connected"); log_info("api::websocket", "client connected");
overlay::notifications::add(
overlay::notifications::Severity::Success,
"API websocket client connected");
} }
void WebSocketClient::on_disconnect() { void WebSocketClient::on_disconnect() {
// log disconnection // log disconnection
log_info("api::websocket", "client disconnected"); log_info("api::websocket", "client disconnected");
overlay::notifications::add(
overlay::notifications::Severity::Info,
"API websocket client disconnected");
// get pointer to server // get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get()); auto srv = reinterpret_cast<WebSocketServer *>(server().get());
+2 -2
View File
@@ -390,8 +390,8 @@ namespace avs {
// fall back to default PCBID if node is not found // fall back to default PCBID if node is not found
if (!EA3_PCBID[0] && PCBID_CUSTOM.empty()) { if (!EA3_PCBID[0] && PCBID_CUSTOM.empty()) {
log_warning("avs-ea3", "no PCBID set, falling back to default PCBID value (04040000000000000000)"); log_warning("avs-ea3", "no PCBID set, falling back to default PCBID value (01201000000000010101)");
PCBID_CUSTOM = "04040000000000000000"; PCBID_CUSTOM = "01201000000000010101";
} }
// custom PCBID // custom PCBID
File diff suppressed because one or more lines are too long
+44 -18
View File
@@ -91,14 +91,20 @@ fi
# is the XP-compatible toolchain installed? # is the XP-compatible toolchain installed?
XP_MUST_BUILD=0 XP_MUST_BUILD=0
BUILD_XP=0 # 64-bit WinXP builds are disabled by default; set to 1 to opt in
if [ -f "$TOOLCHAIN_WINXP_32" ] && [ -f "$TOOLCHAIN_WINXP_64" ]; then BUILD_XP_64_ENABLE=0
BUILD_XP=1; BUILD_XP_32=0
BUILD_XP_64=0
if [ -f "$TOOLCHAIN_WINXP_32" ]; then
BUILD_XP_32=1;
elif ((XP_MUST_BUILD > 0)) elif ((XP_MUST_BUILD > 0))
then then
echo "WinXP toolchain not available, aborting" echo "WinXP 32bit toolchain not available, aborting"
exit 1 exit 1
fi fi
if ((BUILD_XP_64_ENABLE > 0)) && [ -f "$TOOLCHAIN_WINXP_64" ]; then
BUILD_XP_64=1;
fi
# determine number of cores # determine number of cores
CORES=$(nproc) CORES=$(nproc)
@@ -113,12 +119,17 @@ echo "Git Branch: $GIT_BRANCH"
echo "Git Head: $GIT_HEAD" echo "Git Head: $GIT_HEAD"
echo "Toolchain for 32bit targets: $TOOLCHAIN_32" echo "Toolchain for 32bit targets: $TOOLCHAIN_32"
echo "Toolchain for 64bit targets: $TOOLCHAIN_64" echo "Toolchain for 64bit targets: $TOOLCHAIN_64"
if ((BUILD_XP > 0)) if ((BUILD_XP_32 > 0))
then then
echo "Toolchain for WinXP 32bit targets: $TOOLCHAIN_WINXP_32" echo "Toolchain for WinXP 32bit targets: $TOOLCHAIN_WINXP_32"
else
echo "WinXP 32bit toolchain not available, skipping WinXP 32bit builds"
fi
if ((BUILD_XP_64 > 0))
then
echo "Toolchain for WinXP 64bit targets: $TOOLCHAIN_WINXP_64" echo "Toolchain for WinXP 64bit targets: $TOOLCHAIN_WINXP_64"
else else
echo "WinXP toolchain not available, skipping WinXP builds" echo "WinXP 64bit builds disabled, skipping WinXP 64bit builds"
fi fi
echo "Distribution Name: $DIST_NAME" echo "Distribution Name: $DIST_NAME"
echo "Build Type: $BUILD_TYPE" echo "Build Type: $BUILD_TYPE"
@@ -160,7 +171,7 @@ time (
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_64} cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_64}
popd > /dev/null popd > /dev/null
if ((BUILD_XP > 0)) if ((BUILD_XP_32 > 0))
then then
# 32 bit Windows XP # 32 bit Windows XP
echo "" echo ""
@@ -172,9 +183,15 @@ time (
fi fi
mkdir -p ${BUILDDIR_WINXP_32} mkdir -p ${BUILDDIR_WINXP_32}
pushd ${BUILDDIR_WINXP_32} > /dev/null pushd ${BUILDDIR_WINXP_32} > /dev/null
CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP32} cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP32}
popd > /dev/null popd > /dev/null
else
echo ""
echo "Skipping WinXP 32bit builds, toolchain not specified"
fi
if ((BUILD_XP_64 > 0))
then
# 64 bit Windows XP # 64 bit Windows XP
echo "" echo ""
echo "Building 64bit targets (WinXP toolchain)..." echo "Building 64bit targets (WinXP toolchain)..."
@@ -185,11 +202,11 @@ time (
fi fi
mkdir -p ${BUILDDIR_WINXP_64} mkdir -p ${BUILDDIR_WINXP_64}
pushd ${BUILDDIR_WINXP_64} > /dev/null pushd ${BUILDDIR_WINXP_64} > /dev/null
CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP64} cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP64}
popd > /dev/null popd > /dev/null
else else
echo "" echo ""
echo "Skipping WinXP builds, toolchain not specified" echo "Skipping WinXP 64bit builds"
fi fi
echo "" echo ""
@@ -197,7 +214,7 @@ time (
echo "===========================" echo "==========================="
) )
if ((BUILD_XP > 0)) if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then then
echo "" echo ""
echo "Checking XP compatibility..." echo "Checking XP compatibility..."
@@ -205,11 +222,17 @@ then
if ! command -v windows_dll_compat_checker &> /dev/null; then if ! command -v windows_dll_compat_checker &> /dev/null; then
echo "WARNING: windows_dll_compat_checker not found, skipping XP compatibility check" echo "WARNING: windows_dll_compat_checker not found, skipping XP compatibility check"
else else
windows_dll_compat_checker -s PREMADE/winxp_x86_64.ini \ if ((BUILD_XP_64 > 0))
${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe then
windows_dll_compat_checker -s PREMADE/winxp_x86_64_32bit_dlls.ini \ windows_dll_compat_checker -s PREMADE/winxp_x86_64.ini \
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \ ${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe fi
if ((BUILD_XP_32 > 0))
then
windows_dll_compat_checker -s PREMADE/winxp_x86_64_32bit_dlls.ini \
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
fi
fi fi
fi fi
@@ -271,7 +294,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux mkdir -p ${OUTDIR_EXTRAS}/linux
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32 mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64 mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
if ((BUILD_XP > 0)) if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then then
mkdir -p ${OUTDIR_EXTRAS}/winxp mkdir -p ${OUTDIR_EXTRAS}/winxp
fi fi
@@ -301,10 +324,13 @@ else
cp ${BUILDDIR_32}/spicetools/32/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/32/v0_flat_c.dll 2>/dev/null cp ${BUILDDIR_32}/spicetools/32/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/32/v0_flat_c.dll 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_flat_c.dll 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_flat_c.dll 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_cpp.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_cpp.dll 2>/dev/null cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_cpp.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_cpp.dll 2>/dev/null
if ((BUILD_XP > 0)) if ((BUILD_XP_32 > 0))
then then
cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
cp ${BUILDDIR_WINXP_32}/spicetools/32/spice.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null cp ${BUILDDIR_WINXP_32}/spicetools/32/spice.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
fi
if ((BUILD_XP_64 > 0))
then
cp ${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null cp ${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
fi fi
fi fi
+31 -9
View File
@@ -98,7 +98,7 @@ bool Config::getStatus() {
return this->status; return this->status;
} }
bool Config::addGame(Game &game) { bool Config::addGame(Game &game, bool save) {
tinyxml2::XMLNode *rootNode = this->configFile.LastChild(); tinyxml2::XMLNode *rootNode = this->configFile.LastChild();
tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game"); tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game");
@@ -486,13 +486,20 @@ bool Config::addGame(Game &game) {
rootNode->InsertEndChild(gameNode); rootNode->InsertEndChild(gameNode);
} }
// save config // save config (skipped when caller batches multiple addGame calls
this->saveConfigFile(); // and flushes once at the end via Config::save())
if (save) {
this->saveConfigFile();
}
// return success // return success
return true; return true;
} }
void Config::save() {
this->saveConfigFile();
}
bool Config::updateBinding(const Game &game, const Button &button, int alternative) { bool Config::updateBinding(const Game &game, const Button &button, int alternative) {
// get root node // get root node
@@ -1324,17 +1331,32 @@ bool Config::firstFillConfigFile() {
} }
void Config::saveConfigFile() { void Config::saveConfigFile() {
// create a .tmp file and write to it... // write the new config to a .tmp file first...
const auto xml_result = this->configFile.SaveFile(this->configLocationTemp.c_str(), false); const auto xml_result = this->configFile.SaveFile(this->configLocationTemp.c_str(), false);
if (xml_result != tinyxml2::XMLError::XML_SUCCESS) { if (xml_result != tinyxml2::XMLError::XML_SUCCESS) {
log_info("cfg", "failed to write file: {}", this->configLocationTemp); log_info("cfg", "failed to write file: {}", this->configLocationTemp);
return; return;
} }
// copy the .tmp file to the main file...
if (CopyFileW(this->configLocationTemp.c_str(), this->configLocation.c_str(), false) == 0) { // ...flush the .tmp file to disk so a crash/power loss can't leave it half-written...
log_warning("cfg", "CopyFileA failed: 0x{:08x}", GetLastError()); HANDLE tmp_handle = CreateFileW(
this->configLocationTemp.c_str(),
GENERIC_WRITE, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (tmp_handle != INVALID_HANDLE_VALUE) {
FlushFileBuffers(tmp_handle);
CloseHandle(tmp_handle);
}
// ...then atomically replace the real config with the .tmp file.
// Unlike CopyFile (which truncates and rewrites the destination in place),
// an NTFS rename is atomic: the existing config is never left half-overwritten,
// so an interrupted save can't corrupt it. MoveFileEx also removes the .tmp on success.
if (MoveFileExW(
this->configLocationTemp.c_str(),
this->configLocation.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) {
log_warning("cfg", "MoveFileExW failed: 0x{:08x}", GetLastError());
return; return;
} }
// delete the .tmp file (not critical if this fails)
DeleteFileW(this->configLocationTemp.c_str());
} }
+6 -1
View File
@@ -22,7 +22,12 @@ public:
bool getStatus(); bool getStatus();
bool createConfigFile(); bool createConfigFile();
bool addGame(Game &game); bool addGame(Game &game, bool save = true);
// flush pending in-memory changes to disk; intended to be paired with
// addGame(game, false) in bulk-init paths so we write the XML once instead
// of once per game
void save();
bool updateBinding(const Game &game, const Button &button, int alternative); bool updateBinding(const Game &game, const Button &button, int alternative);
bool updateBinding(const Game &game, const Analog &analog); bool updateBinding(const Game &game, const Analog &analog);
+94 -4
View File
@@ -1,11 +1,16 @@
#include "configurator.h" #include "configurator.h"
#include <d3d9.h>
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace cfg { namespace cfg {
// globals // globals
bool CONFIGURATOR_STANDALONE = false; bool CONFIGURATOR_STANDALONE = false;
bool CONFIGURATOR_FORCE_SOFTWARE_RENDER = false;
ConfigType CONFIGURATOR_TYPE = ConfigType::Config; ConfigType CONFIGURATOR_TYPE = ConfigType::Config;
Configurator::Configurator() { Configurator::Configurator() {
@@ -16,13 +21,98 @@ namespace cfg {
CONFIGURATOR_STANDALONE = false; CONFIGURATOR_STANDALONE = false;
} }
// Attempt to bring up a D3D9 device backing the configurator's window so the
// overlay can use the hardware-accelerated imgui_impl_dx9 path instead of
// the CPU rasterizer. Returns true if both Direct3DCreate9 and CreateDevice
// succeed; the caller falls back to overlay::create_software() otherwise so
// that environments without D3D9 (Wine without dxvk, headless test boxes,
// GPUs whose drivers reject HAL) still get a working configurator.
static bool try_init_d3d9(ConfiguratorWindow &wnd) {
if (cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER) {
return false;
}
if (wnd.hWnd == nullptr) {
return false;
}
// load d3d9.dll dynamically rather than linking against it statically.
// a static import would force the system d3d9.dll to load at process
// startup for the main spice executable too (this file is shared with
// spice.exe), which loads system32\d3d9.dll before the modules directory
// is added to the DLL search path - preventing a user-supplied DXVK
// d3d9.dll in modules from ever loading for the game.
typedef IDirect3D9 *(WINAPI *Direct3DCreate9_t)(UINT);
HMODULE d3d9_module = libutils::try_library("d3d9.dll");
if (d3d9_module == nullptr) {
log_warning("configurator", "could not load d3d9.dll, falling back to software renderer");
return false;
}
auto Direct3DCreate9_fn =
reinterpret_cast<Direct3DCreate9_t>(libutils::try_proc(d3d9_module, "Direct3DCreate9"));
if (Direct3DCreate9_fn == nullptr) {
log_warning("configurator", "could not find Direct3DCreate9, falling back to software renderer");
return false;
}
IDirect3D9 *d3d = Direct3DCreate9_fn(D3D_SDK_VERSION);
if (d3d == nullptr) {
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
return false;
}
D3DPRESENT_PARAMETERS pp {};
pp.Windowed = TRUE;
pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
// D3DFMT_UNKNOWN -> driver picks the current desktop format
pp.BackBufferFormat = D3DFMT_UNKNOWN;
pp.BackBufferWidth = static_cast<UINT>(wnd.client_width);
pp.BackBufferHeight = static_cast<UINT>(wnd.client_height);
pp.hDeviceWindow = wnd.hWnd;
pp.EnableAutoDepthStencil = FALSE;
pp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
IDirect3DDevice9 *device = nullptr;
// SOFTWARE_VERTEXPROCESSING is the most compatible behavior flag; the UI
// is tiny so we don't need hardware T&L. FPU_PRESERVE keeps our floating
// point environment intact in case other spice code relies on it.
const DWORD behavior_flags = D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_FPU_PRESERVE;
HRESULT hr = d3d->CreateDevice(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
wnd.hWnd,
behavior_flags,
&pp,
&device);
if (FAILED(hr) || device == nullptr) {
log_warning("configurator",
"D3D9 CreateDevice failed (hr={:#x}), falling back to software renderer",
static_cast<unsigned int>(hr));
d3d->Release();
return false;
}
wnd.d3d = d3d;
wnd.device = device;
wnd.pp = pp;
wnd.use_d3d9 = true;
return true;
}
void Configurator::run() { void Configurator::run() {
// create instance // bring up the overlay against either a real D3D9 device or the software
// rasterizer. The choice is one-shot - the in-game overlay always uses
// the same renderer the configurator picked here.
overlay::ENABLED = true; overlay::ENABLED = true;
overlay::create_software(this->wnd.hWnd); if (try_init_d3d9(this->wnd)) {
log_info("configurator", "using D3D9 hardware-accelerated renderer");
overlay::create_d3d9(this->wnd.hWnd, this->wnd.d3d, this->wnd.device);
} else {
log_info("configurator", "using software renderer");
overlay::create_software(this->wnd.hWnd);
}
overlay::OVERLAY->set_active(true); overlay::OVERLAY->set_active(true);
overlay::OVERLAY->hotkeys_enable = false;
ImGui::GetIO().MouseDrawCursor = false; ImGui::GetIO().MouseDrawCursor = false;
// run window // run window
+1
View File
@@ -11,6 +11,7 @@ namespace cfg {
// globals // globals
extern bool CONFIGURATOR_STANDALONE; extern bool CONFIGURATOR_STANDALONE;
extern ConfigType CONFIGURATOR_TYPE; extern ConfigType CONFIGURATOR_TYPE;
extern bool CONFIGURATOR_FORCE_SOFTWARE_RENDER;
class Configurator { class Configurator {
private: private:
+472 -43
View File
@@ -1,11 +1,18 @@
#include "configurator_wnd.h" #include "configurator_wnd.h"
#include <algorithm>
#include <cstring>
#include <windows.h> #include <windows.h>
#include <windowsx.h>
#include "build/defs.h" #include "build/defs.h"
#include "external/imgui/imgui.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "icon.h" #include "icon.h"
@@ -14,8 +21,177 @@ static const char *CLASS_NAME = "ConfiguratorWindow";
static std::string WINDOW_TITLE; static std::string WINDOW_TITLE;
static int WINDOW_SIZE_X = 800; static int WINDOW_SIZE_X = 800;
static int WINDOW_SIZE_Y = 600; static int WINDOW_SIZE_Y = 600;
static const int WINDOW_MIN_SIZE_X = 540;
static const int WINDOW_MIN_SIZE_Y = 300;
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON)); static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
static const UINT_PTR RENDER_TIMER_ID = 1;
// Map a virtual key code to the matching ImGuiKey. Mirrors the table used by
// the in-game ImGui spice backend so navigation keys behave identically when
// the configurator runs standalone and drives ImGui from Win32 messages.
static ImGuiKey vk_to_imgui_key(WPARAM vkey) {
switch (vkey) {
case VK_TAB: return ImGuiKey_Tab;
case VK_LEFT: return ImGuiKey_LeftArrow;
case VK_RIGHT: return ImGuiKey_RightArrow;
case VK_UP: return ImGuiKey_UpArrow;
case VK_DOWN: return ImGuiKey_DownArrow;
case VK_PRIOR: return ImGuiKey_PageUp;
case VK_NEXT: return ImGuiKey_PageDown;
case VK_HOME: return ImGuiKey_Home;
case VK_END: return ImGuiKey_End;
case VK_INSERT: return ImGuiKey_Insert;
case VK_DELETE: return ImGuiKey_Delete;
case VK_BACK: return ImGuiKey_Backspace;
case VK_SPACE: return ImGuiKey_Space;
case VK_RETURN: return ImGuiKey_Enter;
case VK_ESCAPE: return ImGuiKey_Escape;
case VK_LSHIFT: return ImGuiKey_LeftShift;
case VK_RSHIFT: return ImGuiKey_RightShift;
case VK_SHIFT: return ImGuiKey_LeftShift;
case VK_LCONTROL: return ImGuiKey_LeftCtrl;
case VK_RCONTROL: return ImGuiKey_RightCtrl;
case VK_CONTROL: return ImGuiKey_LeftCtrl;
case 'A': return ImGuiKey_A;
case 'C': return ImGuiKey_C;
case 'V': return ImGuiKey_V;
case 'X': return ImGuiKey_X;
case 'Y': return ImGuiKey_Y;
case 'Z': return ImGuiKey_Z;
default: return ImGuiKey_None;
}
}
static ImGuiKey vk_to_imgui_mod_key(WPARAM vkey) {
switch (vkey) {
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
return ImGuiMod_Shift;
case VK_CONTROL:
case VK_LCONTROL:
case VK_RCONTROL:
return ImGuiMod_Ctrl;
case VK_MENU:
case VK_LMENU:
case VK_RMENU:
return ImGuiMod_Alt;
default:
return ImGuiMod_None;
}
}
static cfg::ConfiguratorWindow *get_state(HWND hWnd) {
return reinterpret_cast<cfg::ConfiguratorWindow *>(
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
}
// computes the top-left position that centers a window of the given size on
// the primary monitor's work area (the desktop minus the taskbar); falls back
// to (0, 0) if the monitor info can't be queried.
static POINT center_on_primary_monitor(int width, int height) {
POINT pos = { 0, 0 };
HMONITOR mon = MonitorFromPoint(pos, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO mi {};
mi.cbSize = sizeof(mi);
if (GetMonitorInfo(mon, &mi)) {
const int work_w = mi.rcWork.right - mi.rcWork.left;
const int work_h = mi.rcWork.bottom - mi.rcWork.top;
pos.x = mi.rcWork.left + (work_w - width) / 2;
pos.y = mi.rcWork.top + (work_h - height) / 2;
}
return pos;
}
// Returns the refresh rate (Hz) of the monitor the window currently lives on,
// clamped to a sane range. Falls back to 60 if detection fails or the value
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
static UINT detect_monitor_refresh_hz(HWND hWnd) {
UINT hz = 0;
HMONITOR mon = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
if (mon) {
MONITORINFOEXW mi {};
mi.cbSize = sizeof(mi);
if (GetMonitorInfoW(mon, reinterpret_cast<LPMONITORINFO>(&mi))) {
DEVMODEW dm {};
dm.dmSize = sizeof(dm);
if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm)) {
hz = dm.dmDisplayFrequency;
}
}
}
if (hz <= 1) {
HDC hdc = GetDC(hWnd);
if (hdc) {
int v = GetDeviceCaps(hdc, VREFRESH);
if (v > 1) {
hz = static_cast<UINT>(v);
}
ReleaseDC(hWnd, hdc);
}
}
if (hz < 30 || hz > 240) {
hz = 60;
}
return hz;
}
// Software-path WM_PAINT: blit the overlay's pixel buffer to the window using
// SetDIBitsToDevice. Avoids creating/destroying a GDI HBITMAP every frame.
static void paint_software(HWND hWnd) {
if (!overlay::OVERLAY) {
PAINTSTRUCT ps {};
BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
return;
}
int width = 0;
int height = 0;
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
if (pixel_data && width > 0 && height > 0) {
BITMAPINFO bmi {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
// negative height -> top-down DIB, matching how ImGui packs pixels
bmi.bmiHeader.biHeight = -height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
SetDIBitsToDevice(
hdc,
0, 0,
width, height,
0, 0,
0, height,
pixel_data,
&bmi,
DIB_RGB_COLORS);
}
EndPaint(hWnd, &paint);
}
void cfg::ConfiguratorWindow::start_timer() {
if (!this->timer_running && this->hWnd) {
SetTimer(this->hWnd, RENDER_TIMER_ID, this->timer_interval_ms, nullptr);
this->timer_running = true;
}
}
void cfg::ConfiguratorWindow::stop_timer() {
if (this->timer_running && this->hWnd) {
KillTimer(this->hWnd, RENDER_TIMER_ID);
this->timer_running = false;
}
}
cfg::ConfiguratorWindow::ConfiguratorWindow() { cfg::ConfiguratorWindow::ConfiguratorWindow() {
// register the window class // register the window class
@@ -27,12 +203,22 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
wc.hIcon = WINDOW_ICON; wc.hIcon = WINDOW_ICON;
RegisterClass(&wc); RegisterClass(&wc);
// raise SetTimer resolution so high refresh rates (120/144Hz) are actually
// achievable. Without this, USER timers quantize to ~15.6ms and cap us
// near ~64 FPS. Uses the shared helper so it respects -notimerhacks
// (Use Legacy Timers) and the Win11 timer-throttling opt-out. The helper
// intentionally never calls timeEndPeriod; the kernel releases the request
// when spicecfg exits.
timeutils::set_timer_resolution();
// determine window title // determine window title
if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) { if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")"; WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")";
WINDOW_SIZE_X = 800; WINDOW_SIZE_X = 800;
WINDOW_SIZE_Y = 600; WINDOW_SIZE_Y = 600;
} }
this->client_width = WINDOW_SIZE_X;
this->client_height = WINDOW_SIZE_Y;
// open window // open window
this->hWnd = CreateWindowEx( this->hWnd = CreateWindowEx(
@@ -48,27 +234,58 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
if (this->hWnd) { if (this->hWnd) {
overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = true; overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = true;
// force dark title bar
set_window_dark_titlebar(this->hWnd, true);
} }
} }
cfg::ConfiguratorWindow::~ConfiguratorWindow() { cfg::ConfiguratorWindow::~ConfiguratorWindow() {
this->stop_timer();
// close window // close window
DestroyWindow(this->hWnd); DestroyWindow(this->hWnd);
// unregister class // unregister class
UnregisterClass(CLASS_NAME, GetModuleHandle(NULL)); UnregisterClass(CLASS_NAME, GetModuleHandle(NULL));
// release D3D9 resources if any
if (this->device) {
this->device->Release();
this->device = nullptr;
}
if (this->d3d) {
this->d3d->Release();
this->d3d = nullptr;
}
} }
void cfg::ConfiguratorWindow::run() { void cfg::ConfiguratorWindow::run() {
const POINT pos = center_on_primary_monitor(WINDOW_SIZE_X, WINDOW_SIZE_Y);
SetWindowPos(this->hWnd, HWND_TOP, pos.x, pos.y, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
// show window // show window
SetWindowPos(this->hWnd, HWND_TOP, 0, 0, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
ShowWindow(this->hWnd, SW_SHOWNORMAL); ShowWindow(this->hWnd, SW_SHOWNORMAL);
UpdateWindow(this->hWnd); UpdateWindow(this->hWnd);
// draw overlay in 60 FPS // SW_SHOWNORMAL usually activates a top-level window, but not always (e.g.,
SetTimer(this->hWnd, 1, 1000 / 60, nullptr); // when the launching process doesn't have foreground rights to transfer).
// Force foreground+focus explicitly so WM_MOUSEWHEEL is routed to us from
// the very first frame; the Win32 default routes wheel events to the
// keyboard-focused window.
SetForegroundWindow(this->hWnd);
SetFocus(this->hWnd);
// match the render timer to the monitor refresh rate so scrolling stays smooth
// on 60/120/144 Hz panels. Idle CPU is bounded by overlay::sw_pixels_dirty
// (software path) and overlay::d3d9_frame_dirty (D3D9 path); the timer is also
// paused entirely when the window is minimized (see WM_SIZE handler).
const UINT hz = detect_monitor_refresh_hz(this->hWnd);
this->timer_interval_ms = std::max<UINT>(1, 1000 / hz);
log_info("configurator", "render timer {} ms ({} Hz)",
this->timer_interval_ms, hz);
this->start_timer();
// window loop // window loop
BOOL ret; BOOL ret;
@@ -94,6 +311,22 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
} }
break; break;
} }
case WM_GETMINMAXINFO: {
// enforce a minimum window size so the UI can't be shrunk to a
// point where the tabs/controls become unusable. The minimum is
// expressed in client-area pixels and converted to a full window
// size that accounts for the current frame/title-bar style.
RECT rc = { 0, 0, WINDOW_MIN_SIZE_X, WINDOW_MIN_SIZE_Y };
DWORD style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_STYLE));
DWORD ex_style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_EXSTYLE));
if (AdjustWindowRectEx(&rc, style, FALSE, ex_style)) {
auto *mmi = reinterpret_cast<MINMAXINFO *>(lParam);
mmi->ptMinTrackSize.x = rc.right - rc.left;
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
}
break;
}
case WM_CREATE: { case WM_CREATE: {
// set user data of window to class pointer // set user data of window to class pointer
@@ -102,6 +335,72 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
break; break;
} }
case WM_SIZE: {
// pause/resume the render timer based on visibility
auto *state = get_state(hWnd);
if (state) {
if (wParam == SIZE_MINIMIZED) {
state->window_minimized = true;
state->stop_timer();
} else {
if (state->window_minimized) {
state->window_minimized = false;
// force a full repaint on the next frame
state->has_valid_draw_hash = false;
state->start_timer();
}
const int new_w = LOWORD(lParam);
const int new_h = HIWORD(lParam);
if (new_w > 0 && new_h > 0
&& (new_w != state->client_width || new_h != state->client_height)) {
state->client_width = new_w;
state->client_height = new_h;
// reset the D3D9 device with the new back-buffer size so the
// hardware-accelerated path matches the window dimensions.
if (state->use_d3d9 && state->device) {
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_invalidate();
}
state->pp.BackBufferWidth = static_cast<UINT>(new_w);
state->pp.BackBufferHeight = static_cast<UINT>(new_h);
HRESULT hr = state->device->Reset(&state->pp);
if (FAILED(hr)) {
log_warning("configurator", "D3D9 device Reset failed, hr={:#x}",
static_cast<unsigned int>(hr));
}
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_recreate();
}
}
// force a full repaint after resize
state->has_valid_draw_hash = false;
}
}
}
break;
}
case WM_DISPLAYCHANGE: {
// monitor refresh rate may have changed (or the window moved to a
// different monitor); re-detect and re-arm the render timer.
auto *state = get_state(hWnd);
if (state && state->timer_running) {
const UINT hz = detect_monitor_refresh_hz(hWnd);
const UINT new_interval = std::max<UINT>(1, 1000 / hz);
if (new_interval != state->timer_interval_ms) {
state->stop_timer();
state->timer_interval_ms = new_interval;
state->start_timer();
log_info("configurator",
"WM_DISPLAYCHANGE: render timer {} ms ({} Hz)",
new_interval, hz);
}
}
break;
}
case WM_CLOSE: case WM_CLOSE:
case WM_DESTROY: { case WM_DESTROY: {
@@ -110,62 +409,192 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
break; break;
} }
case WM_TIMER: { case WM_TIMER: {
if (wParam != RENDER_TIMER_ID) {
break;
}
// update overlay auto *state = get_state(hWnd);
// skip rendering when the window is minimized or hidden - the timer is
// already paused on minimize, but defensively skip here as well.
if (state && (state->window_minimized || !IsWindowVisible(hWnd))) {
break;
}
const bool use_d3d9 = state && state->use_d3d9 && state->device;
// build the imgui frame (input is already buffered via WM_* messages)
if (overlay::OVERLAY) { if (overlay::OVERLAY) {
overlay::OVERLAY->update(); overlay::OVERLAY->update();
overlay::OVERLAY->set_active(true); overlay::OVERLAY->set_active(true);
overlay::OVERLAY->new_frame(); overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
} }
// repaint window if (use_d3d9) {
InvalidateRect(hWnd, nullptr, TRUE); const HRESULT cl = state->device->TestCooperativeLevel();
if (cl == D3DERR_DEVICELOST) {
break;
}
if (cl == D3DERR_DEVICENOTRESET) {
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_invalidate();
}
const HRESULT reset_hr = state->device->Reset(&state->pp);
if (SUCCEEDED(reset_hr) && overlay::OVERLAY) {
overlay::OVERLAY->reset_recreate();
}
break;
}
if (FAILED(cl)) {
break;
}
if (overlay::OVERLAY) {
overlay::OVERLAY->render();
}
// skip Clear/Present when ImGui draw data is unchanged; the last
// presented frame stays visible (same fast path as sw_pixels_dirty).
const bool dirty = overlay::OVERLAY && overlay::OVERLAY->d3d9_frame_dirty;
const bool force_present = state && !state->has_valid_draw_hash;
if (dirty || force_present) {
if (state) {
state->has_valid_draw_hash = true;
}
state->device->Clear(0, nullptr, D3DCLEAR_TARGET,
D3DCOLOR_RGBA(20, 18, 18, 255), 1.0f, 0);
if (SUCCEEDED(state->device->BeginScene())) {
if (overlay::OVERLAY) {
overlay::OVERLAY->d3d9_render_draw(force_present);
}
state->device->EndScene();
}
const HRESULT hr = state->device->Present(nullptr, nullptr, nullptr, nullptr);
if (hr == D3DERR_DEVICELOST) {
break;
}
}
} else {
if (overlay::OVERLAY) {
overlay::OVERLAY->render();
}
// software path: the overlay's renderer already skipped the costly
// paint_imgui call when the draw data was unchanged. Only invalidate
// the window when those pixels actually changed, or on the first
// frame / after a resize when we lost the previously cached state.
const bool dirty = overlay::OVERLAY && overlay::OVERLAY->sw_pixels_dirty;
const bool force_repaint = state && !state->has_valid_draw_hash;
if (dirty || force_repaint) {
if (state) {
state->has_valid_draw_hash = true;
}
// pass FALSE for bErase - WM_ERASEBKGND already short-circuits, so
// skipping the erase region avoids one extra Win32 round trip.
InvalidateRect(hWnd, nullptr, FALSE);
}
}
break; break;
} }
case WM_ERASEBKGND: { case WM_ERASEBKGND: {
return 1; return 1;
} }
case WM_PAINT: { case WM_PAINT: {
paint_software(hWnd);
// render overlay break;
if (overlay::OVERLAY) { }
case WM_ACTIVATE: {
// get pixel data const WORD activation = LOWORD(wParam);
int width, height; if (activation == WA_INACTIVE) {
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height); // dropping any held mouse buttons so we don't get stuck in a
if (width > 0 && height > 0) { // "button still pressed" state when we come back.
auto &io = ImGui::GetIO();
// create bitmap io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data); io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Middle, false);
// prepare paint
PAINTSTRUCT paint{};
HDC hdc = BeginPaint(hWnd, &paint);
HDC hdcMem = CreateCompatibleDC(hdc);
SetBkMode(hdc, TRANSPARENT);
// draw bitmap
SelectObject(hdcMem, bitmap);
BitBlt(hdc, paint.rcPaint.left, paint.rcPaint.top,
paint.rcPaint.right - paint.rcPaint.left,
paint.rcPaint.bottom - paint.rcPaint.top,
hdcMem, paint.rcPaint.left, paint.rcPaint.top, SRCCOPY);
// delete bitmap
DeleteObject(bitmap);
// clean up
DeleteDC(hdcMem);
EndPaint(hWnd, &paint);
} else {
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
} else { } else {
return DefWindowProc(hWnd, uMsg, wParam, lParam); // WA_ACTIVE or WA_CLICKACTIVE: make sure the keyboard focus is
// actually on this window so WM_MOUSEWHEEL gets routed to us
// again. Without this, scrolling silently stops working after
// the user alt-tabs back to spicecfg.
SetFocus(hWnd);
} }
break; break;
} }
case WM_KILLFOCUS: {
// mirror the WA_INACTIVE mouse-button drop for the keyboard -
// without this, a key held during alt-tab stays "down" in ImGui
// state until the user presses and releases it again.
ImGui::GetIO().ClearInputKeys();
break;
}
// input messages routed straight into ImGui IO. This replaces the previous
// per-frame 256-VK rawinput scan in ImGui_ImplSpice_NewFrame for the
// standalone configurator (see CONFIGURATOR_STANDALONE branches there).
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP: {
if (wParam == VK_F4) {
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
const bool down = (uMsg == WM_KEYDOWN) || (uMsg == WM_SYSKEYDOWN);
auto &io = ImGui::GetIO();
const ImGuiKey key = vk_to_imgui_key(wParam);
if (key != ImGuiKey_None) {
io.AddKeyEvent(key, down);
}
const ImGuiKey mod = vk_to_imgui_mod_key(wParam);
if (mod != ImGuiMod_None) {
io.AddKeyEvent(mod, down);
}
break;
}
case WM_MOUSEMOVE: {
auto &io = ImGui::GetIO();
io.AddMousePosEvent(static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
break;
}
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP: {
int button = 0;
bool down = false;
switch (uMsg) {
case WM_LBUTTONDOWN: button = ImGuiMouseButton_Left; down = true; break;
case WM_LBUTTONUP: button = ImGuiMouseButton_Left; down = false; break;
case WM_RBUTTONDOWN: button = ImGuiMouseButton_Right; down = true; break;
case WM_RBUTTONUP: button = ImGuiMouseButton_Right; down = false; break;
case WM_MBUTTONDOWN: button = ImGuiMouseButton_Middle; down = true; break;
case WM_MBUTTONUP: button = ImGuiMouseButton_Middle; down = false; break;
}
auto &io = ImGui::GetIO();
io.AddMouseButtonEvent(button, down);
if (down) {
SetCapture(hWnd);
} else {
ReleaseCapture();
}
break;
}
case WM_MOUSEWHEEL: {
const float delta = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam))
/ static_cast<float>(WHEEL_DELTA);
ImGui::GetIO().AddMouseWheelEvent(0.0f, delta);
break;
}
#if !SPICE_XP
case WM_MOUSEHWHEEL: {
const float delta = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam))
/ static_cast<float>(WHEEL_DELTA);
ImGui::GetIO().AddMouseWheelEvent(delta, 0.0f);
break;
}
#endif
default: default:
return DefWindowProc(hWnd, uMsg, wParam, lParam); return DefWindowProc(hWnd, uMsg, wParam, lParam);
} }
+31 -1
View File
@@ -1,6 +1,9 @@
#pragma once #pragma once
#include <cstdint>
#include <windows.h> #include <windows.h>
#include <d3d9.h>
namespace cfg { namespace cfg {
@@ -9,11 +12,38 @@ namespace cfg {
HWND hWnd; HWND hWnd;
// optional D3D9 device backing the configurator window; nullptr when running
// the software-rendered path. Owned by ConfiguratorWindow when set.
IDirect3D9 *d3d = nullptr;
IDirect3DDevice9 *device = nullptr;
D3DPRESENT_PARAMETERS pp {};
bool use_d3d9 = false;
// throttling / pause state. timer_interval_ms is the default until run()
// refines it to the actual monitor refresh rate (see detect_monitor_refresh_hz).
UINT timer_interval_ms = 1000 / 60;
bool timer_running = false;
bool window_minimized = false;
// tracks whether we've issued at least one InvalidateRect since window
// creation/resize. The overlay's per-frame "pixels changed" flag suppresses
// idle blits; this flag forces the very first blit on startup or after
// a resize so the window doesn't show garbage until the user moves the mouse.
bool has_valid_draw_hash = false;
// dimensions of the configurator client area (kept in sync with WM_SIZE)
int client_width = 0;
int client_height = 0;
ConfiguratorWindow(); ConfiguratorWindow();
~ConfiguratorWindow(); ~ConfiguratorWindow();
void run(); void run();
// start/stop the render timer based on visibility state
void start_timer();
void stop_timer();
static LRESULT CALLBACK window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); static LRESULT CALLBACK window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
}; };
} }
+5 -8
View File
@@ -359,13 +359,12 @@ namespace overlay::windows {
return false; return false;
} }
std::error_code ec; if (MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) {
std::filesystem::rename(path_tmp, path, ec); log_warning("templates", "failed to rename templates file: 0x{:08x}", GetLastError());
if (ec) {
log_warning("templates", "failed to rename templates file: {}", ec.message());
return false; return false;
} }
log_info("templates", "templates file saved successfully");
return true; return true;
} }
@@ -397,8 +396,7 @@ namespace overlay::windows {
auto path_tmp = path; auto path_tmp = path;
path_tmp.replace_extension(L"tmp"); path_tmp.replace_extension(L"tmp");
if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) { if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
std::error_code ec; MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
std::filesystem::rename(path_tmp, path, ec);
} }
} }
@@ -434,8 +432,7 @@ namespace overlay::windows {
auto path_tmp = path; auto path_tmp = path;
path_tmp.replace_extension(L"tmp"); path_tmp.replace_extension(L"tmp");
if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) { if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
std::error_code ec; MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
std::filesystem::rename(path_tmp, path, ec);
} }
} }
+2
View File
@@ -46,6 +46,8 @@ struct OptionDefinition {
// for OptionPickerType::FilePath // for OptionPickerType::FilePath
std::string file_extension = ""; std::string file_extension = "";
std::string quick_setting_category = "";
}; };
class Option { class Option {
@@ -0,0 +1,72 @@
# fails the build if a PE binary statically imports a forbidden DLL.
#
# some DLLs must never end up in spice's static import table, for two reasons:
#
# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy
# into the modules directory to replace the system one. a static import
# forces the loader to load the SYSTEM copy at process startup - before the
# modules directory is added to the DLL search path and before the game DLL
# loads - so the user-supplied override never takes effect (see issue #779).
#
# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/
# mfreadwrite): a static import loads them eagerly and breaks Unity games.
#
# in both cases the DLL must instead be loaded dynamically (libutils::try_library
# / GetProcAddress / delay load) so it is only pulled in when actually needed.
#
# invoked via `cmake -P` from a POST_BUILD step. required -D variables:
# OBJDUMP - path to objdump (CMAKE_OBJDUMP)
# TARGET_FILE - path to the PE binary to inspect
# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject
if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}")
message(WARNING
"check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}")
return()
endif()
execute_process(
COMMAND "${OBJDUMP}" -p "${TARGET_FILE}"
OUTPUT_VARIABLE dump_output
RESULT_VARIABLE dump_result
ERROR_VARIABLE dump_error)
if(NOT dump_result EQUAL 0)
message(WARNING
"check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}")
return()
endif()
# both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per
# statically imported DLL in their PE private-header dump.
string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}")
set(violations "")
foreach(line IN LISTS dll_lines)
string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}")
string(STRIP "${dll_name}" dll_name)
string(TOLOWER "${dll_name}" dll_name_lower)
if(dll_name_lower IN_LIST FORBIDDEN)
list(APPEND violations "${dll_name}")
endif()
endforeach()
if(violations)
list(REMOVE_DUPLICATES violations)
string(REPLACE ";" ", " violations_str "${violations}")
message(FATAL_ERROR
"static DLL import check FAILED for ${TARGET_FILE}\n"
" forbidden static imports found: ${violations_str}\n"
"\n"
" these DLLs must never be statically imported by spice:\n"
" * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n"
" system copy at startup and preempts the modules override (issue #779).\n"
" * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n"
" Unity games.\n"
"\n"
" fix: load the DLL dynamically instead - replace the direct API call with a\n"
" libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n"
" call through the resolved function pointer.")
endif()
message(STATUS "static DLL import check passed for ${TARGET_FILE}")
+552
View File
@@ -0,0 +1,552 @@
// This code comes from:
// https://github.com/dhbaird/easywsclient
//
// To get the latest version:
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
#include "easywsclient.hpp"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment( lib, "ws2_32" )
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include <string>
typedef SOCKET socket_t;
#ifndef _SSIZE_T_DEFINED
typedef int ssize_t;
#define _SSIZE_T_DEFINED
#endif
#ifndef _SOCKET_T_DEFINED
typedef SOCKET socket_t;
#define _SOCKET_T_DEFINED
#endif
#if defined(_MSC_VER) && !defined(snprintf)
#define snprintf _snprintf_s
#endif
#include <stdint.h>
#define socketerrno WSAGetLastError()
#define SOCKET_EAGAIN_EINPROGRESS WSAEINPROGRESS
#define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK
#else
#include <fcntl.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <errno.h>
typedef int socket_t;
#ifndef INVALID_SOCKET
#define INVALID_SOCKET (-1)
#endif
#ifndef SOCKET_ERROR
#define SOCKET_ERROR (-1)
#endif
#define closesocket(s) ::close(s)
#include <errno.h>
#define socketerrno errno
#define SOCKET_EAGAIN_EINPROGRESS EAGAIN
#define SOCKET_EWOULDBLOCK EWOULDBLOCK
#endif
#include <vector>
#include <string>
#include <stdarg.h>
#include "util/logging.h"
// When false, easywsclient's diagnostics are suppressed. The host application
// opts in by setting this flag (see obs_websocket.cpp).
bool EASYWSCLIENT_LOGGING_ENABLED = false;
// Route easywsclient's diagnostic output through the project logger instead of
// writing to stderr, without editing the upstream source lines below: these two
// macros redirect fprintf(stderr, ...) / fputs(..., stderr) to log_misc.
// Only emitted when EASYWSCLIENT_LOGGING_ENABLED is set.
static inline void easywsclient_logf(const char *fmt, ...) {
if (!EASYWSCLIENT_LOGGING_ENABLED) {
return;
}
char buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
// trim trailing newline(s); the logger appends its own
size_t len = strlen(buf);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
buf[--len] = '\0';
}
log_misc("easywsclient", "{}", buf);
}
#define fprintf(stream, ...) easywsclient_logf(__VA_ARGS__)
#define fputs(str, stream) easywsclient_logf("%s", str)
using namespace easywsclient;
namespace { // private module-only namespace
socket_t hostname_connect(const std::string& hostname, int port) {
struct addrinfo hints;
struct addrinfo *result;
struct addrinfo *p;
int ret;
socket_t sockfd = INVALID_SOCKET;
char sport[16];
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
snprintf(sport, 16, "%d", port);
if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
return 1;
}
for(p = result; p != NULL; p = p->ai_next)
{
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == INVALID_SOCKET) { continue; }
if (connect(sockfd, p->ai_addr, p->ai_addrlen) != SOCKET_ERROR) {
break;
}
closesocket(sockfd);
sockfd = INVALID_SOCKET;
}
freeaddrinfo(result);
return sockfd;
}
class _DummyWebSocket : public easywsclient::WebSocket
{
public:
void poll(int timeout) { }
void send(const std::string& message) { }
void sendBinary(const std::string& message) { }
void sendBinary(const std::vector<uint8_t>& message) { }
void sendPing() { }
void close() { }
readyStateValues getReadyState() const { return CLOSED; }
void _dispatch(Callback_Imp & callable) { }
void _dispatchBinary(BytesCallback_Imp& callable) { }
};
class _RealWebSocket : public easywsclient::WebSocket
{
public:
// http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-------+-+-------------+-------------------------------+
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
// |I|S|S|S| (4) |A| (7) | (16/64) |
// |N|V|V|V| |S| | (if payload len==126/127) |
// | |1|2|3| |K| | |
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
// | Extended payload length continued, if payload len == 127 |
// + - - - - - - - - - - - - - - - +-------------------------------+
// | |Masking-key, if MASK set to 1 |
// +-------------------------------+-------------------------------+
// | Masking-key (continued) | Payload Data |
// +-------------------------------- - - - - - - - - - - - - - - - +
// : Payload Data continued ... :
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
// | Payload Data continued ... |
// +---------------------------------------------------------------+
struct wsheader_type {
unsigned header_size;
bool fin;
bool mask;
enum opcode_type {
CONTINUATION = 0x0,
TEXT_FRAME = 0x1,
BINARY_FRAME = 0x2,
CLOSE = 8,
PING = 9,
PONG = 0xa,
} opcode;
int N0;
uint64_t N;
uint8_t masking_key[4];
};
std::vector<uint8_t> rxbuf;
std::vector<uint8_t> txbuf;
std::vector<uint8_t> receivedData;
socket_t sockfd;
readyStateValues readyState;
bool useMask;
bool isRxBad;
_RealWebSocket(socket_t sockfd, bool useMask) : sockfd(sockfd), readyState(OPEN), useMask(useMask), isRxBad(false) {
}
readyStateValues getReadyState() const {
return readyState;
}
void poll(int timeout) { // timeout in milliseconds
if (readyState == CLOSED) {
if (timeout > 0) {
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
select(0, NULL, NULL, NULL, &tv);
}
return;
}
if (timeout != 0) {
fd_set rfds;
fd_set wfds;
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(sockfd, &rfds);
if (txbuf.size()) { FD_SET(sockfd, &wfds); }
select(sockfd + 1, &rfds, &wfds, NULL, timeout > 0 ? &tv : NULL);
}
while (true) {
// FD_ISSET(0, &rfds) will be true
int N = rxbuf.size();
ssize_t ret;
rxbuf.resize(N + 1500);
ret = recv(sockfd, (char*)&rxbuf[0] + N, 1500, 0);
if (false) { }
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
rxbuf.resize(N);
break;
}
else if (ret <= 0) {
rxbuf.resize(N);
closesocket(sockfd);
readyState = CLOSED;
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
break;
}
else {
rxbuf.resize(N + ret);
}
}
while (txbuf.size()) {
int ret = ::send(sockfd, (char*)&txbuf[0], txbuf.size(), 0);
if (false) { } // ??
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
break;
}
else if (ret <= 0) {
closesocket(sockfd);
readyState = CLOSED;
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
break;
}
else {
txbuf.erase(txbuf.begin(), txbuf.begin() + ret);
}
}
if (!txbuf.size() && readyState == CLOSING) {
closesocket(sockfd);
readyState = CLOSED;
}
}
virtual void _dispatch(Callback_Imp & callable) {
struct CallbackAdapter : public BytesCallback_Imp
// Adapt void(const std::string<uint8_t>&) to void(const std::string&)
{
Callback_Imp& callable;
CallbackAdapter(Callback_Imp& callable) : callable(callable) { }
void operator()(const std::vector<uint8_t>& message) {
std::string stringMessage(message.begin(), message.end());
callable(stringMessage);
}
};
CallbackAdapter bytesCallback(callable);
_dispatchBinary(bytesCallback);
}
virtual void _dispatchBinary(BytesCallback_Imp& callable) {
// TODO: consider acquiring a lock on rxbuf...
while (true) {
wsheader_type ws;
if (rxbuf.size() < 2) { return; /* Need at least 2 */ }
const uint8_t * data = (uint8_t *) &rxbuf[0]; // peek, but don't consume
ws.fin = (data[0] & 0x80) == 0x80;
ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f);
ws.mask = (data[1] & 0x80) == 0x80;
ws.N0 = (data[1] & 0x7f);
ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0);
if (rxbuf.size() < ws.header_size) { return; /* Need: ws.header_size - rxbuf.size() */ }
int i = 0;
if (ws.N0 < 126) {
ws.N = ws.N0;
i = 2;
}
else if (ws.N0 == 126) {
ws.N = 0;
ws.N |= ((uint64_t) data[2]) << 8;
ws.N |= ((uint64_t) data[3]) << 0;
i = 4;
}
else if (ws.N0 == 127) {
ws.N = 0;
ws.N |= ((uint64_t) data[2]) << 56;
ws.N |= ((uint64_t) data[3]) << 48;
ws.N |= ((uint64_t) data[4]) << 40;
ws.N |= ((uint64_t) data[5]) << 32;
ws.N |= ((uint64_t) data[6]) << 24;
ws.N |= ((uint64_t) data[7]) << 16;
ws.N |= ((uint64_t) data[8]) << 8;
ws.N |= ((uint64_t) data[9]) << 0;
i = 10;
}
if (ws.mask) {
ws.masking_key[0] = ((uint8_t) data[i+0]) << 0;
ws.masking_key[1] = ((uint8_t) data[i+1]) << 0;
ws.masking_key[2] = ((uint8_t) data[i+2]) << 0;
ws.masking_key[3] = ((uint8_t) data[i+3]) << 0;
}
else {
ws.masking_key[0] = 0;
ws.masking_key[1] = 0;
ws.masking_key[2] = 0;
ws.masking_key[3] = 0;
}
// Note: The checks above should hopefully ensure this addition
// cannot overflow:
if (rxbuf.size() < ws.header_size+ws.N) { return; /* Need: ws.header_size+ws.N - rxbuf.size() */ }
// We got a whole message, now do something with it:
if (false) { }
else if (
ws.opcode == wsheader_type::TEXT_FRAME
|| ws.opcode == wsheader_type::BINARY_FRAME
|| ws.opcode == wsheader_type::CONTINUATION
) {
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed
if (ws.fin) {
callable((const std::vector<uint8_t>) receivedData);
receivedData.erase(receivedData.begin(), receivedData.end());
std::vector<uint8_t> ().swap(receivedData);// free memory
}
}
else if (ws.opcode == wsheader_type::PING) {
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);
sendData(wsheader_type::PONG, data.size(), data.begin(), data.end());
}
else if (ws.opcode == wsheader_type::PONG) { }
else if (ws.opcode == wsheader_type::CLOSE) { close(); }
else { fprintf(stderr, "ERROR: Got unexpected WebSocket message.\n"); close(); }
rxbuf.erase(rxbuf.begin(), rxbuf.begin() + ws.header_size+(size_t)ws.N);
}
}
void sendPing() {
std::string empty;
sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end());
}
void send(const std::string& message) {
sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end());
}
void sendBinary(const std::string& message) {
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
}
void sendBinary(const std::vector<uint8_t>& message) {
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
}
template<class Iterator>
void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) {
// TODO:
// Masking key should (must) be derived from a high quality random
// number generator, to mitigate attacks on non-WebSocket friendly
// middleware:
const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 };
// TODO: consider acquiring a lock on txbuf...
if (readyState == CLOSING || readyState == CLOSED) { return; }
std::vector<uint8_t> header;
header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0);
header[0] = 0x80 | type;
if (false) { }
else if (message_size < 126) {
header[1] = (message_size & 0xff) | (useMask ? 0x80 : 0);
if (useMask) {
header[2] = masking_key[0];
header[3] = masking_key[1];
header[4] = masking_key[2];
header[5] = masking_key[3];
}
}
else if (message_size < 65536) {
header[1] = 126 | (useMask ? 0x80 : 0);
header[2] = (message_size >> 8) & 0xff;
header[3] = (message_size >> 0) & 0xff;
if (useMask) {
header[4] = masking_key[0];
header[5] = masking_key[1];
header[6] = masking_key[2];
header[7] = masking_key[3];
}
}
else { // TODO: run coverage testing here
header[1] = 127 | (useMask ? 0x80 : 0);
header[2] = (message_size >> 56) & 0xff;
header[3] = (message_size >> 48) & 0xff;
header[4] = (message_size >> 40) & 0xff;
header[5] = (message_size >> 32) & 0xff;
header[6] = (message_size >> 24) & 0xff;
header[7] = (message_size >> 16) & 0xff;
header[8] = (message_size >> 8) & 0xff;
header[9] = (message_size >> 0) & 0xff;
if (useMask) {
header[10] = masking_key[0];
header[11] = masking_key[1];
header[12] = masking_key[2];
header[13] = masking_key[3];
}
}
// N.B. - txbuf will keep growing until it can be transmitted over the socket:
txbuf.insert(txbuf.end(), header.begin(), header.end());
txbuf.insert(txbuf.end(), message_begin, message_end);
if (useMask) {
size_t message_offset = txbuf.size() - message_size;
for (size_t i = 0; i != message_size; ++i) {
txbuf[message_offset + i] ^= masking_key[i&0x3];
}
}
}
void close() {
if(readyState == CLOSING || readyState == CLOSED) { return; }
readyState = CLOSING;
uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key
std::vector<uint8_t> header(closeFrame, closeFrame+6);
txbuf.insert(txbuf.end(), header.begin(), header.end());
}
};
easywsclient::WebSocket::pointer from_url(const std::string& url, bool useMask, const std::string& origin) {
char host[512];
int port;
char path[512];
if (url.size() >= 512) {
fprintf(stderr, "ERROR: url size limit exceeded: %s\n", url.c_str());
return NULL;
}
if (origin.size() >= 200) {
fprintf(stderr, "ERROR: origin size limit exceeded: %s\n", origin.c_str());
return NULL;
}
if (false) { }
else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3) {
}
else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2) {
port = 80;
}
else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2) {
path[0] = '\0';
}
else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1) {
port = 80;
path[0] = '\0';
}
else {
fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str());
return NULL;
}
//fprintf(stderr, "easywsclient: connecting: host=%s port=%d path=/%s\n", host, port, path);
socket_t sockfd = hostname_connect(host, port);
if (sockfd == INVALID_SOCKET) {
fprintf(stderr, "Unable to connect to %s:%d\n", host, port);
return NULL;
}
{
// XXX: this should be done non-blocking,
char line[1024];
int status;
int i;
snprintf(line, 1024, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0);
if (port == 80) {
snprintf(line, 1024, "Host: %s\r\n", host); ::send(sockfd, line, strlen(line), 0);
}
else {
snprintf(line, 1024, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
}
snprintf(line, 1024, "Upgrade: websocket\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "Connection: Upgrade\r\n"); ::send(sockfd, line, strlen(line), 0);
if (!origin.empty()) {
snprintf(line, 1024, "Origin: %s\r\n", origin.c_str()); ::send(sockfd, line, strlen(line), 0);
}
snprintf(line, 1024, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "Sec-WebSocket-Version: 13\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "\r\n"); ::send(sockfd, line, strlen(line), 0);
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
line[i] = 0;
if (i == 1023) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; }
if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; }
// TODO: verify response headers,
while (true) {
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
if (line[0] == '\r' && line[1] == '\n') { break; }
}
}
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
#ifdef _WIN32
u_long on = 1;
ioctlsocket(sockfd, FIONBIO, &on);
#else
fcntl(sockfd, F_SETFL, O_NONBLOCK);
#endif
//fprintf(stderr, "Connected to: %s\n", url.c_str());
return easywsclient::WebSocket::pointer(new _RealWebSocket(sockfd, useMask));
}
} // end of module-only namespace
namespace easywsclient {
WebSocket::pointer WebSocket::create_dummy() {
static pointer dummy = pointer(new _DummyWebSocket);
return dummy;
}
WebSocket::pointer WebSocket::from_url(const std::string& url, const std::string& origin) {
return ::from_url(url, true, origin);
}
WebSocket::pointer WebSocket::from_url_no_mask(const std::string& url, const std::string& origin) {
return ::from_url(url, false, origin);
}
} // namespace easywsclient
+73
View File
@@ -0,0 +1,73 @@
#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
// This code comes from:
// https://github.com/dhbaird/easywsclient
//
// To get the latest version:
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
#include <string>
#include <vector>
#include <cstdint>
namespace easywsclient {
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
class WebSocket {
public:
typedef WebSocket * pointer;
typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues;
// Factories:
static pointer create_dummy();
static pointer from_url(const std::string& url, const std::string& origin = std::string());
static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string());
// Interfaces:
virtual ~WebSocket() { }
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
virtual void send(const std::string& message) = 0;
virtual void sendBinary(const std::string& message) = 0;
virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
virtual void sendPing() = 0;
virtual void close() = 0;
virtual readyStateValues getReadyState() const = 0;
template<class Callable>
void dispatch(Callable callable)
// For callbacks that accept a string argument.
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
struct _Callback : public Callback_Imp {
Callable& callable;
_Callback(Callable& callable) : callable(callable) { }
void operator()(const std::string& message) { callable(message); }
};
_Callback callback(callable);
_dispatch(callback);
}
template<class Callable>
void dispatchBinary(Callable callable)
// For callbacks that accept a std::vector<uint8_t> argument.
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
struct _Callback : public BytesCallback_Imp {
Callable& callable;
_Callback(Callable& callable) : callable(callable) { }
void operator()(const std::vector<uint8_t>& message) { callable(message); }
};
_Callback callback(callable);
_dispatchBinary(callback);
}
protected:
virtual void _dispatch(Callback_Imp& callable) = 0;
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
};
} // namespace easywsclient
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
+6
View File
@@ -21,5 +21,11 @@ set(IMGUI_SOURCES
misc/cpp/imgui_stdlib.cpp misc/cpp/imgui_stdlib.cpp
) )
# spice2x: DX11 backend is only built for non-XP toolchains
if(NOT SPICE_XP)
list(APPEND IMGUI_HEADERS backends/imgui_impl_dx11.h)
list(APPEND IMGUI_SOURCES backends/imgui_impl_dx11.cpp)
endif()
add_library(imgui STATIC ${IMGUI_HEADERS} ${IMGUI_SOURCES}) add_library(imgui STATIC ${IMGUI_HEADERS} ${IMGUI_SOURCES})
target_include_directories(imgui PRIVATE ${PROJECT_SOURCE_DIR}) target_include_directories(imgui PRIVATE ${PROJECT_SOURCE_DIR})
+894
View File
@@ -0,0 +1,894 @@
// dear imgui: Renderer Backend for DirectX11
// This needs to be used along with a Platform Backend (e.g. Win32)
// Implemented features:
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'.
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
// Learn about Dear ImGui:
// - FAQ https://dearimgui.com/faq
// - Getting Started https://dearimgui.com/getting-started
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2026-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface.
// 2026-04-23: DirectX11: Added support for standard draw callbacks (in platform_io): DrawCallback_ResetRenderState, DrawCallback_SetSamplerLinear, DrawCallback_SetSamplerNearest. Obsoleting samplers from ImGui_ImplDX11_RenderState. (#9378)
// 2026-01-19: DirectX11: Added 'SamplerNearest' in ImGui_ImplDX11_RenderState. Renamed 'SamplerDefault' to 'SamplerLinear'.
// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown.
// 2025-06-11: DirectX11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas.
// 2025-05-07: DirectX11: Honor draw_data->FramebufferScale to allow for custom backends and experiment using it (consistently with other renderer backends, even though in normal condition it is not set under Windows).
// 2025-02-24: [Docking] Added undocumented ImGui_ImplDX11_SetSwapChainDescs() to configure swap chain creation for secondary viewports.
// 2025-01-06: DirectX11: Expose VertexConstantBuffer in ImGui_ImplDX11_RenderState. Reset projection matrix in ImDrawCallback_ResetRenderState handler.
// 2024-10-07: DirectX11: Changed default texture sampler to Clamp instead of Repeat/Wrap.
// 2024-10-07: DirectX11: Expose selected render state in ImGui_ImplDX11_RenderState, which you can access in 'void* platform_io.Renderer_RenderState' during draw callbacks.
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
// 2021-05-19: DirectX11: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
// 2021-02-18: DirectX11: Change blending equation to preserve alpha in output buffer.
// 2019-08-01: DirectX11: Fixed code querying the Geometry Shader state (would generally error with Debug layer enabled).
// 2019-07-21: DirectX11: Backup, clear and restore Geometry Shader is any is bound when calling ImGui_ImplDX11_RenderDrawData. Clearing Hull/Domain/Compute shaders without backup/restore.
// 2019-05-29: DirectX11: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
// 2019-04-30: DirectX11: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
// 2018-12-03: Misc: Added #pragma comment statement to automatically link with d3dcompiler.lib when using D3DCompile().
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
// 2018-08-01: DirectX11: Querying for IDXGIFactory instead of IDXGIFactory1 to increase compatibility.
// 2018-07-13: DirectX11: Fixed unreleased resources in Init and Shutdown functions.
// 2018-06-08: Misc: Extracted imgui_impl_dx11.cpp/.h away from the old combined DX11+Win32 example.
// 2018-06-08: DirectX11: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle.
// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplDX11_RenderDrawData() in the .h file so you can call it yourself.
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
// 2016-05-07: DirectX11: Disabling depth-write.
#include "imgui.h"
#ifndef IMGUI_DISABLE
#include "imgui_impl_dx11.h"
// DirectX
#include <stdio.h>
#include <d3d11.h>
#include <d3dcompiler.h>
// #ifdef _MSC_VER
// #pragma comment(lib, "d3dcompiler") // Automatically link with d3dcompiler.lib as we are using D3DCompile() below.
// #endif
// spice2x: resolve D3DCompile at runtime rather than static-linking d3dcompiler,
// which would hardwire an import on d3dcompiler_47.dll (absent on stock Win7).
static pD3DCompile ImGui_ImplDX11_GetD3DCompile()
{
static pD3DCompile fn = []() -> pD3DCompile {
static const char *dlls[] = {
"d3dcompiler_47.dll",
"d3dcompiler_46.dll",
"d3dcompiler_43.dll" };
for (const char *dll : dlls) {
HMODULE mod = GetModuleHandleA(dll);
if (!mod) {
mod = LoadLibraryA(dll);
}
if (mod) {
if (auto p = (pD3DCompile) GetProcAddress(mod, "D3DCompile")) {
return p;
}
}
}
return nullptr;
}();
return fn;
}
// Clang/GCC warnings with -Weverything
#if defined(__clang__)
#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse.
#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
#endif
// DirectX11 data
struct ImGui_ImplDX11_Texture
{
ID3D11Texture2D* pTexture;
ID3D11ShaderResourceView* pTextureView;
};
struct ImGui_ImplDX11_Data
{
ID3D11Device* pd3dDevice;
ID3D11DeviceContext* pd3dDeviceContext;
IDXGIFactory* pFactory;
ID3D11Buffer* pVB;
ID3D11Buffer* pIB;
ID3D11VertexShader* pVertexShader;
ID3D11InputLayout* pInputLayout;
ID3D11Buffer* pVertexConstantBuffer;
ID3D11PixelShader* pPixelShader;
ID3D11SamplerState* pTexSamplerLinear;
ID3D11SamplerState* pTexSamplerNearest;
ID3D11RasterizerState* pRasterizerState;
ID3D11BlendState* pBlendState;
ID3D11DepthStencilState* pDepthStencilState;
int VertexBufferSize;
int IndexBufferSize;
ImGui_ImplDX11_RenderState* RenderState; // == (ImGui_ImplDX11_RenderState*)ImGui::GetPlatformIO().Renderer_RenderState during rendering.
ImVector<DXGI_SWAP_CHAIN_DESC> SwapChainDescsForViewports;
ImGui_ImplDX11_Data() { memset((void*)this, 0, sizeof(*this)); VertexBufferSize = 5000; IndexBufferSize = 10000; }
};
struct VERTEX_CONSTANT_BUFFER_DX11
{
float mvp[4][4];
};
// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
static ImGui_ImplDX11_Data* ImGui_ImplDX11_GetBackendData()
{
return ImGui::GetCurrentContext() ? (ImGui_ImplDX11_Data*)ImGui::GetIO().BackendRendererUserData : nullptr;
}
// Forward Declarations
static void ImGui_ImplDX11_InitMultiViewportSupport();
static void ImGui_ImplDX11_ShutdownMultiViewportSupport();
// Functions
static void ImGui_ImplDX11_SetupRenderState(const ImDrawData* draw_data, ID3D11DeviceContext* device_ctx)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
// Setup viewport
D3D11_VIEWPORT vp = {};
vp.Width = draw_data->DisplaySize.x * draw_data->FramebufferScale.x;
vp.Height = draw_data->DisplaySize.y * draw_data->FramebufferScale.y;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
vp.TopLeftX = vp.TopLeftY = 0;
device_ctx->RSSetViewports(1, &vp);
// Setup orthographic projection matrix into our constant buffer
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
D3D11_MAPPED_SUBRESOURCE mapped_resource;
if (device_ctx->Map(bd->pVertexConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource) == S_OK)
{
VERTEX_CONSTANT_BUFFER_DX11* constant_buffer = (VERTEX_CONSTANT_BUFFER_DX11*)mapped_resource.pData;
float L = draw_data->DisplayPos.x;
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
float T = draw_data->DisplayPos.y;
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
float mvp[4][4] =
{
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
{ 0.0f, 0.0f, 0.5f, 0.0f },
{ (R+L)/(L-R), (T+B)/(B-T), 0.5f, 1.0f },
};
memcpy(&constant_buffer->mvp, mvp, sizeof(mvp));
device_ctx->Unmap(bd->pVertexConstantBuffer, 0);
}
// Setup shader and vertex buffers
unsigned int stride = sizeof(ImDrawVert);
unsigned int offset = 0;
device_ctx->IASetInputLayout(bd->pInputLayout);
device_ctx->IASetVertexBuffers(0, 1, &bd->pVB, &stride, &offset);
device_ctx->IASetIndexBuffer(bd->pIB, sizeof(ImDrawIdx) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, 0);
device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
device_ctx->VSSetShader(bd->pVertexShader, nullptr, 0);
device_ctx->VSSetConstantBuffers(0, 1, &bd->pVertexConstantBuffer);
device_ctx->PSSetShader(bd->pPixelShader, nullptr, 0);
device_ctx->PSSetSamplers(0, 1, &bd->pTexSamplerLinear);
device_ctx->GSSetShader(nullptr, nullptr, 0);
device_ctx->HSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
device_ctx->DSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
device_ctx->CSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
// Setup render state
const float blend_factor[4] = { 0.f, 0.f, 0.f, 0.f };
device_ctx->OMSetBlendState(bd->pBlendState, blend_factor, 0xffffffff);
device_ctx->OMSetDepthStencilState(bd->pDepthStencilState, 0);
device_ctx->RSSetState(bd->pRasterizerState);
}
// Draw callbacks
static void ImGui_ImplDX11_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way.
static void ImGui_ImplDX11_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); bd->RenderState->DeviceContext->PSSetSamplers(0, 1, &bd->pTexSamplerLinear); }
static void ImGui_ImplDX11_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); bd->RenderState->DeviceContext->PSSetSamplers(0, 1, &bd->pTexSamplerNearest); }
// Render function
void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data)
{
// Avoid rendering when minimized
if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f)
return;
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
ID3D11DeviceContext* device = bd->pd3dDeviceContext;
// Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do.
// (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates).
if (draw_data->Textures != nullptr)
for (ImTextureData* tex : *draw_data->Textures)
if (tex->Status != ImTextureStatus_OK)
ImGui_ImplDX11_UpdateTexture(tex);
// Create and grow vertex/index buffers if needed
if (!bd->pVB || bd->VertexBufferSize < draw_data->TotalVtxCount)
{
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
bd->VertexBufferSize = draw_data->TotalVtxCount + 5000;
D3D11_BUFFER_DESC desc = {};
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.ByteWidth = (UINT)bd->VertexBufferSize * sizeof(ImDrawVert);
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
desc.MiscFlags = 0;
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVB) < 0)
return;
}
if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount)
{
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
bd->IndexBufferSize = draw_data->TotalIdxCount + 10000;
D3D11_BUFFER_DESC desc = {};
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.ByteWidth = (UINT)bd->IndexBufferSize * sizeof(ImDrawIdx);
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pIB) < 0)
return;
}
// Upload vertex/index data into a single contiguous GPU buffer
D3D11_MAPPED_SUBRESOURCE vtx_resource, idx_resource;
if (device->Map(bd->pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &vtx_resource) != S_OK)
return;
if (device->Map(bd->pIB, 0, D3D11_MAP_WRITE_DISCARD, 0, &idx_resource) != S_OK)
return;
ImDrawVert* vtx_dst = (ImDrawVert*)vtx_resource.pData;
ImDrawIdx* idx_dst = (ImDrawIdx*)idx_resource.pData;
for (const ImDrawList* draw_list : draw_data->CmdLists)
{
memcpy(vtx_dst, draw_list->VtxBuffer.Data, draw_list->VtxBuffer.Size * sizeof(ImDrawVert));
memcpy(idx_dst, draw_list->IdxBuffer.Data, draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
vtx_dst += draw_list->VtxBuffer.Size;
idx_dst += draw_list->IdxBuffer.Size;
}
device->Unmap(bd->pVB, 0);
device->Unmap(bd->pIB, 0);
// Backup DX state that will be modified to restore it afterwards (unfortunately this is very ugly looking and verbose. Close your eyes!)
struct BACKUP_DX11_STATE
{
UINT ScissorRectsCount, ViewportsCount;
D3D11_RECT ScissorRects[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
D3D11_VIEWPORT Viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
ID3D11RasterizerState* RS;
ID3D11BlendState* BlendState;
FLOAT BlendFactor[4];
UINT SampleMask;
UINT StencilRef;
ID3D11DepthStencilState* DepthStencilState;
ID3D11ShaderResourceView* PSShaderResource;
ID3D11SamplerState* PSSampler;
ID3D11PixelShader* PS;
ID3D11VertexShader* VS;
ID3D11GeometryShader* GS;
UINT PSInstancesCount, VSInstancesCount, GSInstancesCount;
ID3D11ClassInstance *PSInstances[256], *VSInstances[256], *GSInstances[256]; // 256 is max according to PSSetShader documentation
D3D11_PRIMITIVE_TOPOLOGY PrimitiveTopology;
ID3D11Buffer* IndexBuffer, *VertexBuffer, *VSConstantBuffer;
UINT IndexBufferOffset, VertexBufferStride, VertexBufferOffset;
DXGI_FORMAT IndexBufferFormat;
ID3D11InputLayout* InputLayout;
};
BACKUP_DX11_STATE old = {};
old.ScissorRectsCount = old.ViewportsCount = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE;
device->RSGetScissorRects(&old.ScissorRectsCount, old.ScissorRects);
device->RSGetViewports(&old.ViewportsCount, old.Viewports);
device->RSGetState(&old.RS);
device->OMGetBlendState(&old.BlendState, old.BlendFactor, &old.SampleMask);
device->OMGetDepthStencilState(&old.DepthStencilState, &old.StencilRef);
device->PSGetShaderResources(0, 1, &old.PSShaderResource);
device->PSGetSamplers(0, 1, &old.PSSampler);
old.PSInstancesCount = old.VSInstancesCount = old.GSInstancesCount = 256;
device->PSGetShader(&old.PS, old.PSInstances, &old.PSInstancesCount);
device->VSGetShader(&old.VS, old.VSInstances, &old.VSInstancesCount);
device->VSGetConstantBuffers(0, 1, &old.VSConstantBuffer);
device->GSGetShader(&old.GS, old.GSInstances, &old.GSInstancesCount);
device->IAGetPrimitiveTopology(&old.PrimitiveTopology);
device->IAGetIndexBuffer(&old.IndexBuffer, &old.IndexBufferFormat, &old.IndexBufferOffset);
device->IAGetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset);
device->IAGetInputLayout(&old.InputLayout);
// Setup desired DX state
ImGui_ImplDX11_SetupRenderState(draw_data, device);
// Setup render state structure (for callbacks and custom texture bindings)
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImGui_ImplDX11_RenderState render_state;
render_state.Device = bd->pd3dDevice;
render_state.DeviceContext = bd->pd3dDeviceContext;
render_state.VertexConstantBuffer = bd->pVertexConstantBuffer;
platform_io.Renderer_RenderState = bd->RenderState = &render_state;
// Render command lists
// (Because we merged all buffers into a single one, we maintain our own offset into them)
int global_idx_offset = 0;
int global_vtx_offset = 0;
ImVec2 clip_off = draw_data->DisplayPos;
ImVec2 clip_scale = draw_data->FramebufferScale;
for (const ImDrawList* draw_list : draw_data->CmdLists)
{
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
{
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
if (pcmd->UserCallback != nullptr)
{
// User callback, registered via ImDrawList::AddCallback()
if (pcmd->UserCallback == ImGui_ImplDX11_DrawCallback_ResetRenderState)
ImGui_ImplDX11_SetupRenderState(draw_data, device);
else
pcmd->UserCallback(draw_list, pcmd);
}
else
{
// Project scissor/clipping rectangles into framebuffer space
ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y);
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
continue;
// Apply scissor/clipping rectangle
const D3D11_RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y };
device->RSSetScissorRects(1, &r);
// Bind texture, Draw
ID3D11ShaderResourceView* texture_srv = (ID3D11ShaderResourceView*)pcmd->GetTexID();
device->PSSetShaderResources(0, 1, &texture_srv);
device->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset);
}
}
global_idx_offset += draw_list->IdxBuffer.Size;
global_vtx_offset += draw_list->VtxBuffer.Size;
}
platform_io.Renderer_RenderState = bd->RenderState = nullptr;
// Restore modified DX state
device->RSSetScissorRects(old.ScissorRectsCount, old.ScissorRects);
device->RSSetViewports(old.ViewportsCount, old.Viewports);
device->RSSetState(old.RS); if (old.RS) old.RS->Release();
device->OMSetBlendState(old.BlendState, old.BlendFactor, old.SampleMask); if (old.BlendState) old.BlendState->Release();
device->OMSetDepthStencilState(old.DepthStencilState, old.StencilRef); if (old.DepthStencilState) old.DepthStencilState->Release();
device->PSSetShaderResources(0, 1, &old.PSShaderResource); if (old.PSShaderResource) old.PSShaderResource->Release();
device->PSSetSamplers(0, 1, &old.PSSampler); if (old.PSSampler) old.PSSampler->Release();
device->PSSetShader(old.PS, old.PSInstances, old.PSInstancesCount); if (old.PS) old.PS->Release();
for (UINT i = 0; i < old.PSInstancesCount; i++) if (old.PSInstances[i]) old.PSInstances[i]->Release();
device->VSSetShader(old.VS, old.VSInstances, old.VSInstancesCount); if (old.VS) old.VS->Release();
device->VSSetConstantBuffers(0, 1, &old.VSConstantBuffer); if (old.VSConstantBuffer) old.VSConstantBuffer->Release();
device->GSSetShader(old.GS, old.GSInstances, old.GSInstancesCount); if (old.GS) old.GS->Release();
for (UINT i = 0; i < old.VSInstancesCount; i++) if (old.VSInstances[i]) old.VSInstances[i]->Release();
device->IASetPrimitiveTopology(old.PrimitiveTopology);
device->IASetIndexBuffer(old.IndexBuffer, old.IndexBufferFormat, old.IndexBufferOffset); if (old.IndexBuffer) old.IndexBuffer->Release();
device->IASetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset); if (old.VertexBuffer) old.VertexBuffer->Release();
device->IASetInputLayout(old.InputLayout); if (old.InputLayout) old.InputLayout->Release();
}
static void ImGui_ImplDX11_DestroyTexture(ImTextureData* tex)
{
if (ImGui_ImplDX11_Texture* backend_tex = (ImGui_ImplDX11_Texture*)tex->BackendUserData)
{
IM_ASSERT(backend_tex->pTextureView == (ID3D11ShaderResourceView*)(intptr_t)tex->TexID);
backend_tex->pTextureView->Release();
backend_tex->pTexture->Release();
IM_DELETE(backend_tex);
// Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running)
tex->SetTexID(ImTextureID_Invalid);
tex->BackendUserData = nullptr;
}
tex->SetStatus(ImTextureStatus_Destroyed);
}
void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
if (tex->Status == ImTextureStatus_WantCreate)
{
// Create and upload new texture to graphics system
//IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height);
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr);
IM_ASSERT(tex->Format == ImTextureFormat_RGBA32);
unsigned int* pixels = (unsigned int*)tex->GetPixels();
ImGui_ImplDX11_Texture* backend_tex = IM_NEW(ImGui_ImplDX11_Texture)();
// Create texture
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Width = (UINT)tex->Width;
desc.Height = (UINT)tex->Height;
desc.MipLevels = 1;
desc.ArraySize = 1;
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
#else
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
#endif
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA subResource;
subResource.pSysMem = pixels;
subResource.SysMemPitch = desc.Width * 4;
subResource.SysMemSlicePitch = 0;
bd->pd3dDevice->CreateTexture2D(&desc, &subResource, &backend_tex->pTexture);
IM_ASSERT(backend_tex->pTexture != nullptr && "Backend failed to create texture!");
// Create texture view
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(srvDesc));
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
srvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
#else
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
#endif
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = desc.MipLevels;
srvDesc.Texture2D.MostDetailedMip = 0;
bd->pd3dDevice->CreateShaderResourceView(backend_tex->pTexture, &srvDesc, &backend_tex->pTextureView);
IM_ASSERT(backend_tex->pTextureView != nullptr && "Backend failed to create texture!");
// Store identifiers
tex->SetTexID((ImTextureID)(intptr_t)backend_tex->pTextureView);
tex->SetStatus(ImTextureStatus_OK);
tex->BackendUserData = backend_tex;
}
else if (tex->Status == ImTextureStatus_WantUpdates)
{
// Update selected blocks. We only ever write to textures regions which have never been used before!
// This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region.
ImGui_ImplDX11_Texture* backend_tex = (ImGui_ImplDX11_Texture*)tex->BackendUserData;
IM_ASSERT(backend_tex->pTextureView == (ID3D11ShaderResourceView*)(intptr_t)tex->TexID);
for (ImTextureRect& r : tex->Updates)
{
D3D11_BOX box = { (UINT)r.x, (UINT)r.y, (UINT)0, (UINT)(r.x + r.w), (UINT)(r.y + r .h), (UINT)1 };
bd->pd3dDeviceContext->UpdateSubresource(backend_tex->pTexture, 0, &box, tex->GetPixelsAt(r.x, r.y), (UINT)tex->GetPitch(), 0);
}
tex->SetStatus(ImTextureStatus_OK);
}
if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0)
ImGui_ImplDX11_DestroyTexture(tex);
}
bool ImGui_ImplDX11_CreateDeviceObjects()
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
if (!bd->pd3dDevice)
return false;
ImGui_ImplDX11_InvalidateDeviceObjects();
// By using D3DCompile() from <d3dcompiler.h> / d3dcompiler.lib, we introduce a dependency to a given version of d3dcompiler_XX.dll (see D3DCOMPILER_DLL_A)
// If you would like to use this DX11 sample code but remove this dependency you can:
// 1) compile once, save the compiled shader blobs into a file or source code and pass them to CreateVertexShader()/CreatePixelShader() [preferred solution]
// 2) use code to detect any version of the DLL and grab a pointer to D3DCompile from the DLL.
// See https://github.com/ocornut/imgui/pull/638 for sources and details.
// spice2x: dynamically resolved; see ImGui_ImplDX11_GetD3DCompile.
pD3DCompile D3DCompile_fn = ImGui_ImplDX11_GetD3DCompile();
if (!D3DCompile_fn) {
return false;
}
// Create the vertex shader
{
static const char* vertexShader =
"cbuffer vertexBuffer : register(b0) \
{\
float4x4 ProjectionMatrix; \
};\
struct VS_INPUT\
{\
float2 pos : POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
struct PS_INPUT\
{\
float4 pos : SV_POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
PS_INPUT main(VS_INPUT input)\
{\
PS_INPUT output;\
output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\
output.col = input.col;\
output.uv = input.uv;\
return output;\
}";
ID3DBlob* vertexShaderBlob;
if (FAILED(D3DCompile_fn(vertexShader, strlen(vertexShader), nullptr, nullptr, nullptr, "main", "vs_4_0", 0, 0, &vertexShaderBlob, nullptr)))
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
if (bd->pd3dDevice->CreateVertexShader(vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), nullptr, &bd->pVertexShader) != S_OK)
{
vertexShaderBlob->Release();
return false;
}
// Create the input layout
D3D11_INPUT_ELEMENT_DESC local_layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, pos), D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, uv), D3D11_INPUT_PER_VERTEX_DATA, 0 },
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
{ "COLOR", 0, DXGI_FORMAT_B8G8R8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
#else
{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
#endif
};
if (bd->pd3dDevice->CreateInputLayout(local_layout, 3, vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), &bd->pInputLayout) != S_OK)
{
vertexShaderBlob->Release();
return false;
}
vertexShaderBlob->Release();
// Create the constant buffer
{
D3D11_BUFFER_DESC desc = {};
desc.ByteWidth = sizeof(VERTEX_CONSTANT_BUFFER_DX11);
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
desc.MiscFlags = 0;
bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVertexConstantBuffer);
}
}
// Create the pixel shader
{
static const char* pixelShader =
"struct PS_INPUT\
{\
float4 pos : SV_POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
sampler sampler0;\
Texture2D texture0;\
\
float4 main(PS_INPUT input) : SV_Target\
{\
float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \
return out_col; \
}";
ID3DBlob* pixelShaderBlob;
if (FAILED(D3DCompile_fn(pixelShader, strlen(pixelShader), nullptr, nullptr, nullptr, "main", "ps_4_0", 0, 0, &pixelShaderBlob, nullptr)))
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
if (bd->pd3dDevice->CreatePixelShader(pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize(), nullptr, &bd->pPixelShader) != S_OK)
{
pixelShaderBlob->Release();
return false;
}
pixelShaderBlob->Release();
}
// Create the blending setup
{
D3D11_BLEND_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.AlphaToCoverageEnable = false;
desc.RenderTarget[0].BlendEnable = true;
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
bd->pd3dDevice->CreateBlendState(&desc, &bd->pBlendState);
}
// Create the rasterizer state
{
D3D11_RASTERIZER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.FillMode = D3D11_FILL_SOLID;
desc.CullMode = D3D11_CULL_NONE;
desc.ScissorEnable = true;
desc.DepthClipEnable = true;
bd->pd3dDevice->CreateRasterizerState(&desc, &bd->pRasterizerState);
}
// Create depth-stencil State
{
D3D11_DEPTH_STENCIL_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.DepthEnable = false;
desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
desc.DepthFunc = D3D11_COMPARISON_ALWAYS;
desc.StencilEnable = false;
desc.FrontFace.StencilFailOp = desc.FrontFace.StencilDepthFailOp = desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
desc.BackFace = desc.FrontFace;
bd->pd3dDevice->CreateDepthStencilState(&desc, &bd->pDepthStencilState);
}
// Create texture sampler
// (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling)
{
D3D11_SAMPLER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
desc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
desc.MipLODBias = 0.f;
desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
desc.MinLOD = 0.f;
desc.MaxLOD = 0.f;
bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTexSamplerLinear);
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTexSamplerNearest);
}
return true;
}
void ImGui_ImplDX11_InvalidateDeviceObjects()
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
if (!bd->pd3dDevice)
return;
// Destroy all textures
for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
if (tex->RefCount == 1)
ImGui_ImplDX11_DestroyTexture(tex);
if (bd->pTexSamplerLinear) { bd->pTexSamplerLinear->Release(); bd->pTexSamplerLinear = nullptr; }
if (bd->pTexSamplerNearest) { bd->pTexSamplerNearest->Release(); bd->pTexSamplerNearest = nullptr; }
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
if (bd->pBlendState) { bd->pBlendState->Release(); bd->pBlendState = nullptr; }
if (bd->pDepthStencilState) { bd->pDepthStencilState->Release(); bd->pDepthStencilState = nullptr; }
if (bd->pRasterizerState) { bd->pRasterizerState->Release(); bd->pRasterizerState = nullptr; }
if (bd->pPixelShader) { bd->pPixelShader->Release(); bd->pPixelShader = nullptr; }
if (bd->pVertexConstantBuffer) { bd->pVertexConstantBuffer->Release(); bd->pVertexConstantBuffer = nullptr; }
if (bd->pInputLayout) { bd->pInputLayout->Release(); bd->pInputLayout = nullptr; }
if (bd->pVertexShader) { bd->pVertexShader->Release(); bd->pVertexShader = nullptr; }
}
bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context)
{
ImGuiIO& io = ImGui::GetIO();
IMGUI_CHECKVERSION();
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
// Setup backend capabilities flags
ImGui_ImplDX11_Data* bd = IM_NEW(ImGui_ImplDX11_Data)();
io.BackendRendererUserData = (void*)bd;
io.BackendRendererName = "imgui_impl_dx11";
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional)
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.Renderer_TextureMaxWidth = platform_io.Renderer_TextureMaxHeight = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION;
platform_io.DrawCallback_ResetRenderState = ImGui_ImplDX11_DrawCallback_ResetRenderState;
platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplDX11_DrawCallback_SetSamplerLinear;
platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplDX11_DrawCallback_SetSamplerNearest;
// Get factory from device
IDXGIDevice* pDXGIDevice = nullptr;
IDXGIAdapter* pDXGIAdapter = nullptr;
IDXGIFactory* pFactory = nullptr;
if (device->QueryInterface(IID_PPV_ARGS(&pDXGIDevice)) == S_OK)
if (pDXGIDevice->GetParent(IID_PPV_ARGS(&pDXGIAdapter)) == S_OK)
if (pDXGIAdapter->GetParent(IID_PPV_ARGS(&pFactory)) == S_OK)
{
bd->pd3dDevice = device;
bd->pd3dDeviceContext = device_context;
bd->pFactory = pFactory;
}
if (pDXGIDevice) pDXGIDevice->Release();
if (pDXGIAdapter) pDXGIAdapter->Release();
bd->pd3dDevice->AddRef();
bd->pd3dDeviceContext->AddRef();
ImGui_ImplDX11_InitMultiViewportSupport();
return true;
}
void ImGui_ImplDX11_Shutdown()
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
ImGuiIO& io = ImGui::GetIO();
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImGui_ImplDX11_ShutdownMultiViewportSupport();
ImGui_ImplDX11_InvalidateDeviceObjects();
if (bd->pFactory) { bd->pFactory->Release(); }
if (bd->pd3dDevice) { bd->pd3dDevice->Release(); }
if (bd->pd3dDeviceContext) { bd->pd3dDeviceContext->Release(); }
io.BackendRendererName = nullptr;
io.BackendRendererUserData = nullptr;
io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures | ImGuiBackendFlags_RendererHasViewports);
platform_io.ClearRendererHandlers();
IM_DELETE(bd);
}
void ImGui_ImplDX11_NewFrame()
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX11_Init()?");
if (!bd->pVertexShader)
if (!ImGui_ImplDX11_CreateDeviceObjects())
IM_ASSERT(0 && "ImGui_ImplDX11_CreateDeviceObjects() failed!");
}
//--------------------------------------------------------------------------------------------------------
// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT
// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously.
// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first..
//--------------------------------------------------------------------------------------------------------
// Helper structure we store in the void* RendererUserData field of each ImGuiViewport to easily retrieve our backend data.
struct ImGui_ImplDX11_ViewportData
{
IDXGISwapChain* SwapChain;
ID3D11RenderTargetView* RTView;
ImGui_ImplDX11_ViewportData() { SwapChain = nullptr; RTView = nullptr; }
~ImGui_ImplDX11_ViewportData() { IM_ASSERT(SwapChain == nullptr && RTView == nullptr); }
};
// Multi-Viewports: configure templates used when creating swapchains for secondary viewports. Will try them in order.
// This is intentionally not declared in the .h file yet, so you will need to copy this declaration:
void ImGui_ImplDX11_SetSwapChainDescs(const DXGI_SWAP_CHAIN_DESC* desc_templates, int desc_templates_count);
void ImGui_ImplDX11_SetSwapChainDescs(const DXGI_SWAP_CHAIN_DESC* desc_templates, int desc_templates_count)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
bd->SwapChainDescsForViewports.resize(desc_templates_count);
memcpy(bd->SwapChainDescsForViewports.Data, desc_templates, sizeof(DXGI_SWAP_CHAIN_DESC));
}
static void ImGui_ImplDX11_CreateWindow(ImGuiViewport* viewport)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
ImGui_ImplDX11_ViewportData* vd = IM_NEW(ImGui_ImplDX11_ViewportData)();
viewport->RendererUserData = vd;
// PlatformHandleRaw should always be a HWND, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL's WindowID).
// Some backends will leave PlatformHandleRaw == 0, in which case we assume PlatformHandle will contain the HWND.
HWND hwnd = viewport->PlatformHandleRaw ? (HWND)viewport->PlatformHandleRaw : (HWND)viewport->PlatformHandle;
IM_ASSERT(hwnd != 0);
IM_ASSERT(vd->SwapChain == nullptr && vd->RTView == nullptr);
// Create swap chain
HRESULT hr = DXGI_ERROR_UNSUPPORTED;
for (const DXGI_SWAP_CHAIN_DESC& sd_template : bd->SwapChainDescsForViewports)
{
IM_ASSERT(sd_template.BufferDesc.Width == 0 && sd_template.BufferDesc.Height == 0 && sd_template.OutputWindow == nullptr);
DXGI_SWAP_CHAIN_DESC sd = sd_template;
sd.BufferDesc.Width = (UINT)viewport->Size.x;
sd.BufferDesc.Height = (UINT)viewport->Size.y;
sd.OutputWindow = hwnd;
hr = bd->pFactory->CreateSwapChain(bd->pd3dDevice, &sd, &vd->SwapChain);
if (SUCCEEDED(hr))
break;
}
IM_ASSERT(SUCCEEDED(hr));
bd->pFactory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES); // Disable e.g. Alt+Enter
// Create the render target
if (vd->SwapChain != nullptr)
{
ID3D11Texture2D* pBackBuffer;
vd->SwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
bd->pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &vd->RTView);
pBackBuffer->Release();
}
}
static void ImGui_ImplDX11_DestroyWindow(ImGuiViewport* viewport)
{
// The main viewport (owned by the application) will always have RendererUserData == nullptr since we didn't create the data for it.
if (ImGui_ImplDX11_ViewportData* vd = (ImGui_ImplDX11_ViewportData*)viewport->RendererUserData)
{
if (vd->SwapChain)
vd->SwapChain->Release();
vd->SwapChain = nullptr;
if (vd->RTView)
vd->RTView->Release();
vd->RTView = nullptr;
IM_DELETE(vd);
}
viewport->RendererUserData = nullptr;
}
static void ImGui_ImplDX11_SetWindowSize(ImGuiViewport* viewport, ImVec2 size)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
ImGui_ImplDX11_ViewportData* vd = (ImGui_ImplDX11_ViewportData*)viewport->RendererUserData;
if (vd->RTView)
{
vd->RTView->Release();
vd->RTView = nullptr;
}
if (vd->SwapChain)
{
ID3D11Texture2D* pBackBuffer = nullptr;
vd->SwapChain->ResizeBuffers(0, (UINT)size.x, (UINT)size.y, DXGI_FORMAT_UNKNOWN, 0);
vd->SwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
if (pBackBuffer == nullptr) { fprintf(stderr, "ImGui_ImplDX11_SetWindowSize() failed creating buffers.\n"); return; }
bd->pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &vd->RTView);
pBackBuffer->Release();
}
}
static void ImGui_ImplDX11_RenderWindow(ImGuiViewport* viewport, void*)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
ImGui_ImplDX11_ViewportData* vd = (ImGui_ImplDX11_ViewportData*)viewport->RendererUserData;
ImVec4 clear_color = ImVec4(0.0f, 0.0f, 0.0f, 1.0f);
bd->pd3dDeviceContext->OMSetRenderTargets(1, &vd->RTView, nullptr);
if (!(viewport->Flags & ImGuiViewportFlags_NoRendererClear))
bd->pd3dDeviceContext->ClearRenderTargetView(vd->RTView, (float*)&clear_color);
ImGui_ImplDX11_RenderDrawData(viewport->DrawData);
}
static void ImGui_ImplDX11_SwapBuffers(ImGuiViewport* viewport, void*)
{
ImGui_ImplDX11_ViewportData* vd = (ImGui_ImplDX11_ViewportData*)viewport->RendererUserData;
if (vd->SwapChain)
vd->SwapChain->Present(0, 0); // Present without vsync
}
static void ImGui_ImplDX11_InitMultiViewportSupport()
{
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.Renderer_CreateWindow = ImGui_ImplDX11_CreateWindow;
platform_io.Renderer_DestroyWindow = ImGui_ImplDX11_DestroyWindow;
platform_io.Renderer_SetWindowSize = ImGui_ImplDX11_SetWindowSize;
platform_io.Renderer_RenderWindow = ImGui_ImplDX11_RenderWindow;
platform_io.Renderer_SwapBuffers = ImGui_ImplDX11_SwapBuffers;
// Default swapchain format
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;
ImGui_ImplDX11_SetSwapChainDescs(&sd, 1);
}
static void ImGui_ImplDX11_ShutdownMultiViewportSupport()
{
ImGui::DestroyPlatformWindows();
}
//-----------------------------------------------------------------------------
#endif // #ifndef IMGUI_DISABLE
+53
View File
@@ -0,0 +1,53 @@
// dear imgui: Renderer Backend for DirectX11
// This needs to be used along with a Platform Backend (e.g. Win32)
// Implemented features:
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'.
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
// Learn about Dear ImGui:
// - FAQ https://dearimgui.com/faq
// - Getting Started https://dearimgui.com/getting-started
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp
#pragma once
#include "imgui.h" // IMGUI_IMPL_API
#ifndef IMGUI_DISABLE
struct ID3D11Device;
struct ID3D11DeviceContext;
struct ID3D11SamplerState;
struct ID3D11Buffer;
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context);
IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown();
IMGUI_IMPL_API void ImGui_ImplDX11_NewFrame();
IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data);
// Use if you want to reset your rendering device without losing Dear ImGui state.
IMGUI_IMPL_API bool ImGui_ImplDX11_CreateDeviceObjects();
IMGUI_IMPL_API void ImGui_ImplDX11_InvalidateDeviceObjects();
// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually.
IMGUI_IMPL_API void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex);
// [BETA] Selected render state data shared with callbacks.
// This is temporarily stored in GetPlatformIO().Renderer_RenderState during the ImGui_ImplDX11_RenderDrawData() call.
// (Please open an issue if you feel you need access to more data)
struct ImGui_ImplDX11_RenderState
{
ID3D11Device* Device;
ID3D11DeviceContext* DeviceContext;
ID3D11Buffer* VertexConstantBuffer;
//ID3D11SamplerState* SamplerLinear; // Use ImDrawList::AddCallback(ImGui::GetPlatform().DrawCallback_SetSamplerLinear)
//ID3D11SamplerState* SamplerNearest; // Use ImDrawList::AddCallback(ImGui::GetPlatform().DrawCallback_SetSamplerNearest)
};
#endif // #ifndef IMGUI_DISABLE
+156 -123
View File
@@ -19,6 +19,7 @@
// CHANGELOG // CHANGELOG
// (minor and older changes stripped away, please see git history for details) // (minor and older changes stripped away, please see git history for details)
// 2026-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. // 2026-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface.
// 2026-04-23: Added support for standard draw callbacks (in platform_io): DrawCallback_ResetRenderState, DrawCallback_SetSamplerLinear, DrawCallback_SetSamplerNearest. (#9378)
// 2026-03-19: Fixed issue in ImGui_ImplDX9_UpdateTexture() if ImTextureID_Invalid is defined to be != 0, which became the default since 2026-03-12. (#9295, #9310) // 2026-03-19: Fixed issue in ImGui_ImplDX9_UpdateTexture() if ImTextureID_Invalid is defined to be != 0, which became the default since 2026-03-12. (#9295, #9310)
// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown. // 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown.
// 2025-06-11: DirectX9: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas. // 2025-06-11: DirectX9: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas.
@@ -110,7 +111,70 @@ struct render_state_t {
}; };
// Functions // Functions
static void ImGui_ImplDX9_SetupRenderState(ImDrawData* draw_data, struct render_state_t *render_state)
// spice ldj
// Save the host game's render targets / depth-stencil and redirect rendering to the
// device back buffer. Must be called exactly once per RenderDrawData (paired with the
// restore block at the end of RenderDrawData), NOT from SetupRenderState which can be
// re-invoked via ImDrawCallback_ResetRenderState.
static void ImGui_ImplDX9_RedirectToBackBuffer(LPDIRECT3DDEVICE9 device, struct render_state_t *render_state)
{
if (FAILED(device->GetDeviceCaps(&render_state->caps))) {
render_state->caps.NumSimultaneousRTs = 0UL;
}
// save all previous render target state
for (size_t target = 0; target < std::min(8UL, render_state->caps.NumSimultaneousRTs); target++) {
if (FAILED(device->GetRenderTarget(target, &render_state->render_targets[target]))) {
render_state->render_targets[target] = nullptr;
}
}
// get the previous depth stencil
if (FAILED(device->GetDepthStencilSurface(&render_state->depth_stencil))) {
render_state->depth_stencil = nullptr;
}
// set the back buffer as the current render target
if (SUCCEEDED(device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &render_state->back_buffer))) {
device->SetRenderTarget(0, render_state->back_buffer);
device->SetDepthStencilSurface(nullptr);
for (size_t target = 1; target < std::min(8UL, render_state->caps.NumSimultaneousRTs); target++) {
device->SetRenderTarget(target, nullptr);
}
} else {
render_state->back_buffer = nullptr;
}
}
// spice ldj
// Restore the host game's render targets / depth-stencil saved by
// ImGui_ImplDX9_RedirectToBackBuffer and release all references taken there.
static void ImGui_ImplDX9_RestoreRenderTargets(LPDIRECT3DDEVICE9 device, struct render_state_t *render_state)
{
if (render_state->back_buffer) {
render_state->back_buffer->Release();
render_state->back_buffer = nullptr;
}
// restore previous depth stencil
if (render_state->depth_stencil) {
device->SetDepthStencilSurface(render_state->depth_stencil);
render_state->depth_stencil->Release();
render_state->depth_stencil = nullptr;
}
// restore all render target state
for (size_t target = 0; target < std::min(8UL, render_state->caps.NumSimultaneousRTs); target++) {
auto render_target = render_state->render_targets[target];
if (render_target) {
device->SetRenderTarget(target, render_target);
render_target->Release();
}
}
}
static void ImGui_ImplDX9_SetupRenderState(ImDrawData* draw_data)
{ {
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData(); ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
@@ -125,37 +189,6 @@ static void ImGui_ImplDX9_SetupRenderState(ImDrawData* draw_data, struct render_
LPDIRECT3DDEVICE9 device = bd->pd3dDevice; LPDIRECT3DDEVICE9 device = bd->pd3dDevice;
device->SetViewport(&vp); device->SetViewport(&vp);
// spice ldj
{
if (FAILED(device->GetDeviceCaps(&render_state->caps))) {
render_state->caps.NumSimultaneousRTs = 0UL;
}
// save all previous render target state
for (size_t target = 0; target < std::min(8UL, render_state->caps.NumSimultaneousRTs); target++) {
if (FAILED(device->GetRenderTarget(target, &render_state->render_targets[target]))) {
render_state->render_targets[target] = nullptr;
}
}
// get the previous depth stencil
if (FAILED(device->GetDepthStencilSurface(&render_state->depth_stencil))) {
render_state->depth_stencil = nullptr;
}
// set the back buffer as the current render target
if (SUCCEEDED(device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &render_state->back_buffer))) {
device->SetRenderTarget(0, render_state->back_buffer);
device->SetDepthStencilSurface(nullptr);
for (size_t target = 1; target < std::min(8UL, render_state->caps.NumSimultaneousRTs); target++) {
device->SetRenderTarget(target, nullptr);
}
} else {
render_state->back_buffer = nullptr;
}
}
// Setup render state: fixed-pipeline, alpha-blending, no face culling, no depth testing, shade mode (for gradient), bilinear sampling. // Setup render state: fixed-pipeline, alpha-blending, no face culling, no depth testing, shade mode (for gradient), bilinear sampling.
device->SetPixelShader(nullptr); device->SetPixelShader(nullptr);
device->SetVertexShader(nullptr); device->SetVertexShader(nullptr);
@@ -214,6 +247,11 @@ static void ImGui_ImplDX9_SetupRenderState(ImDrawData* draw_data, struct render_
} }
} }
// Draw callbacks
static void ImGui_ImplDX9_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way.
static void ImGui_ImplDX9_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData(); bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); }
static void ImGui_ImplDX9_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData(); bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_POINT); bd->pd3dDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_POINT); }
// Render function. // Render function.
void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data) void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data)
{ {
@@ -236,14 +274,14 @@ void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data)
{ {
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; } if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
bd->VertexBufferSize = draw_data->TotalVtxCount + 5000; bd->VertexBufferSize = draw_data->TotalVtxCount + 5000;
if (device->CreateVertexBuffer(bd->VertexBufferSize * sizeof(CUSTOMVERTEX), D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &bd->pVB, nullptr) < 0) if (device->CreateVertexBuffer((UINT)bd->VertexBufferSize * sizeof(CUSTOMVERTEX), D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &bd->pVB, nullptr) < 0)
return; return;
} }
if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount) if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount)
{ {
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; } if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
bd->IndexBufferSize = draw_data->TotalIdxCount + 10000; bd->IndexBufferSize = draw_data->TotalIdxCount + 10000;
if (device->CreateIndexBuffer(bd->IndexBufferSize * sizeof(ImDrawIdx), D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, sizeof(ImDrawIdx) == 2 ? D3DFMT_INDEX16 : D3DFMT_INDEX32, D3DPOOL_DEFAULT, &bd->pIB, nullptr) < 0) if (device->CreateIndexBuffer((UINT)bd->IndexBufferSize * sizeof(ImDrawIdx), D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, sizeof(ImDrawIdx) == 2 ? D3DFMT_INDEX16 : D3DFMT_INDEX32, D3DPOOL_DEFAULT, &bd->pIB, nullptr) < 0)
return; return;
} }
@@ -305,9 +343,16 @@ void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data)
device->SetIndices(bd->pIB); device->SetIndices(bd->pIB);
device->SetFVF(D3DFVF_CUSTOMVERTEX); device->SetFVF(D3DFVF_CUSTOMVERTEX);
// Setup desired DX state
// spice ldj: redirect to back buffer once (before SetupRenderState so its
// viewport isn't clobbered by SetRenderTarget); paired with
// ImGui_ImplDX9_RestoreRenderTargets after the render loop.
struct render_state_t render_state = {}; struct render_state_t render_state = {};
ImGui_ImplDX9_SetupRenderState(draw_data, &render_state); ImGui_ImplDX9_RedirectToBackBuffer(device, &render_state);
// Setup desired DX state
ImGui_ImplDX9_SetupRenderState(draw_data);
// Render command lists // Render command lists
// (Because we merged all buffers into a single one, we maintain our own offset into them) // (Because we merged all buffers into a single one, we maintain our own offset into them)
@@ -322,9 +367,8 @@ void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data)
if (pcmd->UserCallback != nullptr) if (pcmd->UserCallback != nullptr)
{ {
// User callback, registered via ImDrawList::AddCallback() // User callback, registered via ImDrawList::AddCallback()
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.) if (pcmd->UserCallback == ImGui_ImplDX9_DrawCallback_ResetRenderState)
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState) ImGui_ImplDX9_SetupRenderState(draw_data);
ImGui_ImplDX9_SetupRenderState(draw_data, &render_state);
else else
pcmd->UserCallback(draw_list, pcmd); pcmd->UserCallback(draw_list, pcmd);
} }
@@ -356,27 +400,7 @@ void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data)
bd->pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 0, 0, 0); bd->pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 0, 0, 0);
// spice ldj // spice ldj
{ ImGui_ImplDX9_RestoreRenderTargets(device, &render_state);
if (render_state.back_buffer) {
render_state.back_buffer->Release();
render_state.back_buffer = nullptr;
}
// restore previous depth stencil
if (render_state.depth_stencil) {
device->SetDepthStencilSurface(render_state.depth_stencil);
render_state.depth_stencil->Release();
render_state.depth_stencil = nullptr;
}
// restore all render target state
for (size_t target = 0; target < std::min(8UL, render_state.caps.NumSimultaneousRTs); target++) {
auto render_target = render_state.render_targets[target];
if (render_target) {
device->SetRenderTarget(target, render_target);
render_target->Release();
}
}
}
// Restore the DX9 transform // Restore the DX9 transform
device->SetTransform(D3DTS_WORLD, &last_world); device->SetTransform(D3DTS_WORLD, &last_world);
@@ -406,50 +430,6 @@ static bool ImGui_ImplDX9_CheckFormatSupport(LPDIRECT3DDEVICE9 pDevice, D3DFORMA
return support; return support;
} }
bool ImGui_ImplDX9_Init(IDirect3DDevice9* device)
{
ImGuiIO& io = ImGui::GetIO();
IMGUI_CHECKVERSION();
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
// Setup backend capabilities flags
ImGui_ImplDX9_Data* bd = IM_NEW(ImGui_ImplDX9_Data)();
io.BackendRendererUserData = (void*)bd;
io.BackendRendererName = "imgui_impl_dx9";
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional)
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.Renderer_TextureMaxWidth = platform_io.Renderer_TextureMaxHeight = 4096;
bd->pd3dDevice = device;
bd->pd3dDevice->AddRef();
bd->HasRgbaSupport = ImGui_ImplDX9_CheckFormatSupport(bd->pd3dDevice, D3DFMT_A8B8G8R8);
ImGui_ImplDX9_InitMultiViewportSupport();
return true;
}
void ImGui_ImplDX9_Shutdown()
{
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
ImGuiIO& io = ImGui::GetIO();
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImGui_ImplDX9_ShutdownMultiViewportSupport();
ImGui_ImplDX9_InvalidateDeviceObjects();
if (bd->pd3dDevice) { bd->pd3dDevice->Release(); }
io.BackendRendererName = nullptr;
io.BackendRendererUserData = nullptr;
io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures | ImGuiBackendFlags_RendererHasViewports);
platform_io.ClearRendererHandlers();
IM_DELETE(bd);
}
// Convert RGBA32 to BGRA32 (because RGBA32 is not well supported by DX9 devices) // Convert RGBA32 to BGRA32 (because RGBA32 is not well supported by DX9 devices)
static void ImGui_ImplDX9_CopyTextureRegion(bool tex_use_colors, const ImU32* src, int src_pitch, ImU32* dst, int dst_pitch, int w, int h) static void ImGui_ImplDX9_CopyTextureRegion(bool tex_use_colors, const ImU32* src, int src_pitch, ImU32* dst, int dst_pitch, int w, int h)
{ {
@@ -557,6 +537,34 @@ void ImGui_ImplDX9_InvalidateDeviceObjects()
ImGui_ImplDX9_InvalidateDeviceObjectsForPlatformWindows(); ImGui_ImplDX9_InvalidateDeviceObjectsForPlatformWindows();
} }
// spice ldj
// Derive io.DisplaySize from the device's swap chain back buffer dimensions, so the
// overlay sizes itself correctly without relying on a platform backend to feed it.
static void ImGui_ImplDX9_UpdateDisplaySize(ImGui_ImplDX9_Data* bd)
{
IDirect3DSwapChain9 *swap_chain = nullptr;
if (SUCCEEDED(bd->pd3dDevice->GetSwapChain(0, &swap_chain))) {
auto &io = ImGui::GetIO();
D3DPRESENT_PARAMETERS present_params {};
if (SUCCEEDED(swap_chain->GetPresentParameters(&present_params))) {
if (present_params.BackBufferWidth != 0 && present_params.BackBufferHeight != 0) {
io.DisplaySize.x = static_cast<float>(present_params.BackBufferWidth);
io.DisplaySize.y = static_cast<float>(present_params.BackBufferHeight);
} else {
RECT rect {};
GetClientRect(present_params.hDeviceWindow, &rect);
io.DisplaySize.x = static_cast<float>(rect.right - rect.left);
io.DisplaySize.y = static_cast<float>(rect.bottom - rect.top);
}
}
swap_chain->Release();
}
}
void ImGui_ImplDX9_NewFrame() void ImGui_ImplDX9_NewFrame()
{ {
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData(); ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
@@ -564,29 +572,54 @@ void ImGui_ImplDX9_NewFrame()
// IM_UNUSED(bd); // IM_UNUSED(bd);
// spice ldj // spice ldj
{ ImGui_ImplDX9_UpdateDisplaySize(bd);
IDirect3DSwapChain9 *swap_chain = nullptr; }
if (SUCCEEDED(bd->pd3dDevice->GetSwapChain(0, &swap_chain))) {
auto &io = ImGui::GetIO();
D3DPRESENT_PARAMETERS present_params {}; bool ImGui_ImplDX9_Init(IDirect3DDevice9* device)
{
ImGuiIO& io = ImGui::GetIO();
IMGUI_CHECKVERSION();
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
if (SUCCEEDED(swap_chain->GetPresentParameters(&present_params))) { // Setup backend capabilities flags
if (present_params.BackBufferWidth != 0 && present_params.BackBufferHeight != 0) { ImGui_ImplDX9_Data* bd = IM_NEW(ImGui_ImplDX9_Data)();
io.DisplaySize.x = static_cast<float>(present_params.BackBufferWidth); io.BackendRendererUserData = (void*)bd;
io.DisplaySize.y = static_cast<float>(present_params.BackBufferHeight); io.BackendRendererName = "imgui_impl_dx9";
} else { io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
RECT rect {}; io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
GetClientRect(present_params.hDeviceWindow, &rect); io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional)
io.DisplaySize.x = static_cast<float>(rect.right - rect.left); ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
io.DisplaySize.y = static_cast<float>(rect.bottom - rect.top); platform_io.Renderer_TextureMaxWidth = platform_io.Renderer_TextureMaxHeight = 4096;
} platform_io.DrawCallback_ResetRenderState = ImGui_ImplDX9_DrawCallback_ResetRenderState;
} platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplDX9_DrawCallback_SetSamplerLinear;
platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplDX9_DrawCallback_SetSamplerNearest;
swap_chain->Release(); bd->pd3dDevice = device;
} bd->pd3dDevice->AddRef();
} bd->HasRgbaSupport = ImGui_ImplDX9_CheckFormatSupport(bd->pd3dDevice, D3DFMT_A8B8G8R8);
ImGui_ImplDX9_InitMultiViewportSupport();
return true;
}
void ImGui_ImplDX9_Shutdown()
{
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
ImGuiIO& io = ImGui::GetIO();
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImGui_ImplDX9_ShutdownMultiViewportSupport();
ImGui_ImplDX9_InvalidateDeviceObjects();
if (bd->pd3dDevice) { bd->pd3dDevice->Release(); }
io.BackendRendererName = nullptr;
io.BackendRendererUserData = nullptr;
io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures | ImGuiBackendFlags_RendererHasViewports);
platform_io.ClearRendererHandlers();
IM_DELETE(bd);
} }
//-------------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------------
+199 -105
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7 // dear imgui, v1.92.9 WIP
// (main code and documentation) // (main code and documentation)
// Help: // Help:
@@ -402,7 +402,34 @@ IMPLEMENTING SUPPORT for ImGuiBackendFlags_RendererHasTextures:
you may use GetMainViewport()->Pos to offset hard-coded positions, e.g. SetNextWindowPos(GetMainViewport()->Pos) you may use GetMainViewport()->Pos to offset hard-coded positions, e.g. SetNextWindowPos(GetMainViewport()->Pos)
- likewise io.MousePos and GetMousePos() will use OS coordinates. - likewise io.MousePos and GetMousePos() will use OS coordinates.
If you query mouse positions to interact with non-imgui coordinates you will need to offset them, e.g. subtract GetWindowViewport()->Pos. If you query mouse positions to interact with non-imgui coordinates you will need to offset them, e.g. subtract GetWindowViewport()->Pos.
- 2026/06/02 (1.92.9) - TreeNode: commented out legacy name ImGuiTreeNodeFlags_SpanTextWidth which was obsoleted in 1.90.7 (May 2024). Use ImGuiTreeNodeFlags_SpanLabelWidth instead.
- 2026/05/07 (1.92.8) - DrawList: swapped the last two arguments of AddRect(), AddPolyline(), PathStroke().
- Before: void ImDrawList::AddRect(ImVec2 p_min, ImVec2 p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0, float thickness = 1.0f);
- After: void ImDrawList::AddRect(ImVec2 p_min, ImVec2 p_max, ImU32 col, float rounding = 0.0f, float thickness = 1.0f, ImDrawFlags flags = 0);
- Before: void ImDrawList::AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness);
- After: void ImDrawList::AddPolyline(const ImVec2* points, int num_points, ImU32 col, float thickness, ImDrawFlags flags = 0);
- Before: void ImDrawList::PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f);
- After: void ImDrawList::PathStroke(ImU32 col, float thickness = 1.0f, ImDrawFlags flags = 0);
Added inline redirection functions when IMGUI_DISABLE_OBSOLETE_FUNCTIONS is off.
Marked the old functions are =delete when IMGUI_DISABLE_OBSOLETE_FUNCTIONS is on, to allow for better type-checking.
Effectively the typical call site is changing from:
- Before: window->DrawList->AddRect(p_min, p_max, color, rounding, ImDrawFlags_None, border_size);
- After: window->DrawList->AddRect(p_min, p_max, color, rounding, border_size);
Notes:
- Users of C++ and other languages with type-checking will be notified at compile-time of any mistakes.
- Users of high-level bindings or languages with no type-checking will be notified at runtime via an assert for invalid flags value.
If you are a binding maintainer consider doing something to facilitate transition or error detection.
- This is perhaps the worst breaking change in our history :( but it makes ImDrawList function signatures consistent.
As we are aiming to add flags and features to variety of ImDrawList functions, that consistency becomes more important.
The new order is also more convenient as `flags` are less frequently used than `thickness` in real code.
- As a general policy in Dear ImGui, all our flags default to 0 so ImDrawFlags_None was likely written 0 in some call sites.
- Consider adding `#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS` in your imconfig.h, even temporarily, to clean up legacy code.
- 2026/05/07 (1.92.8) - DrawList: changed value of `ImDrawFlags_Closed`. It was previously advertised as "always == 1" when introduced in 1.82 (2021/02), in order to facilitate backward compatibility with the legacy `bool closed` flag.
This guarantee has been removed. The bit is reserved and `AddPolyline()`, `PathStroke()` will assert when it is used.
- 2026/04/23 (1.92.8) - DrawList: obsoleted `ImDrawCallback_ResetRenderState` in favor of using `ImGui::GetPlatformIO().DrawCallback_ResetRenderState`, which is part of our new standard draw callbacks. (#9378)
- 2026/04/22 (1.92.8) - Backends: Vulkan: redesigned to use separate ImageView + Sampler instead of Combined Image Sampler.
- When registering custom textures: changed ImGui_ImplVulkan_AddTexture() signature to remove Sampler.
- When creating your own descriptor pool (instead of letting backend creates its own): need at least IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE descriptors of type VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE + IMGUI_IMPL_VULKAN_MINIMUM_SAMPLER_POOL_SIZE descriptors of type VK_DESCRIPTOR_TYPE_SAMPLER.
- 2026/03/19 (1.92.7) - MultiSelect: renamed ImGuiMultiSelectFlags_SelectOnClick to ImGuiMultiSelectFlags_SelectOnAuto. - 2026/03/19 (1.92.7) - MultiSelect: renamed ImGuiMultiSelectFlags_SelectOnClick to ImGuiMultiSelectFlags_SelectOnAuto.
- 2026/02/26 (1.92.7) - Separator: fixed a legacy quirk where Separator() was submitting a zero-height item for layout purpose, even though it draws a 1-pixel separator. - 2026/02/26 (1.92.7) - Separator: fixed a legacy quirk where Separator() was submitting a zero-height item for layout purpose, even though it draws a 1-pixel separator.
The fix could affect code e.g. computing height from multiple widgets in order to allocate vertical space for a footer or multi-line status bar. (#2657, #9263) The fix could affect code e.g. computing height from multiple widgets in order to allocate vertical space for a footer or multi-line status bar. (#2657, #9263)
@@ -1524,7 +1551,7 @@ ImGuiStyle::ImGuiStyle()
TabRounding = 5.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. TabRounding = 5.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs.
TabBorderSize = 0.0f; // Thickness of border around tabs. TabBorderSize = 0.0f; // Thickness of border around tabs.
TabMinWidthBase = 1.0f; // Minimum tab width, to make tabs larger than their contents. TabBar buttons are not affected. TabMinWidthBase = 1.0f; // Minimum tab width, to make tabs larger than their contents. TabBar buttons are not affected.
TabMinWidthShrink = 80.0f; // Minimum tab width after shrinking, when using ImGuiTabBarFlags_FittingPolicyMixed policy. TabMinWidthShrink = 80.0f; // Minimum tab width after shrinking, when using ImGuiTabBarFlags_FittingPolicyMixed policy. FLT_MAX: never shrink, will behave like ImGuiTabBarFlags_FittingPolicyScroll.
TabCloseButtonMinWidthSelected = -1.0f; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. TabCloseButtonMinWidthSelected = -1.0f; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width.
TabCloseButtonMinWidthUnselected = 0.0f; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected. TabCloseButtonMinWidthUnselected = 0.0f; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected.
TabBarBorderSize = 1.0f; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. TabBarBorderSize = 1.0f; // Thickness of tab-bar separator, which takes on the tab active color to denote focus.
@@ -1541,6 +1568,7 @@ ImGuiStyle::ImGuiStyle()
ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right.
ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text.
SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line.
InputTextCursorSize = 1.0f; // Thickness of cursor/caret in InputText().
SeparatorSize = 1.0f; // Thickness of border in Separator(). SeparatorSize = 1.0f; // Thickness of border in Separator().
SeparatorTextBorderSize = 3.0f; // Thickness of border in SeparatorText(). SeparatorTextBorderSize = 3.0f; // Thickness of border in SeparatorText().
SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center).
@@ -1573,6 +1601,7 @@ ImGuiStyle::ImGuiStyle()
// Scale all spacing/padding/thickness values. Do not scale fonts. // Scale all spacing/padding/thickness values. Do not scale fonts.
// Consider not calling this if your initial scale factor if <1.0.
// Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times. // Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times.
void ImGuiStyle::ScaleAllSizes(float scale_factor) void ImGuiStyle::ScaleAllSizes(float scale_factor)
{ {
@@ -1617,6 +1646,7 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor)
DragDropTargetBorderSize = ImTrunc(DragDropTargetBorderSize * scale_factor); DragDropTargetBorderSize = ImTrunc(DragDropTargetBorderSize * scale_factor);
DragDropTargetPadding = ImTrunc(DragDropTargetPadding * scale_factor); DragDropTargetPadding = ImTrunc(DragDropTargetPadding * scale_factor);
ColorMarkerSize = ImTrunc(ColorMarkerSize * scale_factor); ColorMarkerSize = ImTrunc(ColorMarkerSize * scale_factor);
InputTextCursorSize = ImTrunc(InputTextCursorSize * scale_factor);
SeparatorSize = ImTrunc(SeparatorSize * scale_factor); SeparatorSize = ImTrunc(SeparatorSize * scale_factor);
SeparatorTextBorderSize = ImTrunc(SeparatorTextBorderSize * scale_factor); SeparatorTextBorderSize = ImTrunc(SeparatorTextBorderSize * scale_factor);
SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor); SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor);
@@ -3454,9 +3484,6 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper)
if (clipper->ItemsHeight <= 0.0f) if (clipper->ItemsHeight <= 0.0f)
{ {
IM_ASSERT(data->StepNo == 1); IM_ASSERT(data->StepNo == 1);
if (table)
IM_ASSERT(table->RowPosY1 == clipper->StartPosY && table->RowPosY2 == window->DC.CursorPos.y);
bool affected_by_floating_point_precision = ImIsFloatAboveGuaranteedIntegerPrecision((float)clipper->StartPosY) || ImIsFloatAboveGuaranteedIntegerPrecision(window->DC.CursorPos.y); bool affected_by_floating_point_precision = ImIsFloatAboveGuaranteedIntegerPrecision((float)clipper->StartPosY) || ImIsFloatAboveGuaranteedIntegerPrecision(window->DC.CursorPos.y);
if (affected_by_floating_point_precision) if (affected_by_floating_point_precision)
{ {
@@ -3470,7 +3497,14 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper)
} }
if (clipper->ItemsHeight == 0.0f && clipper->ItemsCount == INT_MAX) // Accept that no item have been submitted if in indeterminate mode. if (clipper->ItemsHeight == 0.0f && clipper->ItemsCount == INT_MAX) // Accept that no item have been submitted if in indeterminate mode.
return false; return false;
IM_ASSERT(clipper->ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!"); if (clipper->ItemsHeight <= 0.0f)
{
IM_ASSERT_USER_ERROR(clipper->ItemsHeight > 0.0f, "ImGuiListClipper: Failed to calculate item height! First item hasn't been submitted by user code, or has not moved the cursor vertically!");
return false;
}
if (table)
IM_ASSERT(table->RowPosY1 == clipper->StartPosY && table->RowPosY2 == window->DC.CursorPos.y);
calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards. calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards.
} }
@@ -3516,12 +3550,14 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper)
// FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos.
// RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that.
// As a workaround we currently half ItemSpacing worth on each side. // As a workaround we currently half ItemSpacing worth on each side.
min_y -= g.Style.ItemSpacing.y; float pad_y = g.Style.ItemSpacing.y;
max_y += g.Style.ItemSpacing.y; min_y -= pad_y;
max_y += pad_y;
// Box-select on 2D area requires different clipping. // Box-select on 2D area requires different clipping.
// (best adding pad_y here than in BeginBoxSelect() as we are closer to current state)
if (bs->UnclipMode) if (bs->UnclipMode)
data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y, bs->UnclipRect.Max.y, 0, 0)); data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y - pad_y, bs->UnclipRect.Max.y + pad_y, 0, 0));
} }
// Add main visible range // Add main visible range
@@ -3720,6 +3756,7 @@ static const ImGuiStyleVarInfo GStyleVarsInfo[] =
{ 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign
{ 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TreeLinesSize)}, // ImGuiStyleVar_TreeLinesSize { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TreeLinesSize)}, // ImGuiStyleVar_TreeLinesSize
{ 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TreeLinesRounding)}, // ImGuiStyleVar_TreeLinesRounding { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TreeLinesRounding)}, // ImGuiStyleVar_TreeLinesRounding
{ 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, DragDropTargetRounding)}, // ImGuiStyleVar_DragDropTargetRounding
{ 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign
{ 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign
{ 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorSize)}, // ImGuiStyleVar_SeparatorSize { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorSize)}, // ImGuiStyleVar_SeparatorSize
@@ -3821,6 +3858,7 @@ const char* ImGui::GetStyleColorName(ImGuiCol idx)
case ImGuiCol_ScrollbarGrabHovered: return "ScrollbarGrabHovered"; case ImGuiCol_ScrollbarGrabHovered: return "ScrollbarGrabHovered";
case ImGuiCol_ScrollbarGrabActive: return "ScrollbarGrabActive"; case ImGuiCol_ScrollbarGrabActive: return "ScrollbarGrabActive";
case ImGuiCol_CheckMark: return "CheckMark"; case ImGuiCol_CheckMark: return "CheckMark";
case ImGuiCol_CheckboxSelectedBg: return "CheckboxSelectedBg";
case ImGuiCol_SliderGrab: return "SliderGrab"; case ImGuiCol_SliderGrab: return "SliderGrab";
case ImGuiCol_SliderGrabActive: return "SliderGrabActive"; case ImGuiCol_SliderGrabActive: return "SliderGrabActive";
case ImGuiCol_Button: return "Button"; case ImGuiCol_Button: return "Button";
@@ -3903,7 +3941,7 @@ void ImGui::RenderText(ImVec2 pos, const char* text, const char* text_end, bool
else else
{ {
if (!text_end) if (!text_end)
text_end = text + ImStrlen(text); // FIXME-OPT text_end = text + ImStrlen(text); // FIXME-OPT (not reached by our internal calls)
text_display_end = text_end; text_display_end = text_end;
} }
@@ -3921,7 +3959,7 @@ void ImGui::RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end
ImGuiWindow* window = g.CurrentWindow; ImGuiWindow* window = g.CurrentWindow;
if (!text_end) if (!text_end)
text_end = text + ImStrlen(text); // FIXME-OPT text_end = text + ImStrlen(text); // FIXME-OPT (not reached by our internal calls)
if (text != text_end) if (text != text_end)
{ {
@@ -3990,8 +4028,8 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con
text_end_full = FindRenderedTextEnd(text); text_end_full = FindRenderedTextEnd(text);
const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_end_full, false, 0.0f); const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_end_full, false, 0.0f);
//draw_list->AddLine(ImVec2(pos_max.x, pos_min.y - 4), ImVec2(pos_max.x, pos_max.y + 6), IM_COL32(0, 0, 255, 255)); //draw_list->AddLineV(pos_max.x, pos_min.y - 4, pos_max.y + 6, IM_COL32(0, 0, 255, 255));
//draw_list->AddLine(ImVec2(ellipsis_max_x, pos_min.y - 2), ImVec2(ellipsis_max_x, pos_max.y + 3), IM_COL32(0, 255, 0, 255)); //draw_list->AddLineV(ellipsis_max_x, pos_min.y - 2, pos_max.y + 3, IM_COL32(0, 255, 0, 255));
// FIXME: We could technically remove (last_glyph->AdvanceX - last_glyph->X1) from text_size.x here and save a few pixels. // FIXME: We could technically remove (last_glyph->AdvanceX - last_glyph->X1) from text_size.x here and save a few pixels.
if (text_size.x > pos_max.x - pos_min.x) if (text_size.x > pos_max.x - pos_min.x)
@@ -4036,8 +4074,8 @@ void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders
const float border_size = g.Style.FrameBorderSize; const float border_size = g.Style.FrameBorderSize;
if (borders && border_size > 0.0f) if (borders && border_size > 0.0f)
{ {
window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, border_size);
window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, border_size);
} }
} }
@@ -4048,8 +4086,8 @@ void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding)
const float border_size = g.Style.FrameBorderSize; const float border_size = g.Style.FrameBorderSize;
if (border_size > 0.0f) if (border_size > 0.0f)
{ {
window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, border_size);
window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, border_size);
} }
} }
@@ -4084,7 +4122,7 @@ void ImGui::RenderNavCursor(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFl
const float thickness = 2.0f; const float thickness = 2.0f;
if (flags & ImGuiNavRenderCursorFlags_Compact) if (flags & ImGuiNavRenderCursorFlags_Compact)
{ {
window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavCursor), rounding, 0, thickness); window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavCursor), rounding, thickness);
} }
else else
{ {
@@ -4093,7 +4131,7 @@ void ImGui::RenderNavCursor(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFl
bool fully_visible = window->ClipRect.Contains(display_rect); bool fully_visible = window->ClipRect.Contains(display_rect);
if (!fully_visible) if (!fully_visible)
window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); window->DrawList->PushClipRect(display_rect.Min, display_rect.Max);
window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavCursor), rounding, 0, thickness); window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavCursor), rounding, thickness);
if (!fully_visible) if (!fully_visible)
window->DrawList->PopClipRect(); window->DrawList->PopClipRect();
} }
@@ -4127,7 +4165,7 @@ void ImGui::RenderMouseCursor(ImVec2 base_pos, float base_scale, ImGuiMouseCurso
float a_min = ImFmod((float)g.Time * 5.0f, 2.0f * IM_PI); float a_min = ImFmod((float)g.Time * 5.0f, 2.0f * IM_PI);
float a_max = a_min + IM_PI * 1.65f; float a_max = a_min + IM_PI * 1.65f;
draw_list->PathArcTo(pos + ImVec2(14, -1) * scale, 6.0f * scale, a_min, a_max); draw_list->PathArcTo(pos + ImVec2(14, -1) * scale, 6.0f * scale, a_min, a_max);
draw_list->PathStroke(col_fill, ImDrawFlags_None, 3.0f * scale); draw_list->PathStroke(col_fill, 3.0f * scale);
} }
draw_list->PopTexture(); draw_list->PopTexture();
} }
@@ -4229,7 +4267,7 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas)
IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)();
if (shared_font_atlas == NULL) if (shared_font_atlas == NULL)
IO.Fonts->OwnerContext = this; IO.Fonts->OwnerContext = this;
WithinEndChildID = 0; WithinEndChildID = WithinEndPopupID = 0;
TestEngine = NULL; TestEngine = NULL;
InputEventsNextMouseSource = ImGuiMouseSource_Mouse; InputEventsNextMouseSource = ImGuiMouseSource_Mouse;
@@ -4537,7 +4575,7 @@ void ImGui::Shutdown()
for (ImFontAtlas* atlas : g.FontAtlases) for (ImFontAtlas* atlas : g.FontAtlases)
{ {
UnregisterFontAtlas(atlas); UnregisterFontAtlas(atlas);
if (atlas->RefCount == 0) if (atlas->RefCount == 0 && atlas->OwnerContext == &g)
{ {
atlas->Locked = false; atlas->Locked = false;
IM_DELETE(atlas); IM_DELETE(atlas);
@@ -4795,7 +4833,8 @@ void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window)
g.ActiveIdIsJustActivated = (g.ActiveId != id); g.ActiveIdIsJustActivated = (g.ActiveId != id);
if (g.ActiveIdIsJustActivated) if (g.ActiveIdIsJustActivated)
{ {
IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() old:0x%08X (window \"%s\") -> new:0x%08X (window \"%s\")\n", g.ActiveId, g.ActiveIdWindow ? g.ActiveIdWindow->Name : "", id, window ? window->Name : ""); IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() 0x%08X in \"%s\"%*s(previously 0x%08X in \"%s\")\n", id, window ? window->Name : "",
ImMax(0, 20 - (int)(window ? strlen(window->Name) : 0)), "", g.ActiveId, g.ActiveIdWindow ? g.ActiveIdWindow->Name : "");
g.ActiveIdTimer = 0.0f; g.ActiveIdTimer = 0.0f;
g.ActiveIdHasBeenPressedBefore = false; g.ActiveIdHasBeenPressedBefore = false;
g.ActiveIdHasBeenEditedBefore = false; g.ActiveIdHasBeenEditedBefore = false;
@@ -4851,8 +4890,12 @@ void ImGui::MarkItemEdited(ImGuiID id)
// This marking is to be able to provide info for IsItemDeactivatedAfterEdit(). // This marking is to be able to provide info for IsItemDeactivatedAfterEdit().
// ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data. // ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data.
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_EditedInternal;
if (g.LastItemData.ItemFlags & ImGuiItemFlags_NoMarkEdited) if (g.LastItemData.ItemFlags & ImGuiItemFlags_NoMarkEdited)
return; return;
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited;
if (g.ActiveId == id || g.ActiveId == 0) if (g.ActiveId == id || g.ActiveId == 0)
{ {
// FIXME: Can't we fully rely on LastItemData yet? // FIXME: Can't we fully rely on LastItemData yet?
@@ -4866,9 +4909,6 @@ void ImGui::MarkItemEdited(ImGuiID id)
// We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714) // We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714)
// FIXME: This assert is getting a bit meaningless over time. It helped detect some unusual use cases but eventually it is becoming an unnecessary restriction. // FIXME: This assert is getting a bit meaningless over time. It helped detect some unusual use cases but eventually it is becoming an unnecessary restriction.
IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || g.NavJustMovedToId || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive)); IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || g.NavJustMovedToId || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive));
//IM_ASSERT(g.CurrentWindow->DC.LastItemId == id);
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited;
} }
bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags)
@@ -5042,7 +5082,7 @@ bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flag
{ {
g.HoveredIdPreviousFrameItemCount++; g.HoveredIdPreviousFrameItemCount++;
if (g.DebugDrawIdConflictsId == id) if (g.DebugDrawIdConflictsId == id)
window->DrawList->AddRect(bb.Min - ImVec2(1,1), bb.Max + ImVec2(1,1), IM_COL32(255, 0, 0, 255), 0.0f, ImDrawFlags_None, 2.0f); window->DrawList->AddRect(bb.Min - ImVec2(1,1), bb.Max + ImVec2(1,1), IM_COL32(255, 0, 0, 255), 0.0f, 2.0f);
} }
#endif #endif
@@ -5487,14 +5527,15 @@ void ImGui::UpdateMouseMovingWindowEndFrame()
// Click on empty space to focus window and start moving // Click on empty space to focus window and start moving
// (after we're done with all our widgets, so e.g. clicking on docking tab-bar which have set HoveredId already and not get us here!) // (after we're done with all our widgets, so e.g. clicking on docking tab-bar which have set HoveredId already and not get us here!)
if (g.IO.MouseClicked[0]) if (IsMouseClicked(0, ImGuiInputFlags_None, ImGuiKeyOwner_NoOwner))
{ {
// Handle the edge case of a popup being closed while clicking in its empty space. // Handle the edge case of a popup being closed while clicking in its empty space.
// If we try to focus it, FocusWindow() > ClosePopupsOverWindow() will accidentally close any parent popups because they are not linked together any more. // If we try to focus it, FocusWindow() > ClosePopupsOverWindow() will accidentally close any parent popups because they are not linked together any more.
ImGuiWindow* hovered_root = hovered_window ? hovered_window->RootWindow : NULL; ImGuiWindow* hovered_root = hovered_window ? hovered_window->RootWindow : NULL;
const bool is_closed_popup = hovered_root && (hovered_root->Flags & ImGuiWindowFlags_Popup) && !IsPopupOpen(hovered_root->PopupId, ImGuiPopupFlags_AnyPopupLevel); const bool is_closed_popup = hovered_root && (hovered_root->Flags & ImGuiWindowFlags_Popup) && !IsPopupOpen(hovered_root->PopupId, ImGuiPopupFlags_AnyPopupLevel);
const bool is_queued_focus_request = g.NavMoveSubmitted && (g.NavMoveFlags & ImGuiNavMoveFlags_FocusApi);
if (hovered_window != NULL && !is_closed_popup) if (hovered_window != NULL && !is_closed_popup && !is_queued_focus_request)
{ {
StartMouseMovingWindow(hovered_window); //-V595 StartMouseMovingWindow(hovered_window); //-V595
@@ -5525,7 +5566,7 @@ void ImGui::UpdateMouseMovingWindowEndFrame()
// With right mouse button we close popups without changing focus based on where the mouse is aimed // With right mouse button we close popups without changing focus based on where the mouse is aimed
// Instead, focus will be restored to the window under the bottom-most closed popup. // Instead, focus will be restored to the window under the bottom-most closed popup.
// (The left mouse button path calls FocusWindow on the hovered window, which will lead NewFrame->ClosePopupsOverWindow to trigger) // (The left mouse button path calls FocusWindow on the hovered window, which will lead NewFrame->ClosePopupsOverWindow to trigger)
if (g.IO.MouseClicked[1] && g.HoveredId == 0) if (g.HoveredId == 0 && IsMouseClicked(1, ImGuiInputFlags_None, ImGuiKeyOwner_NoOwner))
{ {
// Find the top-most window between HoveredWindow and the top-most Modal Window. // Find the top-most window between HoveredWindow and the top-most Modal Window.
// This is where we can trim the popup stack. // This is where we can trim the popup stack.
@@ -5920,7 +5961,7 @@ void ImGui::NewFrame()
g.CurrentWindowStack.resize(0); g.CurrentWindowStack.resize(0);
g.BeginPopupStack.resize(0); g.BeginPopupStack.resize(0);
g.ItemFlagsStack.resize(0); g.ItemFlagsStack.resize(0);
g.ItemFlagsStack.push_back(ImGuiItemFlags_AutoClosePopups); // Default flags g.ItemFlagsStack.push_back(ImGuiItemFlags_Default_); // Default flags
g.CurrentItemFlags = g.ItemFlagsStack.back(); g.CurrentItemFlags = g.ItemFlagsStack.back();
g.GroupStack.resize(0); g.GroupStack.resize(0);
@@ -6090,14 +6131,6 @@ void ImGui::PopClipRect()
window->ClipRect = window->DrawList->_ClipRectStack.back(); window->ClipRect = window->DrawList->_ClipRectStack.back();
} }
static ImGuiWindow* FindFrontMostVisibleChildWindow(ImGuiWindow* window)
{
for (int n = window->DC.ChildWindows.Size - 1; n >= 0; n--)
if (IsWindowActiveAndVisible(window->DC.ChildWindows[n]))
return FindFrontMostVisibleChildWindow(window->DC.ChildWindows[n]);
return window;
}
static void ImGui::RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col) static void ImGui::RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col)
{ {
if ((col & IM_COL32_A_MASK) == 0) if ((col & IM_COL32_A_MASK) == 0)
@@ -6199,7 +6232,7 @@ static void ImGui::RenderDimmedBackgrounds()
if (window->DrawList->CmdBuffer.Size == 0) if (window->DrawList->CmdBuffer.Size == 0)
window->DrawList->AddDrawCmd(); window->DrawList->AddDrawCmd();
window->DrawList->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size); window->DrawList->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size);
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 0, 3.0f); // FIXME-DPI window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 3.0f); // FIXME-DPI
window->DrawList->PopClipRect(); window->DrawList->PopClipRect();
} }
@@ -6422,11 +6455,9 @@ ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_tex
ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL); ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL);
// Round // Round
// FIXME: This has been here since Dec 2015 (7b0bf230) but down the line we want this out. // (see 7b0bf230, 4622fa4b6, #791 for details about this.)
// FIXME: Investigate using ceilf or e.g. // FIXME: Add a way to disable this.
// - https://git.musl-libc.org/cgit/musl/tree/src/math/ceilf.c text_size.x = ImCeilFast(text_size.x);
// - https://embarkstudios.github.io/rust-gpu/api/src/libm/math/ceilf.rs.html
text_size.x = IM_TRUNC(text_size.x + 0.99999f);
return text_size; return text_size;
} }
@@ -6566,11 +6597,12 @@ bool ImGui::IsItemToggledOpen()
return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledOpen) ? true : false; return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledOpen) ? true : false;
} }
// Call after a Selectable() or TreeNode() involved in multi-selection. // Call after a Selectable() or TreeNode() items inside a BeginMultiSelect()/EndMultiSelect() scope.
// Useful if you need the per-item information before reaching EndMultiSelect(), e.g. for rendering purpose. // - Useful if you need the per-item information before reaching EndMultiSelect(), e.g. for rendering purpose.
// This is only meant to be called inside a BeginMultiSelect()/EndMultiSelect() block. // Outside of a multi-select block:
// (Outside of multi-select, it would be misleading/ambiguous to report this signal, as widgets // - It would be misleading/ambiguous to report this signal, as widgets return e.g. a pressed event,
// return e.g. a pressed event and user code is in charge of altering selection in ways we cannot predict.) // and user code is in charge of altering selection in ways we cannot predict.
// Prefer using 'if (IsItemClicked() && !IsItemToggledOpen())' for a manual reimplementation of selection.
bool ImGui::IsItemToggledSelection() bool ImGui::IsItemToggledSelection()
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
@@ -6717,7 +6749,7 @@ bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, I
window_flags |= ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDocking; window_flags |= ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDocking;
window_flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag window_flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag
if (child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize)) if (child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize))
window_flags |= ImGuiWindowFlags_AlwaysAutoResize; window_flags |= ImGuiWindowFlags_AlwaysAutoResize; // FIXME: Would be sane to not make single-axis flag set this. (#9355)
if ((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0) if ((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0)
window_flags |= ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; window_flags |= ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings;
@@ -6866,6 +6898,14 @@ void ImGui::EndChild()
g.LogLinePosY = -FLT_MAX; // To enforce a carriage return g.LogLinePosY = -FLT_MAX; // To enforce a carriage return
} }
ImGuiWindow* ImGui::FindFrontMostVisibleChildWindow(ImGuiWindow* window)
{
for (int n = window->DC.ChildWindows.Size - 1; n >= 0; n--)
if (IsWindowActiveAndVisible(window->DC.ChildWindows[n]))
return FindFrontMostVisibleChildWindow(window->DC.ChildWindows[n]);
return window;
}
static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled)
{ {
window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags);
@@ -7050,8 +7090,8 @@ static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_cont
const float decoration_h_without_scrollbars = window->DecoOuterSizeY1 + window->DecoOuterSizeY2 - window->ScrollbarSizes.y; const float decoration_h_without_scrollbars = window->DecoOuterSizeY1 + window->DecoOuterSizeY2 - window->ScrollbarSizes.y;
ImVec2 size_pad = window->WindowPadding * 2.0f; ImVec2 size_pad = window->WindowPadding * 2.0f;
ImVec2 size_desired; ImVec2 size_desired;
size_desired[ImGuiAxis_X] = (axis_mask & 1) ? size_contents.x + size_pad.x + decoration_w_without_scrollbars : window->Size.x; size_desired.x = (axis_mask & 1) ? size_contents.x + size_pad.x + decoration_w_without_scrollbars : window->Size.x;
size_desired[ImGuiAxis_Y] = (axis_mask & 2) ? size_contents.y + size_pad.y + decoration_h_without_scrollbars : window->Size.y; size_desired.y = (axis_mask & 2) ? size_contents.y + size_pad.y + decoration_h_without_scrollbars : window->Size.y;
// Determine maximum window size // Determine maximum window size
// Child windows are laid within their parent (unless they are also popups/menus) and thus have no restriction // Child windows are laid within their parent (unless they are also popups/menus) and thus have no restriction
@@ -7078,8 +7118,10 @@ static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_cont
// When the window cannot fit all contents (either because of constraints, either because screen is too small), // When the window cannot fit all contents (either because of constraints, either because screen is too small),
// we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding. // we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding.
ImVec2 size_auto_fit_after_constraint = CalcWindowSizeAfterConstraint(window, size_auto_fit); ImVec2 size_auto_fit_after_constraint = CalcWindowSizeAfterConstraint(window, size_auto_fit);
bool will_have_scrollbar_x = (size_auto_fit_after_constraint.x - size_pad.x - decoration_w_without_scrollbars < size_contents.x && !(window->Flags & ImGuiWindowFlags_NoScrollbar) && (window->Flags & ImGuiWindowFlags_HorizontalScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar); float size_contents_for_scrollbar_x = (axis_mask & 1) ? size_contents.x : window->ContentSize.x; // See #9352. In theory this should use same logic as `window->ScrollbarY = ...` codepath in Begin(). Needs some plumbling.
bool will_have_scrollbar_y = (size_auto_fit_after_constraint.y - size_pad.y - decoration_h_without_scrollbars < size_contents.y && !(window->Flags & ImGuiWindowFlags_NoScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysVerticalScrollbar); float size_contents_for_scrollbar_y = (axis_mask & 2) ? size_contents.y : window->ContentSize.y;
bool will_have_scrollbar_x = (size_auto_fit_after_constraint.x < size_contents_for_scrollbar_x + size_pad.x + decoration_w_without_scrollbars && !(window->Flags & ImGuiWindowFlags_NoScrollbar) && (window->Flags & ImGuiWindowFlags_HorizontalScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar);
bool will_have_scrollbar_y = (size_auto_fit_after_constraint.y < size_contents_for_scrollbar_y + size_pad.y + decoration_h_without_scrollbars && !(window->Flags & ImGuiWindowFlags_NoScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysVerticalScrollbar);
if (will_have_scrollbar_x) if (will_have_scrollbar_x)
size_auto_fit.y += style.ScrollbarSize; size_auto_fit.y += style.ScrollbarSize;
if (will_have_scrollbar_y) if (will_have_scrollbar_y)
@@ -7433,7 +7475,7 @@ static void RenderWindowOuterSingleBorder(ImGuiWindow* window, int border_n, ImU
const ImRect border_r = GetResizeBorderRect(window, border_n, rounding, 0.0f); const ImRect border_r = GetResizeBorderRect(window, border_n, rounding, 0.0f);
window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle); window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle);
window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f); window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f);
window->DrawList->PathStroke(border_col, ImDrawFlags_None, border_size); window->DrawList->PathStroke(border_col, border_size);
} }
static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window) static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window)
@@ -7442,7 +7484,7 @@ static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window)
const float border_size = window->WindowBorderSize; const float border_size = window->WindowBorderSize;
const ImU32 border_col = GetColorU32(ImGuiCol_Border); const ImU32 border_col = GetColorU32(ImGuiCol_Border);
if (border_size > 0.0f && (window->Flags & ImGuiWindowFlags_NoBackground) == 0) if (border_size > 0.0f && (window->Flags & ImGuiWindowFlags_NoBackground) == 0)
window->DrawList->AddRect(window->Pos, window->Pos + window->Size, border_col, window->WindowRounding, 0, window->WindowBorderSize); window->DrawList->AddRect(window->Pos, window->Pos + window->Size, border_col, window->WindowRounding, window->WindowBorderSize);
else if (border_size > 0.0f) else if (border_size > 0.0f)
{ {
if (window->ChildFlags & ImGuiChildFlags_ResizeX) // Similar code as 'resize_border_mask' computation in UpdateWindowManualResize() but we specifically only always draw explicit child resize border. if (window->ChildFlags & ImGuiChildFlags_ResizeX) // Similar code as 'resize_border_mask' computation in UpdateWindowManualResize() but we specifically only always draw explicit child resize border.
@@ -7459,7 +7501,7 @@ static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window)
if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive) if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive)
{ {
float y = window->Pos.y + window->TitleBarHeight - 1; float y = window->Pos.y + window->TitleBarHeight - 1;
window->DrawList->AddLine(ImVec2(window->Pos.x + border_size * 0.5f, y), ImVec2(window->Pos.x + window->Size.x - border_size * 0.5f, y), border_col, g.Style.FrameBorderSize); window->DrawList->AddLineH(window->Pos.x + border_size * 0.5f, window->Pos.x + window->Size.x - border_size * 0.5f, y, border_col, g.Style.FrameBorderSize);
} }
} }
@@ -7569,7 +7611,7 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar
menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them. menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them.
window->DrawList->AddRectFilled(menu_bar_rect.Min, menu_bar_rect.Max, GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawFlags_RoundCornersTop); window->DrawList->AddRectFilled(menu_bar_rect.Min, menu_bar_rect.Max, GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawFlags_RoundCornersTop);
if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y) if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y)
window->DrawList->AddLine(menu_bar_rect.GetBL() + ImVec2(window_border_size * 0.5f, 0.0f), menu_bar_rect.GetBR() - ImVec2(window_border_size * 0.5f, 0.0f), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); window->DrawList->AddLineH(menu_bar_rect.Min.x + window_border_size * 0.5f, menu_bar_rect.Max.x - window_border_size * 0.5f, menu_bar_rect.Max.y, GetColorU32(ImGuiCol_Border), style.FrameBorderSize);
} }
// Docking: Unhide tab bar (small triangle in the corner), drag from small triangle to quickly undock // Docking: Unhide tab bar (small triangle in the corner), drag from small triangle to quickly undock
@@ -8357,12 +8399,12 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags)
ImVec2 avail_size_from_current_frame = ImVec2(window->SizeFull.x, window->SizeFull.y - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2)); ImVec2 avail_size_from_current_frame = ImVec2(window->SizeFull.x, window->SizeFull.y - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2));
ImVec2 avail_size_from_last_frame = window->InnerRect.GetSize() + scrollbar_sizes_from_last_frame; ImVec2 avail_size_from_last_frame = window->InnerRect.GetSize() + scrollbar_sizes_from_last_frame;
ImVec2 needed_size_from_last_frame = window_just_created ? ImVec2(0, 0) : window->ContentSize + window->WindowPadding * 2.0f; ImVec2 needed_size_from_last_frame = window_just_created ? ImVec2(0, 0) : window->ContentSize + window->WindowPadding * 2.0f;
float size_x_for_scrollbars = use_current_size_for_scrollbar_x ? avail_size_from_current_frame.x : avail_size_from_last_frame.x; float size_for_scrollbars_x = use_current_size_for_scrollbar_x ? avail_size_from_current_frame.x : avail_size_from_last_frame.x;
float size_y_for_scrollbars = use_current_size_for_scrollbar_y ? avail_size_from_current_frame.y : avail_size_from_last_frame.y; float size_for_scrollbars_y = use_current_size_for_scrollbar_y ? avail_size_from_current_frame.y : avail_size_from_last_frame.y;
bool scrollbar_x_prev = window->ScrollbarX; bool scrollbar_x_prev = window->ScrollbarX;
//bool scrollbar_y_from_last_frame = window->ScrollbarY; // FIXME: May want to use that in the ScrollbarX expression? How many pros vs cons? //bool scrollbar_y_from_last_frame = window->ScrollbarY; // FIXME: May want to use that in the ScrollbarX expression? How many pros vs cons?
window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((needed_size_from_last_frame.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((needed_size_from_last_frame.y > size_for_scrollbars_y) && !(flags & ImGuiWindowFlags_NoScrollbar));
window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((needed_size_from_last_frame.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((needed_size_from_last_frame.x > size_for_scrollbars_x - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar));
// Track when ScrollbarX visibility keeps toggling, which is a sign of a feedback loop, and stabilize by enforcing visibility (#3285, #8488) // Track when ScrollbarX visibility keeps toggling, which is a sign of a feedback loop, and stabilize by enforcing visibility (#3285, #8488)
// (Feedback loops of this sort can manifest in various situations, but combining horizontal + vertical scrollbar + using a clipper with varying width items is one frequent cause. // (Feedback loops of this sort can manifest in various situations, but combining horizontal + vertical scrollbar + using a clipper with varying width items is one frequent cause.
@@ -8377,7 +8419,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags)
window->ScrollbarXStabilizeEnabled = scrollbar_x_stabilize; window->ScrollbarXStabilizeEnabled = scrollbar_x_stabilize;
if (window->ScrollbarX && !window->ScrollbarY) if (window->ScrollbarX && !window->ScrollbarY)
window->ScrollbarY = (needed_size_from_last_frame.y > size_y_for_scrollbars - style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); window->ScrollbarY = (needed_size_from_last_frame.y > size_for_scrollbars_y - style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar);
window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f);
// Amend the partially filled window->DecorationXXX values. // Amend the partially filled window->DecorationXXX values.
@@ -8546,9 +8588,14 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags)
window->DC.LayoutType = ImGuiLayoutType_Vertical; window->DC.LayoutType = ImGuiLayoutType_Vertical;
window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical; window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical;
// Default item width. Make it proportional to window size if window manually resizes // Default item width. Make it proportional to window size if window can be manually resized.
const bool is_resizable_window = (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)); // (we cannot use AutoFitFramesX/AutoFitFramesY which is a temporary state)
if (is_resizable_window) bool is_resizable_width;
if (flags & ImGuiWindowFlags_ChildWindow)
is_resizable_width = (window->Size.x > 0.0f) && !(window->ChildFlags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AlwaysAutoResize));
else
is_resizable_width = (window->Size.x > 0.0f) && !(flags & ImGuiWindowFlags_AlwaysAutoResize);
if (is_resizable_width)
window->DC.ItemWidthDefault = ImTrunc(window->Size.x * 0.65f); window->DC.ItemWidthDefault = ImTrunc(window->Size.x * 0.65f);
else else
window->DC.ItemWidthDefault = ImTrunc(g.FontSize * 16.0f); window->DC.ItemWidthDefault = ImTrunc(g.FontSize * 16.0f);
@@ -8770,6 +8817,8 @@ void ImGui::End()
ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back(); ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back();
// Error checking: verify that user doesn't directly call End() on a child window. // Error checking: verify that user doesn't directly call End() on a child window.
if (window->Flags & ImGuiWindowFlags_Popup)
IM_ASSERT_USER_ERROR(g.WithinEndPopupID == window->ID, "Must call EndPopup() and not End()!");
if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_DockNodeHost) && !window->DockIsActive) if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_DockNodeHost) && !window->DockIsActive)
IM_ASSERT_USER_ERROR(g.WithinEndChildID == window->ID, "Must call EndChild() and not End()!"); IM_ASSERT_USER_ERROR(g.WithinEndChildID == window->ID, "Must call EndChild() and not End()!");
@@ -9367,6 +9416,17 @@ void ImGui::PopFocusScope()
g.CurrentFocusScopeId = g.FocusScopeStack.Size ? g.FocusScopeStack.back().ID : 0; g.CurrentFocusScopeId = g.FocusScopeStack.Size ? g.FocusScopeStack.back().ID : 0;
} }
bool ImGui::IsInNavFocusRoute(ImGuiID focus_scope_id)
{
ImGuiContext& g = *GImGui;
if (g.NavFocusScopeId == focus_scope_id)
return true;
for (const ImGuiFocusScopeData& focus_scope : g.NavFocusRoute)
if (focus_scope.ID == focus_scope_id)
return true;
return false;
}
void ImGui::SetNavFocusScope(ImGuiID focus_scope_id) void ImGui::SetNavFocusScope(ImGuiID focus_scope_id)
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
@@ -9540,6 +9600,8 @@ static void ImGui::UpdateTexturesNewFrame()
IM_ASSERT(atlas->RendererHasTextures == has_textures); IM_ASSERT(atlas->RendererHasTextures == has_textures);
} }
} }
for (ImTextureData* tex : g.UserTextures)
ImTextureDataUpdateNewFrame(tex);
} }
// Build a single texture list // Build a single texture list
@@ -9601,7 +9663,7 @@ ImFont* ImGui::GetDefaultFont()
return g.IO.FontDefault ? g.IO.FontDefault : atlas->Fonts[0]; return g.IO.FontDefault ? g.IO.FontDefault : atlas->Fonts[0];
} }
// EXPERIMENTAL: DO NOT USE YET. // EXPERIMENTAL. Use ImTextureDataQueueUpload() to queue updates. Textures logic will be automatically be updated in NewFrame().
void ImGui::RegisterUserTexture(ImTextureData* tex) void ImGui::RegisterUserTexture(ImTextureData* tex)
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
@@ -9723,7 +9785,7 @@ void ImGui::UpdateCurrentFontSize(float restore_font_size_after_scaling)
} }
g.FontBaked = (g.Font != NULL && window != NULL) ? g.Font->GetFontBaked(final_size) : NULL; g.FontBaked = (g.Font != NULL && window != NULL) ? g.Font->GetFontBaked(final_size) : NULL;
g.FontBakedScale = (g.Font != NULL && window != NULL) ? (g.FontSize / g.FontBaked->Size) : 0.0f; g.FontBakedScale = (g.FontBaked != NULL) ? (g.FontSize / g.FontBaked->Size) : 0.0f;
g.DrawListSharedData.FontScale = g.FontBakedScale; g.DrawListSharedData.FontScale = g.FontBakedScale;
} }
@@ -10936,15 +10998,21 @@ void ImGui::UpdateMouseWheel()
LockWheelingWindow(NULL, 0.0f); LockWheelingWindow(NULL, 0.0f);
} }
ImVec2 wheel;
wheel.x = TestKeyOwner(ImGuiKey_MouseWheelX, ImGuiKeyOwner_NoOwner) ? g.IO.MouseWheelH : 0.0f;
wheel.y = TestKeyOwner(ImGuiKey_MouseWheelY, ImGuiKeyOwner_NoOwner) ? g.IO.MouseWheel : 0.0f;
//IMGUI_DEBUG_LOG("MouseWheel X:%.3f Y:%.3f\n", wheel_x, wheel_y);
ImGuiWindow* mouse_window = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow; ImGuiWindow* mouse_window = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow;
if (!mouse_window || mouse_window->Collapsed) if (!mouse_window || mouse_window->Collapsed)
return; return;
ImGuiID owner_id = mouse_window->ID;
ImVec2 wheel;
wheel.x = TestKeyOwner(ImGuiKey_MouseWheelX, owner_id) ? g.IO.MouseWheelH : 0.0f;
wheel.y = TestKeyOwner(ImGuiKey_MouseWheelY, owner_id) ? g.IO.MouseWheel : 0.0f;
//IMGUI_DEBUG_LOG("MouseWheel X:%.3f Y:%.3f\n", wheel_x, wheel_y);
if (g.WheelingWindow != NULL)
{
SetKeyOwner(ImGuiKey_MouseWheelX, owner_id);
SetKeyOwner(ImGuiKey_MouseWheelY, owner_id);
}
// Zoom / Scale window // Zoom / Scale window
// FIXME-OBSOLETE: This is an old feature, it still works but pretty much nobody is using it and may be best redesigned. // FIXME-OBSOLETE: This is an old feature, it still works but pretty much nobody is using it and may be best redesigned.
if (wheel.y != 0.0f && g.IO.KeyCtrl && g.IO.FontAllowUserScaling) if (wheel.y != 0.0f && g.IO.KeyCtrl && g.IO.FontAllowUserScaling)
@@ -11253,6 +11321,7 @@ bool ImGui::TestKeyOwner(ImGuiKey key, ImGuiID owner_id)
// - SetKeyOwner(..., None) : clears owner // - SetKeyOwner(..., None) : clears owner
// - SetKeyOwner(..., Any, !Lock) : illegal (assert) // - SetKeyOwner(..., Any, !Lock) : illegal (assert)
// - SetKeyOwner(..., Any or None, Lock) : set lock // - SetKeyOwner(..., Any or None, Lock) : set lock
// Ownership is automatically released on the frame after a release, see code in UpdateKeyboardInputs().
void ImGui::SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags) void ImGui::SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags)
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
@@ -11279,30 +11348,34 @@ void ImGui::SetKeyOwnersForKeyChord(ImGuiKeyChord key_chord, ImGuiID owner_id, I
if (key_chord & ~ImGuiMod_Mask_) { SetKeyOwner((ImGuiKey)(key_chord & ~ImGuiMod_Mask_), owner_id, flags); } if (key_chord & ~ImGuiMod_Mask_) { SetKeyOwner((ImGuiKey)(key_chord & ~ImGuiMod_Mask_), owner_id, flags); }
} }
// This is more or less equivalent to: // This is more or less equivalent to a fancier version of:
// if (IsItemHovered() || IsItemActive()) // if (IsItemHovered() || IsItemActive())
// SetKeyOwner(key, GetItemID()); // SetKeyOwner(key, GetItemID());
// Extensive uses of that (e.g. many calls for a single item) may want to manually perform the tests once and then call SetKeyOwner() multiple times. // Extensive uses of that (e.g. many calls for a single item) may want to manually perform the tests once and then call SetKeyOwner() multiple times.
// More advanced usage scenarios may want to call SetKeyOwner() manually based on different condition. // More advanced usage scenarios may want to call SetKeyOwner() manually based on different condition.
// Worth noting is that only one item can be hovered and only one item can be active, therefore this usage pattern doesn't need to bother with routing and priority. // Worth noting is that only one item can be hovered and only one item can be active, therefore this usage pattern doesn't need to bother with routing and priority.
void ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags) bool ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags)
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
ImGuiID id = g.LastItemData.ID; ImGuiID id = g.LastItemData.ID;
if (id == 0 || (g.HoveredId != id && g.ActiveId != id)) if (id == 0 || (g.HoveredId != id && g.ActiveId != id))
return; return false;
if ((flags & ImGuiInputFlags_CondMask_) == 0) if ((flags & ImGuiInputFlags_CondMask_) == 0)
flags |= ImGuiInputFlags_CondDefault_; flags |= ImGuiInputFlags_CondDefault_;
if ((g.HoveredId == id && (flags & ImGuiInputFlags_CondHovered)) || (g.ActiveId == id && (flags & ImGuiInputFlags_CondActive))) if ((g.HoveredId == id && (flags & ImGuiInputFlags_CondHovered)) || (g.ActiveId == id && (flags & ImGuiInputFlags_CondActive)))
{ {
IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetItemKeyOwner) == 0); // Passing flags not supported by this function! IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetItemKeyOwner) == 0); // Passing flags not supported by this function!
if (!TestKeyOwner(key, id))
return false;
SetKeyOwner(key, id, flags & ~ImGuiInputFlags_CondMask_); SetKeyOwner(key, id, flags & ~ImGuiInputFlags_CondMask_);
return true;
} }
return false;
} }
void ImGui::SetItemKeyOwner(ImGuiKey key) bool ImGui::SetItemKeyOwner(ImGuiKey key)
{ {
SetItemKeyOwner(key, ImGuiInputFlags_None); return SetItemKeyOwner(key, ImGuiInputFlags_None);
} }
// This is the only public API until we expose owner_id versions of the API as replacements. // This is the only public API until we expose owner_id versions of the API as replacements.
@@ -11443,7 +11516,8 @@ bool ImGui::DebugCheckVersionAndDataLayout(const char* version, size_t sz_io, si
// to extend contents size of our parent container (e.g. window contents size, which is used for auto-resizing // to extend contents size of our parent container (e.g. window contents size, which is used for auto-resizing
// windows, table column contents size used for auto-resizing columns, group size). // windows, table column contents size used for auto-resizing columns, group size).
// This was causing issues and ambiguities and we needed to retire that. // This was causing issues and ambiguities and we needed to retire that.
// From 1.89, extending contents size boundaries REQUIRES AN ITEM TO BE SUBMITTED. // 2022/08/05 (1.89): extending contents size boundaries REQUIRES AN ITEM TO BE SUBMITTED. However we gated the new logic behind a '#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS' block.
// 2025/06/25 (1.92): removed the legacy path and turned into an assert. It was a mistake that there was a #ifndef before: our obsolescence schedule gets pushed back a bit more :(
// //
// Previously this would make the window content size ~200x200: // Previously this would make the window content size ~200x200:
// Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); // NOT OK ANYMORE // Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); // NOT OK ANYMORE
@@ -13102,6 +13176,17 @@ bool ImGui::BeginPopupMenuEx(ImGuiID id, const char* label, ImGuiWindowFlags ext
return false; return false;
} }
// As we bypass BeginChild(), set ImGuiChildFlags_AlwaysAutoResize as it is checked independently from ImGuiWindowFlags_AlwaysAutoResize for now (see #9355)
// Ideally we should remove setting ImGuiWindowFlags_AlwaysAutoResize in BeginChild().
if ((extra_window_flags & ImGuiWindowFlags_ChildWindow) && (extra_window_flags & ImGuiWindowFlags_AlwaysAutoResize))
{
if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasChildFlags)
g.NextWindowData.ChildFlags |= ImGuiChildFlags_AlwaysAutoResize;
else
g.NextWindowData.ChildFlags = ImGuiChildFlags_AlwaysAutoResize;
g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasChildFlags;
}
char name[128]; char name[128];
IM_ASSERT(extra_window_flags & ImGuiWindowFlags_ChildMenu); IM_ASSERT(extra_window_flags & ImGuiWindowFlags_ChildMenu);
ImFormatString(name, IM_COUNTOF(name), "%s###Menu_%02d", label, g.BeginMenuDepth); // Recycle windows based on depth ImFormatString(name, IM_COUNTOF(name), "%s###Menu_%02d", label, g.BeginMenuDepth); // Recycle windows based on depth
@@ -13174,10 +13259,13 @@ void ImGui::EndPopup()
NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags_LoopY); NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags_LoopY);
// Child-popups don't need to be laid out // Child-popups don't need to be laid out
const ImGuiID backup_within_end_popup_id = g.WithinEndPopupID;
const ImGuiID backup_within_end_child_id = g.WithinEndChildID; const ImGuiID backup_within_end_child_id = g.WithinEndChildID;
g.WithinEndPopupID = window->ID;
if (window->Flags & ImGuiWindowFlags_ChildWindow) if (window->Flags & ImGuiWindowFlags_ChildWindow)
g.WithinEndChildID = window->ID; g.WithinEndChildID = window->ID;
End(); End();
g.WithinEndPopupID = backup_within_end_popup_id;
g.WithinEndChildID = backup_within_end_child_id; g.WithinEndChildID = backup_within_end_child_id;
} }
@@ -14025,7 +14113,7 @@ static void ImGui::NavProcessItem()
const ImGuiID id = g.LastItemData.ID; const ImGuiID id = g.LastItemData.ID;
const ImGuiItemFlags item_flags = g.LastItemData.ItemFlags; const ImGuiItemFlags item_flags = g.LastItemData.ItemFlags;
// When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221, #8816) // When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221, #8816, #7994)
ImRect nav_bb = g.LastItemData.NavRect; ImRect nav_bb = g.LastItemData.NavRect;
if (window->DC.NavIsScrollPushableX == false) if (window->DC.NavIsScrollPushableX == false)
{ {
@@ -14428,13 +14516,13 @@ static void ImGui::NavUpdate()
// FIXME-NAV: Now that keys are separated maybe we can get rid of NavInputSource? // FIXME-NAV: Now that keys are separated maybe we can get rid of NavInputSource?
const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
const ImGuiKey nav_gamepad_keys_to_change_source[] = { ImGuiKey_GamepadFaceRight, ImGuiKey_GamepadFaceLeft, ImGuiKey_GamepadFaceUp, ImGuiKey_GamepadFaceDown, ImGuiKey_GamepadDpadRight, ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadDpadDown }; const ImGuiKey nav_gamepad_keys_to_change_source[] = { ImGuiKey_GamepadFaceRight, ImGuiKey_GamepadFaceLeft, ImGuiKey_GamepadFaceUp, ImGuiKey_GamepadFaceDown, ImGuiKey_GamepadDpadRight, ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadDpadDown };
if (nav_gamepad_active) if (nav_gamepad_active && g.NavInputSource != ImGuiInputSource_Gamepad)
for (ImGuiKey key : nav_gamepad_keys_to_change_source) for (ImGuiKey key : nav_gamepad_keys_to_change_source)
if (IsKeyDown(key)) if (IsKeyDown(key))
g.NavInputSource = ImGuiInputSource_Gamepad; g.NavInputSource = ImGuiInputSource_Gamepad;
const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0;
const ImGuiKey nav_keyboard_keys_to_change_source[] = { ImGuiKey_Space, ImGuiKey_Enter, ImGuiKey_Escape, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow }; const ImGuiKey nav_keyboard_keys_to_change_source[] = { ImGuiKey_Space, ImGuiKey_Enter, ImGuiKey_Escape, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow };
if (nav_keyboard_active) if (nav_keyboard_active && g.NavInputSource != ImGuiInputSource_Keyboard)
for (ImGuiKey key : nav_keyboard_keys_to_change_source) for (ImGuiKey key : nav_keyboard_keys_to_change_source)
if (IsKeyDown(key)) if (IsKeyDown(key))
g.NavInputSource = ImGuiInputSource_Keyboard; g.NavInputSource = ImGuiInputSource_Keyboard;
@@ -15807,7 +15895,7 @@ const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDrop
IM_ASSERT(viewport != NULL); IM_ASSERT(viewport != NULL);
ImRect bb = g.DragDropTargetRect; ImRect bb = g.DragDropTargetRect;
bb.Expand(-3.5f); bb.Expand(-3.5f);
RenderDragDropTargetRectEx(GetForegroundDrawList(viewport), bb); RenderDragDropTargetRectEx(GetForegroundDrawList(viewport), bb, g.Style.DragDropTargetRounding);
} }
else if (draw_target_rect) else if (draw_target_rect)
{ {
@@ -15838,16 +15926,16 @@ void ImGui::RenderDragDropTargetRectForItem(const ImRect& bb)
bool push_clip_rect = !window->ClipRect.Contains(bb_display); bool push_clip_rect = !window->ClipRect.Contains(bb_display);
if (push_clip_rect) if (push_clip_rect)
window->DrawList->PushClipRectFullScreen(); window->DrawList->PushClipRectFullScreen();
RenderDragDropTargetRectEx(window->DrawList, bb_display); RenderDragDropTargetRectEx(window->DrawList, bb_display, g.Style.DragDropTargetRounding);
if (push_clip_rect) if (push_clip_rect)
window->DrawList->PopClipRect(); window->DrawList->PopClipRect();
} }
void ImGui::RenderDragDropTargetRectEx(ImDrawList* draw_list, const ImRect& bb) void ImGui::RenderDragDropTargetRectEx(ImDrawList* draw_list, const ImRect& bb, float rounding)
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
draw_list->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTargetBg), g.Style.DragDropTargetRounding, 0); draw_list->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTargetBg), rounding, 0);
draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTarget), g.Style.DragDropTargetRounding, 0, g.Style.DragDropTargetBorderSize); draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTarget), rounding, g.Style.DragDropTargetBorderSize);
} }
const ImGuiPayload* ImGui::GetDragDropPayload() const ImGuiPayload* ImGui::GetDragDropPayload()
@@ -16576,6 +16664,7 @@ void ImGuiPlatformIO::ClearRendererHandlers()
Renderer_CreateWindow = Renderer_DestroyWindow = NULL; Renderer_CreateWindow = Renderer_DestroyWindow = NULL;
Renderer_SetWindowSize = NULL; Renderer_SetWindowSize = NULL;
Renderer_RenderWindow = Renderer_SwapBuffers = NULL; Renderer_RenderWindow = Renderer_SwapBuffers = NULL;
DrawCallback_ResetRenderState = DrawCallback_SetSamplerLinear = DrawCallback_SetSamplerNearest = NULL;
} }
ImGuiViewport* ImGui::GetMainViewport() ImGuiViewport* ImGui::GetMainViewport()
@@ -16691,7 +16780,7 @@ static bool ImGui::UpdateTryMergeWindowIntoHostViewport(ImGuiWindow* window, ImG
for (ImGuiViewportP* viewport_obstructing : g.Viewports) for (ImGuiViewportP* viewport_obstructing : g.Viewports)
{ {
if (viewport_obstructing == viewport_src || viewport_obstructing == viewport_dst) if (viewport_obstructing == viewport_src || viewport_obstructing == viewport_dst || !viewport_obstructing->PlatformWindowCreated)
continue; continue;
if (viewport_obstructing->GetMainRect().Overlaps(window->Rect())) if (viewport_obstructing->GetMainRect().Overlaps(window->Rect()))
if (IsViewportAbove(viewport_obstructing, viewport_dst)) if (IsViewportAbove(viewport_obstructing, viewport_dst))
@@ -17366,6 +17455,8 @@ void ImGui::WindowSyncOwnedViewport(ImGuiWindow* window, ImGuiWindow* parent_win
window->Viewport->Flags = viewport_flags; window->Viewport->Flags = viewport_flags;
window->Viewport->PlatformIconData = window->WindowClass.PlatformIconData;
// Update parent viewport ID // Update parent viewport ID
// (the !IsFallbackWindow test mimic the one done in WindowSelectViewport()) // (the !IsFallbackWindow test mimic the one done in WindowSelectViewport())
if (window->WindowClass.ParentViewportId != (ImGuiID)-1) if (window->WindowClass.ParentViewportId != (ImGuiID)-1)
@@ -18883,10 +18974,13 @@ static void ImGui::DockNodeUpdateFlagsAndCollapse(ImGuiDockNode* node)
node->WantHiddenTabBarToggle = false; node->WantHiddenTabBarToggle = false;
// Apply toggles at a single point of the frame (here!) // Apply toggles at a single point of the frame (here!)
const ImGuiDockNodeFlags prev_local_flags = node->LocalFlags;
if (node->Windows.Size > 1) if (node->Windows.Size > 1)
node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_HiddenTabBar); node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_HiddenTabBar);
else if (node->WantHiddenTabBarToggle) else if (node->WantHiddenTabBarToggle)
node->SetLocalFlags(node->LocalFlags ^ ImGuiDockNodeFlags_HiddenTabBar); node->SetLocalFlags(node->LocalFlags ^ ImGuiDockNodeFlags_HiddenTabBar);
if ((node->LocalFlags ^ prev_local_flags) & ImGuiDockNodeFlags_SavedFlagsMask_)
MarkIniSettingsDirty(); // Bit flaky to only do this here. Perhaps compare node flags every frame? #9380
node->WantHiddenTabBarToggle = false; node->WantHiddenTabBarToggle = false;
DockNodeUpdateVisibleFlag(node); DockNodeUpdateVisibleFlag(node);
@@ -21987,7 +22081,7 @@ void ImGui::DebugRenderKeyboardPreview(ImDrawList* draw_list)
draw_list->AddRect(key_min, key_max, IM_COL32(24, 24, 24, 255), key_rounding); draw_list->AddRect(key_min, key_max, IM_COL32(24, 24, 24, 255), key_rounding);
ImVec2 face_min = ImVec2(key_min.x + key_face_pos.x, key_min.y + key_face_pos.y); ImVec2 face_min = ImVec2(key_min.x + key_face_pos.x, key_min.y + key_face_pos.y);
ImVec2 face_max = ImVec2(face_min.x + key_face_size.x, face_min.y + key_face_size.y); ImVec2 face_max = ImVec2(face_min.x + key_face_size.x, face_min.y + key_face_size.y);
draw_list->AddRect(face_min, face_max, IM_COL32(193, 193, 193, 255), key_face_rounding, ImDrawFlags_None, 2.0f); draw_list->AddRect(face_min, face_max, IM_COL32(193, 193, 193, 255), key_face_rounding, 2.0f);
draw_list->AddRectFilled(face_min, face_max, IM_COL32(252, 252, 252, 255), key_face_rounding); draw_list->AddRectFilled(face_min, face_max, IM_COL32(252, 252, 252, 255), key_face_rounding);
ImVec2 label_min = ImVec2(key_min.x + key_label_pos.x, key_min.y + key_label_pos.y); ImVec2 label_min = ImVec2(key_min.x + key_label_pos.x, key_min.y + key_label_pos.y);
draw_list->AddText(label_min, IM_COL32(64, 64, 64, 255), key_data->Label); draw_list->AddText(label_min, IM_COL32(64, 64, 64, 255), key_data->Label);
@@ -22410,7 +22504,7 @@ void ImGui::ShowMetricsWindow(bool* p_open)
BulletText("Table 0x%08X (%d columns, in '%s')", table->ID, table->ColumnsCount, table->OuterWindow->Name); BulletText("Table 0x%08X (%d columns, in '%s')", table->ID, table->ColumnsCount, table->OuterWindow->Name);
if (IsItemHovered()) if (IsItemHovered())
GetForegroundDrawList(table->OuterWindow)->AddRect(table->OuterRect.Min - ImVec2(1, 1), table->OuterRect.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); GetForegroundDrawList(table->OuterWindow)->AddRect(table->OuterRect.Min - ImVec2(1, 1), table->OuterRect.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 2.0f);
Indent(); Indent();
char buf[128]; char buf[128];
for (int rect_n = 0; rect_n < TRT_Count; rect_n++) for (int rect_n = 0; rect_n < TRT_Count; rect_n++)
@@ -22425,7 +22519,7 @@ void ImGui::ShowMetricsWindow(bool* p_open)
ImFormatString(buf, IM_COUNTOF(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) Col %d %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), column_n, trt_rects_names[rect_n]); ImFormatString(buf, IM_COUNTOF(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) Col %d %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), column_n, trt_rects_names[rect_n]);
Selectable(buf); Selectable(buf);
if (IsItemHovered()) if (IsItemHovered())
GetForegroundDrawList(table->OuterWindow)->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); GetForegroundDrawList(table->OuterWindow)->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 2.0f);
} }
} }
else else
@@ -22434,7 +22528,7 @@ void ImGui::ShowMetricsWindow(bool* p_open)
ImFormatString(buf, IM_COUNTOF(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), trt_rects_names[rect_n]); ImFormatString(buf, IM_COUNTOF(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), trt_rects_names[rect_n]);
Selectable(buf); Selectable(buf);
if (IsItemHovered()) if (IsItemHovered())
GetForegroundDrawList(table->OuterWindow)->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); GetForegroundDrawList(table->OuterWindow)->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 2.0f);
} }
} }
Unindent(); Unindent();
@@ -22922,7 +23016,7 @@ void ImGui::ShowMetricsWindow(bool* p_open)
ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, column_n); ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, column_n);
ImU32 col = (table->HoveredColumnBody == column_n) ? IM_COL32(255, 255, 128, 255) : IM_COL32(255, 0, 128, 255); ImU32 col = (table->HoveredColumnBody == column_n) ? IM_COL32(255, 255, 128, 255) : IM_COL32(255, 0, 128, 255);
float thickness = (table->HoveredColumnBody == column_n) ? 3.0f : 1.0f; float thickness = (table->HoveredColumnBody == column_n) ? 3.0f : 1.0f;
draw_list->AddRect(r.Min, r.Max, col, 0.0f, 0, thickness); draw_list->AddRect(r.Min, r.Max, col, 0.0f, thickness);
} }
} }
else else
@@ -23199,7 +23293,7 @@ void ImGui::DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, con
{ {
ImDrawListFlags backup_flags = fg_draw_list->Flags; ImDrawListFlags backup_flags = fg_draw_list->Flags;
fg_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. fg_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles.
fg_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); fg_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), 1.0f, ImDrawFlags_Closed);
fg_draw_list->Flags = backup_flags; fg_draw_list->Flags = backup_flags;
} }
} }
@@ -23227,7 +23321,7 @@ void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, co
for (int n = 0; n < 3; n++, idx_n++) for (int n = 0; n < 3; n++, idx_n++)
vtxs_rect.Add((triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos)); vtxs_rect.Add((triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos));
if (show_mesh) if (show_mesh)
out_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); // In yellow: mesh triangles out_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), 1.0f, ImDrawFlags_Closed); // In yellow: mesh triangles
} }
// Draw bounding boxes // Draw bounding boxes
if (show_aabb) if (show_aabb)
@@ -23508,8 +23602,8 @@ void ImGui::DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label)
{ {
ImDrawList* draw_list = GetForegroundDrawList(tab_bar->Window); ImDrawList* draw_list = GetForegroundDrawList(tab_bar->Window);
draw_list->AddRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, IM_COL32(255, 255, 0, 255)); draw_list->AddRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, IM_COL32(255, 255, 0, 255));
draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); draw_list->AddLineV(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Min.y, tab_bar->BarRect.Max.y, IM_COL32(0, 255, 0, 255));
draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); draw_list->AddLineV(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Min.y, tab_bar->BarRect.Max.y, IM_COL32(0, 255, 0, 255));
} }
if (open) if (open)
{ {
@@ -23871,8 +23965,8 @@ void ImGui::DebugDrawCursorPos(ImU32 col)
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow; ImGuiWindow* window = g.CurrentWindow;
ImVec2 pos = window->DC.CursorPos; ImVec2 pos = window->DC.CursorPos;
window->DrawList->AddLine(ImVec2(pos.x, pos.y - 3.0f), ImVec2(pos.x, pos.y + 4.0f), col, 1.0f); window->DrawList->AddLineV(pos.x, pos.y - 3.0f, pos.y + 4.0f, col, 1.0f);
window->DrawList->AddLine(ImVec2(pos.x - 3.0f, pos.y), ImVec2(pos.x + 4.0f, pos.y), col, 1.0f); window->DrawList->AddLineH(pos.x - 3.0f, pos.x + 4.0f, pos.y, col, 1.0f);
} }
// Draw a 10px wide rectangle around CurposPos.x using Line Y1/Y2 in current window's DrawList // Draw a 10px wide rectangle around CurposPos.x using Line Y1/Y2 in current window's DrawList
@@ -23883,9 +23977,9 @@ void ImGui::DebugDrawLineExtents(ImU32 col)
float curr_x = window->DC.CursorPos.x; float curr_x = window->DC.CursorPos.x;
float line_y1 = (window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y); float line_y1 = (window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y);
float line_y2 = line_y1 + (window->DC.IsSameLine ? window->DC.PrevLineSize.y : window->DC.CurrLineSize.y); float line_y2 = line_y1 + (window->DC.IsSameLine ? window->DC.PrevLineSize.y : window->DC.CurrLineSize.y);
window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y1), ImVec2(curr_x + 5.0f, line_y1), col, 1.0f); window->DrawList->AddLineH(curr_x - 5.0f, curr_x + 5.0f, line_y1, col, 1.0f);
window->DrawList->AddLine(ImVec2(curr_x - 0.5f, line_y1), ImVec2(curr_x - 0.5f, line_y2), col, 1.0f); window->DrawList->AddLineV(curr_x - 0.5f, line_y1, line_y2, col, 1.0f);
window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y2), ImVec2(curr_x + 5.0f, line_y2), col, 1.0f); window->DrawList->AddLineH(curr_x - 5.0f, curr_x + 5.0f, line_y2, col, 1.0f);
} }
// Draw last item rect in ForegroundDrawList (so it is always visible) // Draw last item rect in ForegroundDrawList (so it is always visible)
@@ -24255,7 +24349,7 @@ void ImGui::ShowFontSelector(const char* label)
"- Load additional fonts with io.Fonts->AddFontXXX() functions.\n" "- Load additional fonts with io.Fonts->AddFontXXX() functions.\n"
"- The font atlas is built when calling io.Fonts->GetTexDataAsXXXX() or io.Fonts->Build().\n" "- The font atlas is built when calling io.Fonts->GetTexDataAsXXXX() or io.Fonts->Build().\n"
"- Read FAQ and docs/FONTS.md for more details.\n" "- Read FAQ and docs/FONTS.md for more details.\n"
"- If you need to add/remove fonts at runtime (e.g. for DPI change), do it before calling NewFrame()."); "- Legacy backend: if you need to add/remove fonts at runtime (e.g. for DPI change), do it before calling NewFrame().");
} }
#endif // #if !defined(IMGUI_DISABLE_DEMO_WINDOWS) || !defined(IMGUI_DISABLE_DEBUG_TOOLS) #endif // #if !defined(IMGUI_DISABLE_DEMO_WINDOWS) || !defined(IMGUI_DISABLE_DEBUG_TOOLS)
+80 -48
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7 // dear imgui, v1.92.9 WIP
// (headers) // (headers)
// Help: // Help:
@@ -29,8 +29,8 @@
// Library Version // Library Version
// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345')
#define IMGUI_VERSION "1.92.7" #define IMGUI_VERSION "1.92.9 WIP"
#define IMGUI_VERSION_NUM 19270 #define IMGUI_VERSION_NUM 19282
#define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000 #define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000
#define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198 #define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198
#define IMGUI_HAS_VIEWPORT // In 'docking' WIP branch. #define IMGUI_HAS_VIEWPORT // In 'docking' WIP branch.
@@ -621,7 +621,8 @@ namespace ImGui
IMGUI_API ImGuiID GetID(int int_id); IMGUI_API ImGuiID GetID(int int_id);
// Widgets: Text // Widgets: Text
IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. // - Note that all functions taking format strings in the API may be passed ("%s", text) or ("%.*s", text_len, text): which will automatically bypass the formatter.
IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Practically equivalent to 'Text("%s", text)' but doesn't require null terminated string if 'text_end' is specified.
IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // formatted text IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // formatted text
IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1); IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1);
IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor();
@@ -845,7 +846,7 @@ namespace ImGui
// Popups, Modals // Popups, Modals
// - They block normal mouse hovering detection (and therefore most mouse interactions) behind them. // - They block normal mouse hovering detection (and therefore most mouse interactions) behind them.
// - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. // - If not modal: they can be closed by clicking anywhere outside them, or by pressing Escape (call 'Shortcut(ImGuiKey_Escape)' to claim a higher-priority shortcut).
// - Their visibility state (~bool) is held internally instead of being held by the programmer as we are used to with regular Begin*() calls. // - Their visibility state (~bool) is held internally instead of being held by the programmer as we are used to with regular Begin*() calls.
// - The 3 properties above are related: we need to retain popup visibility state in the library because popups may be closed as any time. // - The 3 properties above are related: we need to retain popup visibility state in the library because popups may be closed as any time.
// - You can bypass the hovering restriction by using ImGuiHoveredFlags_AllowWhenBlockedByPopup when calling IsItemHovered() or IsWindowHovered(). // - You can bypass the hovering restriction by using ImGuiHoveredFlags_AllowWhenBlockedByPopup when calling IsItemHovered() or IsWindowHovered().
@@ -1128,10 +1129,11 @@ namespace ImGui
// Inputs Utilities: Key/Input Ownership [BETA] // Inputs Utilities: Key/Input Ownership [BETA]
// - One common use case would be to allow your items to disable standard inputs behaviors such // - One common use case would be to allow your items to disable standard inputs behaviors such
// as Tab or Alt key handling, Mouse Wheel scrolling, etc. // as Tab or Alt key handling, Mouse Wheel scrolling, etc.
// e.g. Button(...); SetItemKeyOwner(ImGuiKey_MouseWheelY); to make hovering/activating a button disable wheel for scrolling. // e.g. `Button(...); if (SetItemKeyOwner(ImGuiKey_MouseWheelY)) { ... }` to make hovering/activating a button disable wheel for scrolling.
// - Reminder ImGuiKey enum include access to mouse buttons and gamepad, so key ownership can apply to them. // - Reminder ImGuiKey enum include access to mouse buttons and gamepad, so key ownership can apply to them.
// - The return value of SetItemKeyOwner() says if ownership has been requested for the item, which is a shortcut to calling yet non-public TestKeyOwner() function.
// - Many related features are still in imgui_internal.h. For instance, most IsKeyXXX()/IsMouseXXX() functions have an owner-id-aware version. // - Many related features are still in imgui_internal.h. For instance, most IsKeyXXX()/IsMouseXXX() functions have an owner-id-aware version.
IMGUI_API void SetItemKeyOwner(ImGuiKey key); // Set key owner to last item ID if it is hovered or active. Equivalent to 'if (IsItemHovered() || IsItemActive()) { SetKeyOwner(key, GetItemID());'. IMGUI_API bool SetItemKeyOwner(ImGuiKey key); // Set key owner to last item ID if it is hovered or active. Return true when ownership has been set. Roughly equivalent to 'if (TestKeyOwner(key, GetItemID()) && (IsItemHovered() || IsItemActive())) { SetKeyOwner(key, GetItemID());'.
// Inputs Utilities: Mouse // Inputs Utilities: Mouse
// - To refer to a mouse button, you may use named enums in your code e.g. ImGuiMouseButton_Left, ImGuiMouseButton_Right. // - To refer to a mouse button, you may use named enums in your code e.g. ImGuiMouseButton_Left, ImGuiMouseButton_Right.
@@ -1376,7 +1378,7 @@ enum ImGuiTreeNodeFlags_
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
ImGuiTreeNodeFlags_NavLeftJumpsBackHere = ImGuiTreeNodeFlags_NavLeftJumpsToParent, // Renamed in 1.92.0 ImGuiTreeNodeFlags_NavLeftJumpsBackHere = ImGuiTreeNodeFlags_NavLeftJumpsToParent, // Renamed in 1.92.0
ImGuiTreeNodeFlags_SpanTextWidth = ImGuiTreeNodeFlags_SpanLabelWidth, // Renamed in 1.90.7 //ImGuiTreeNodeFlags_SpanTextWidth = ImGuiTreeNodeFlags_SpanLabelWidth, // Renamed in 1.90.7
//ImGuiTreeNodeFlags_AllowItemOverlap = ImGuiTreeNodeFlags_AllowOverlap, // Renamed in 1.89.7 //ImGuiTreeNodeFlags_AllowItemOverlap = ImGuiTreeNodeFlags_AllowOverlap, // Renamed in 1.89.7
#endif #endif
}; };
@@ -1448,7 +1450,7 @@ enum ImGuiTabBarFlags_
ImGuiTabBarFlags_DrawSelectedOverline = 1 << 6, // Draw selected overline markers over selected tab ImGuiTabBarFlags_DrawSelectedOverline = 1 << 6, // Draw selected overline markers over selected tab
// Fitting/Resize policy // Fitting/Resize policy
ImGuiTabBarFlags_FittingPolicyMixed = 1 << 7, // Shrink down tabs when they don't fit, until width is style.TabMinWidthShrink, then enable scrolling buttons. ImGuiTabBarFlags_FittingPolicyMixed = 1 << 7, // Shrink down tabs when they don't fit, until width is style.TabMinWidthShrink, then enable scrolling. Setting TabMinWidthShrink to FLT_MAX makes this behave like ImGuiTabBarFlags_FittingPolicyScroll.
ImGuiTabBarFlags_FittingPolicyShrink = 1 << 8, // Shrink down tabs when they don't fit ImGuiTabBarFlags_FittingPolicyShrink = 1 << 8, // Shrink down tabs when they don't fit
ImGuiTabBarFlags_FittingPolicyScroll = 1 << 9, // Enable scrolling buttons when tabs don't fit ImGuiTabBarFlags_FittingPolicyScroll = 1 << 9, // Enable scrolling buttons when tabs don't fit
ImGuiTabBarFlags_FittingPolicyMask_ = ImGuiTabBarFlags_FittingPolicyMixed | ImGuiTabBarFlags_FittingPolicyShrink | ImGuiTabBarFlags_FittingPolicyScroll, ImGuiTabBarFlags_FittingPolicyMask_ = ImGuiTabBarFlags_FittingPolicyMixed | ImGuiTabBarFlags_FittingPolicyShrink | ImGuiTabBarFlags_FittingPolicyScroll,
@@ -1813,7 +1815,7 @@ enum ImGuiBackendFlags_
ImGuiBackendFlags_RendererHasViewports = 1 << 10, // Backend Renderer supports multiple viewports. ImGuiBackendFlags_RendererHasViewports = 1 << 10, // Backend Renderer supports multiple viewports.
ImGuiBackendFlags_PlatformHasViewports = 1 << 11, // Backend Platform supports multiple viewports. ImGuiBackendFlags_PlatformHasViewports = 1 << 11, // Backend Platform supports multiple viewports.
ImGuiBackendFlags_HasMouseHoveredViewport=1 << 12, // Backend Platform supports calling io.AddMouseViewportEvent() with the viewport under the mouse. IF POSSIBLE, ignore viewports with the ImGuiViewportFlags_NoInputs flag (Win32 backend, GLFW 3.30+ backend can do this, SDL backend cannot). If this cannot be done, Dear ImGui needs to use a flawed heuristic to find the viewport under. ImGuiBackendFlags_HasMouseHoveredViewport=1 << 12, // Backend Platform supports calling io.AddMouseViewportEvent() with the viewport under the mouse. IF POSSIBLE, ignore viewports with the ImGuiViewportFlags_NoInputs flag (Win32 backend, GLFW 3.30+ backend can do this, SDL backend cannot). If this cannot be done, Dear ImGui needs to use a flawed heuristic to find the viewport under.
ImGuiBackendFlags_HasParentViewport = 1 << 13, // Backend Platform supports honoring viewport->ParentViewport/ParentViewportId value, by applying the corresponding parent/child relation at the Platform level. ImGuiBackendFlags_HasParentViewport = 1 << 13, // Backend Platform supports honoring viewport->ParentViewport/ParentViewportId value, by applying the corresponding parent/child relationship at the Platform level. Child windows always appear in front of their parent window.
}; };
// Enumeration for PushStyleColor() / PopStyleColor() // Enumeration for PushStyleColor() / PopStyleColor()
@@ -1838,6 +1840,7 @@ enum ImGuiCol_
ImGuiCol_ScrollbarGrabHovered, ImGuiCol_ScrollbarGrabHovered,
ImGuiCol_ScrollbarGrabActive, ImGuiCol_ScrollbarGrabActive,
ImGuiCol_CheckMark, // Checkbox tick and RadioButton circle ImGuiCol_CheckMark, // Checkbox tick and RadioButton circle
ImGuiCol_CheckboxSelectedBg, // Checkbox background when Selected, otherwise use FrameBg
ImGuiCol_SliderGrab, ImGuiCol_SliderGrab,
ImGuiCol_SliderGrabActive, ImGuiCol_SliderGrabActive,
ImGuiCol_Button, ImGuiCol_Button,
@@ -1937,6 +1940,7 @@ enum ImGuiStyleVar_
ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign
ImGuiStyleVar_TreeLinesSize, // float TreeLinesSize ImGuiStyleVar_TreeLinesSize, // float TreeLinesSize
ImGuiStyleVar_TreeLinesRounding, // float TreeLinesRounding ImGuiStyleVar_TreeLinesRounding, // float TreeLinesRounding
ImGuiStyleVar_DragDropTargetRounding, // float DragDropTargetRounding
ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign
ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign
ImGuiStyleVar_SeparatorSize, // float SeparatorSize ImGuiStyleVar_SeparatorSize, // float SeparatorSize
@@ -2405,7 +2409,7 @@ struct ImGuiStyle
float TabBorderSize; // Thickness of border around tabs. float TabBorderSize; // Thickness of border around tabs.
float TabMinWidthBase; // Minimum tab width, to make tabs larger than their contents. TabBar buttons are not affected. float TabMinWidthBase; // Minimum tab width, to make tabs larger than their contents. TabBar buttons are not affected.
float TabMinWidthShrink; // Minimum tab width after shrinking, when using ImGuiTabBarFlags_FittingPolicyMixed policy. float TabMinWidthShrink; // Minimum tab width after shrinking, when using ImGuiTabBarFlags_FittingPolicyMixed policy.
float TabCloseButtonMinWidthSelected; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. float TabCloseButtonMinWidthSelected; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never shrink, will behave like ImGuiTabBarFlags_FittingPolicyScroll.
float TabCloseButtonMinWidthUnselected; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected. float TabCloseButtonMinWidthUnselected; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected.
float TabBarBorderSize; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. float TabBarBorderSize; // Thickness of tab-bar separator, which takes on the tab active color to denote focus.
float TabBarOverlineSize; // Thickness of tab-bar overline, which highlights the selected tab-bar. float TabBarOverlineSize; // Thickness of tab-bar overline, which highlights the selected tab-bar.
@@ -2414,14 +2418,15 @@ struct ImGuiStyle
ImGuiTreeNodeFlags TreeLinesFlags; // Default way to draw lines connecting TreeNode hierarchy. ImGuiTreeNodeFlags_DrawLinesNone or ImGuiTreeNodeFlags_DrawLinesFull or ImGuiTreeNodeFlags_DrawLinesToNodes. ImGuiTreeNodeFlags TreeLinesFlags; // Default way to draw lines connecting TreeNode hierarchy. ImGuiTreeNodeFlags_DrawLinesNone or ImGuiTreeNodeFlags_DrawLinesFull or ImGuiTreeNodeFlags_DrawLinesToNodes.
float TreeLinesSize; // Thickness of outlines when using ImGuiTreeNodeFlags_DrawLines. float TreeLinesSize; // Thickness of outlines when using ImGuiTreeNodeFlags_DrawLines.
float TreeLinesRounding; // Radius of lines connecting child nodes to the vertical line. float TreeLinesRounding; // Radius of lines connecting child nodes to the vertical line.
float DragDropTargetRounding; // Radius of the drag and drop target frame. float DragDropTargetRounding; // Radius of the drag and drop target frame. When <0.0f: use FrameRounding.
float DragDropTargetBorderSize; // Thickness of the drag and drop target border. float DragDropTargetBorderSize; // Thickness of the drag and drop target border.
float DragDropTargetPadding; // Size to expand the drag and drop target from actual target item size. float DragDropTargetPadding; // Size to expand the drag and drop target from actual target item size.
float ColorMarkerSize; // Size of R/G/B/A color markers for ColorEdit4() and for Drags/Sliders when using ImGuiSliderFlags_ColorMarkers. float ColorMarkerSize; // Size of R/G/B/A color markers for ColorEdit4() and for Drags/Sliders when using ImGuiSliderFlags_ColorMarkers.
ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right.
ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered).
ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line.
float SeparatorSize; // Thickness of border in Separator() float InputTextCursorSize; // Thickness of cursor/caret in InputText().
float SeparatorSize; // Thickness of border in Separator(). Must be >= 1.0f.
float SeparatorTextBorderSize; // Thickness of border in SeparatorText() float SeparatorTextBorderSize; // Thickness of border in SeparatorText()
ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center).
ImVec2 SeparatorTextPadding; // Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. ImVec2 SeparatorTextPadding; // Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y.
@@ -2453,7 +2458,7 @@ struct ImGuiStyle
// Functions // Functions
IMGUI_API ImGuiStyle(); IMGUI_API ImGuiStyle();
IMGUI_API void ScaleAllSizes(float scale_factor); // Scale all spacing/padding/thickness values. Do not scale fonts. IMGUI_API void ScaleAllSizes(float scale_factor); // Scale all spacing/padding/thickness values. Do not scale fonts. See comments in definition. Consider not calling this if your initial scale factor if <1.0.
// Obsolete names // Obsolete names
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
@@ -2521,10 +2526,11 @@ struct ImGuiIO
bool ConfigDockingTransparentPayload;// = false // [BETA] Make window or viewport transparent when docking and only display docking boxes on the target viewport. Useful if rendering of multiple viewport cannot be synced. Best used with ConfigViewportsNoAutoMerge. bool ConfigDockingTransparentPayload;// = false // [BETA] Make window or viewport transparent when docking and only display docking boxes on the target viewport. Useful if rendering of multiple viewport cannot be synced. Best used with ConfigViewportsNoAutoMerge.
// Viewport options (when ImGuiConfigFlags_ViewportsEnable is set) // Viewport options (when ImGuiConfigFlags_ViewportsEnable is set)
// (sorry for the amount of "NoXXXX" flags, which may be harder to reason about! may rework someday)
bool ConfigViewportsNoAutoMerge; // = false; // Set to make all floating imgui windows always create their own viewport. Otherwise, they are merged into the main host viewports when overlapping it. May also set ImGuiViewportFlags_NoAutoMerge on individual viewport. bool ConfigViewportsNoAutoMerge; // = false; // Set to make all floating imgui windows always create their own viewport. Otherwise, they are merged into the main host viewports when overlapping it. May also set ImGuiViewportFlags_NoAutoMerge on individual viewport.
bool ConfigViewportsNoTaskBarIcon; // = false // Disable default OS task bar icon flag for secondary viewports. When a viewport doesn't want a task bar icon, ImGuiViewportFlags_NoTaskBarIcon will be set on it. bool ConfigViewportsNoTaskBarIcon; // = false // Disable default OS task bar icon flag for secondary viewports. When a viewport doesn't want a task bar icon, ImGuiViewportFlags_NoTaskBarIcon will be set on it.
bool ConfigViewportsNoDecoration; // = true // Disable default OS window decoration flag for secondary viewports. When a viewport doesn't want window decorations, ImGuiViewportFlags_NoDecoration will be set on it. Enabling decoration can create subsequent issues at OS levels (e.g. minimum window size). bool ConfigViewportsNoDecoration; // = true // Disable default OS window decoration flag for secondary viewports. When a viewport doesn't want window decorations, ImGuiViewportFlags_NoDecoration will be set on it. Enabling decoration can create subsequent issues at OS levels (e.g. minimum window size).
bool ConfigViewportsNoDefaultParent; // = true // When false: set secondary viewports' ParentViewportId to main viewport ID by default. Expects the platform backend to setup a parent/child relationship between the OS windows based on this value. Some backend may ignore this. Set to true if you want viewports to automatically be parent of main viewport, otherwise all viewports will be top-level OS windows. bool ConfigViewportsNoDefaultParent; // = true // Disable setting OS window parent to main viewport by default. The platform backend is expected to honor `viewport->ParentViewportID` to setup a parent/child relationship between the OS windows (supported if ImGuiBackendFlags_HasParentViewport is set). When parented: child windows always appear in front of their parent. Set to false if you want viewports to automatically be parent of main viewport, otherwise all viewports will be top-level OS windows. Parent/child relationship may be set on a per-window basis using ImGuiWindowClass.
bool ConfigViewportsPlatformFocusSetsImGuiFocus;//= true // When a platform window is focused (e.g. using Alt+Tab, clicking Platform Title Bar), apply corresponding focus on imgui windows (may clear focus/active id from imgui windows location in other platform windows). In principle this is better enabled but we provide an opt-out, because some Linux window managers tend to eagerly focus windows (e.g. on mouse hover, or even a simple window pos/size change). bool ConfigViewportsPlatformFocusSetsImGuiFocus;//= true // When a platform window is focused (e.g. using Alt+Tab, clicking Platform Title Bar), apply corresponding focus on imgui windows (may clear focus/active id from imgui windows location in other platform windows). In principle this is better enabled but we provide an opt-out, because some Linux window managers tend to eagerly focus windows (e.g. on mouse hover, or even a simple window pos/size change).
// DPI/Scaling options // DPI/Scaling options
@@ -2719,7 +2725,7 @@ struct ImGuiIO
//void* ImeWindowHandle; // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. //void* ImeWindowHandle; // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning.
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
float FontGlobalScale; // Moved io.FontGlobalScale to style.FontScaleMain in 1.92 (June 2025) float FontGlobalScale; // Moved io.FontGlobalScale to style.FontScaleMain in 1.92.0 (June 2025)
// Legacy: before 1.91.1, clipboard functions were stored in ImGuiIO instead of ImGuiPlatformIO. // Legacy: before 1.91.1, clipboard functions were stored in ImGuiIO instead of ImGuiPlatformIO.
// As this is will affect all users of custom engines/backends, we are providing proper legacy redirection (will obsolete). // As this is will affect all users of custom engines/backends, we are providing proper legacy redirection (will obsolete).
@@ -2752,7 +2758,7 @@ struct ImGuiInputTextCallbackData
ImGuiInputTextFlags EventFlag; // One ImGuiInputTextFlags_Callback* // Read-only ImGuiInputTextFlags EventFlag; // One ImGuiInputTextFlags_Callback* // Read-only
ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only
void* UserData; // What user passed to InputText() // Read-only void* UserData; // What user passed to InputText() // Read-only
ImGuiID ID; // Widget ID // Read-only ImGuiID ID; // Widget ID // Read-only
// Arguments for the different callback events // Arguments for the different callback events
// - During Resize callback, Buf will be same as your input buffer. // - During Resize callback, Buf will be same as your input buffer.
@@ -2766,9 +2772,9 @@ struct ImGuiInputTextCallbackData
char* Buf; // Text buffer // Read-write // [Resize] Can replace pointer / [Completion,History,Always] Only write to pointed data, don't replace the actual pointer! char* Buf; // Text buffer // Read-write // [Resize] Can replace pointer / [Completion,History,Always] Only write to pointed data, don't replace the actual pointer!
int BufTextLen; // Text length (in bytes) // Read-write // [Resize,Completion,History,Always] Exclude zero-terminator storage. In C land: == strlen(some_text), in C++ land: string.length() int BufTextLen; // Text length (in bytes) // Read-write // [Resize,Completion,History,Always] Exclude zero-terminator storage. In C land: == strlen(some_text), in C++ land: string.length()
int BufSize; // Buffer size (in bytes) = capacity+1 // Read-only // [Resize,Completion,History,Always] Include zero-terminator storage. In C land: == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1 int BufSize; // Buffer size (in bytes) = capacity+1 // Read-only // [Resize,Completion,History,Always] Include zero-terminator storage. In C land: == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1
int CursorPos; // // Read-write // [Completion,History,Always] int CursorPos; // // Read-write // [Completion,History,Always,CharFilter]
int SelectionStart; // // Read-write // [Completion,History,Always] == to SelectionEnd when no selection int SelectionStart; // // Read-write // [Completion,History,Always,CharFilter] == to SelectionEnd when no selection
int SelectionEnd; // // Read-write // [Completion,History,Always] int SelectionEnd; // // Read-write // [Completion,History,Always,CharFilter]
// Helper functions for text manipulation. // Helper functions for text manipulation.
// Use those function to benefit from the CallbackResize behaviors. Calling those function reset the selection. // Use those function to benefit from the CallbackResize behaviors. Calling those function reset the selection.
@@ -2796,7 +2802,7 @@ struct ImGuiSizeCallbackData
// before we stabilize Docking features. Please be mindful if using this. // before we stabilize Docking features. Please be mindful if using this.
// Provide hints: // Provide hints:
// - To the platform backend via altered viewport flags (enable/disable OS decoration, OS task bar icons, etc.) // - To the platform backend via altered viewport flags (enable/disable OS decoration, OS task bar icons, etc.)
// - To the platform backend for OS level parent/child relationships of viewport. // - To the platform backend for OS level parent/child relationships of viewport (otherwise: default is configured via io.ConfigViewportsNoDefaultParent)
// - To the docking system for various options and filtering. // - To the docking system for various options and filtering.
struct ImGuiWindowClass struct ImGuiWindowClass
{ {
@@ -2809,6 +2815,7 @@ struct ImGuiWindowClass
ImGuiDockNodeFlags DockNodeFlagsOverrideSet; // [EXPERIMENTAL] Dock node flags to set when a window of this class is hosted by a dock node (it doesn't have to be selected!) ImGuiDockNodeFlags DockNodeFlagsOverrideSet; // [EXPERIMENTAL] Dock node flags to set when a window of this class is hosted by a dock node (it doesn't have to be selected!)
bool DockingAlwaysTabBar; // Set to true to enforce single floating windows of this class always having their own docking node (equivalent of setting the global io.ConfigDockingAlwaysTabBar) bool DockingAlwaysTabBar; // Set to true to enforce single floating windows of this class always having their own docking node (equivalent of setting the global io.ConfigDockingAlwaysTabBar)
bool DockingAllowUnclassed; // Set to true to allow windows of this class to be docked/merged with an unclassed window. // FIXME-DOCK: Move to DockNodeFlags override? bool DockingAllowUnclassed; // Set to true to allow windows of this class to be docked/merged with an unclassed window. // FIXME-DOCK: Move to DockNodeFlags override?
void* PlatformIconData; // [EXPERIMENTAL] Pass opaque data for Platform backend to handle.
ImGuiWindowClass() { memset((void*)this, 0, sizeof(*this)); ParentViewportId = (ImGuiID)-1; DockingAllowUnclassed = true; } ImGuiWindowClass() { memset((void*)this, 0, sizeof(*this)); ParentViewportId = (ImGuiID)-1; DockingAllowUnclassed = true; }
}; };
@@ -3298,12 +3305,6 @@ typedef unsigned short ImDrawIdx; // Default: 16-bit (for maximum compatibilit
typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd); typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd);
#endif #endif
// Special Draw callback value to request renderer backend to reset the graphics/render state.
// The renderer backend needs to handle this special value, otherwise it will crash trying to call a function at this address.
// This is useful, for example, if you submitted callbacks which you know have altered the render state and you want it to be restored.
// Render state is not reset by default because they are many perfectly useful way of altering render state (e.g. changing shader/blending settings before an Image call).
#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-8)
// Typically, 1 command = 1 GPU draw call (unless command is a callback) // Typically, 1 command = 1 GPU draw call (unless command is a callback)
// - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled, // - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled,
// this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices. // this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices.
@@ -3377,23 +3378,30 @@ struct ImDrawListSplitter
}; };
// Flags for ImDrawList functions // Flags for ImDrawList functions
// (Legacy: bit 0 must always correspond to ImDrawFlags_Closed to be backward compatible with old API using a bool. Bits 1..3 must be unused)
enum ImDrawFlags_ enum ImDrawFlags_
{ {
ImDrawFlags_None = 0, ImDrawFlags_None = 0,
ImDrawFlags_Closed = 1 << 0, // PathStroke(), AddPolyline(): specify that shape should be closed (Important: this is always == 1 for legacy reason)
ImDrawFlags_RoundCornersTopLeft = 1 << 4, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-left corner only (when rounding > 0.0f, we default to all corners). Was 0x01. // Rounding for AddRect(), AddRectFilled(), PathRect()
ImDrawFlags_RoundCornersTopRight = 1 << 5, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-right corner only (when rounding > 0.0f, we default to all corners). Was 0x02. // - When not specified, we defaults to ImDrawFlags_RoundCornersAll! So you only need to use those flags if you want another configuration.
ImDrawFlags_RoundCornersBottomLeft = 1 << 6, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-left corner only (when rounding > 0.0f, we default to all corners). Was 0x04. ImDrawFlags_RoundCornersTopLeft = 1 << 4, // Round top-left corner only (when rounding > 0.0f, we default to all corners).
ImDrawFlags_RoundCornersBottomRight = 1 << 7, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-right corner only (when rounding > 0.0f, we default to all corners). Wax 0x08. ImDrawFlags_RoundCornersTopRight = 1 << 5, // Round top-right corner only (when rounding > 0.0f, we default to all corners).
ImDrawFlags_RoundCornersNone = 1 << 8, // AddRect(), AddRectFilled(), PathRect(): disable rounding on all corners (when rounding > 0.0f). This is NOT zero, NOT an implicit flag! ImDrawFlags_RoundCornersBottomLeft = 1 << 6, // Round bottom-left corner only (when rounding > 0.0f, we default to all corners).
ImDrawFlags_RoundCornersBottomRight = 1 << 7, // Round bottom-right corner only (when rounding > 0.0f, we default to all corners).
ImDrawFlags_RoundCornersNone = 1 << 8, // Disable rounding even if `float rounding > 0.0f`. This is NOT zero, NOT an implicit flag!
ImDrawFlags_RoundCornersAll = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, // (Default!!)
ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, // Default to ALL corners if none of the _RoundCornersXX flags are specified!
ImDrawFlags_RoundCornersTop = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight, ImDrawFlags_RoundCornersTop = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight,
ImDrawFlags_RoundCornersBottom = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, ImDrawFlags_RoundCornersBottom = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight,
ImDrawFlags_RoundCornersLeft = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft, ImDrawFlags_RoundCornersLeft = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft,
ImDrawFlags_RoundCornersRight = ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight, ImDrawFlags_RoundCornersRight = ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight,
ImDrawFlags_RoundCornersAll = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight,
ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, // Default to ALL corners if none of the _RoundCornersXX flags are specified.
ImDrawFlags_RoundCornersMask_ = ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone, ImDrawFlags_RoundCornersMask_ = ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone,
// Stroke options
ImDrawFlags_Closed = 1 << 9, // PathStroke(), AddPolyline(): specify that shape should be closed.
//ImDrawFlags_Closed = 1 << 0, // Prior to 1.92.8 (May 2026), ImDrawFlags_Closed was guaranteed to be == 1<<0 == 1 for legacy compatibility reason. Hardcoded use of 1 or true should be replaced.
ImDrawFlags_InvalidMask_ = ~0x7FFFFFF0, // == 0x8000000F,
}; };
// Flags for ImDrawList instance. Those are set automatically by ImGui:: functions from ImGuiIO settings, and generally not manipulated directly. // Flags for ImDrawList instance. Those are set automatically by ImGui:: functions from ImGuiIO settings, and generally not manipulated directly.
@@ -3405,6 +3413,7 @@ enum ImDrawListFlags_
ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, // Enable anti-aliased lines/borders using textures when possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, // Enable anti-aliased lines/borders using textures when possible. Require backend to render with bilinear filtering (NOT point/nearest filtering).
ImDrawListFlags_AntiAliasedFill = 1 << 2, // Enable anti-aliased edge around filled shapes (rounded rectangles, circles). ImDrawListFlags_AntiAliasedFill = 1 << 2, // Enable anti-aliased edge around filled shapes (rounded rectangles, circles).
ImDrawListFlags_AllowVtxOffset = 1 << 3, // Can emit 'VtxOffset > 0' to allow large meshes. Set when 'ImGuiBackendFlags_RendererHasVtxOffset' is enabled. ImDrawListFlags_AllowVtxOffset = 1 << 3, // Can emit 'VtxOffset > 0' to allow large meshes. Set when 'ImGuiBackendFlags_RendererHasVtxOffset' is enabled.
ImDrawListFlags_TextNoPixelSnap = 1 << 4, // Disable automatically snapping AddText() calls to pixel boundaries.
}; };
// Draw command list // Draw command list
@@ -3459,7 +3468,9 @@ struct ImDrawList
// In future versions we will use textures to provide cheaper and higher-quality circles. // In future versions we will use textures to provide cheaper and higher-quality circles.
// Use AddNgon() and AddNgonFilled() functions if you need to guarantee a specific number of sides. // Use AddNgon() and AddNgonFilled() functions if you need to guarantee a specific number of sides.
IMGUI_API void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f); IMGUI_API void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f);
IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0, float thickness = 1.0f); // a: upper-left, b: lower-right (== upper-left + size) IMGUI_API void AddLineH(float min_x, float max_x, float y, ImU32 col, float thickness = 1.0f);
IMGUI_API void AddLineV(float x, float min_y, float max_y, ImU32 col, float thickness = 1.0f);
IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, float thickness = 1.0f, ImDrawFlags flags = 0); // a: upper-left, b: lower-right (== upper-left + size)
IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0); // a: upper-left, b: lower-right (== upper-left + size) IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0); // a: upper-left, b: lower-right (== upper-left + size)
IMGUI_API void AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); IMGUI_API void AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left);
IMGUI_API void AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness = 1.0f); IMGUI_API void AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness = 1.0f);
@@ -3480,7 +3491,7 @@ struct ImDrawList
// General polygon // General polygon
// - Only simple polygons are supported by filling functions (no self-intersections, no holes). // - Only simple polygons are supported by filling functions (no self-intersections, no holes).
// - Concave polygon fill is more expensive than convex one: it has O(N^2) complexity. Provided as a convenience for the user but not used by the main library. // - Concave polygon fill is more expensive than convex one: it has O(N^2) complexity. Provided as a convenience for the user but not used by the main library.
IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, float thickness, ImDrawFlags flags = 0);
IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col);
IMGUI_API void AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col); IMGUI_API void AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col);
@@ -3500,7 +3511,7 @@ struct ImDrawList
inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); }
inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; }
inline void PathFillConcave(ImU32 col) { AddConcavePolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } inline void PathFillConcave(ImU32 col) { AddConcavePolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; }
inline void PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, flags, thickness); _Path.Size = 0; } inline void PathStroke(ImU32 col, float thickness = 1.0f, ImDrawFlags flags = 0) { AddPolyline(_Path.Data, _Path.Size, col, thickness, flags); _Path.Size = 0; }
IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0);
IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle
IMGUI_API void PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse IMGUI_API void PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse
@@ -3510,14 +3521,15 @@ struct ImDrawList
// Advanced: Draw Callbacks // Advanced: Draw Callbacks
// - May be used to alter render state (change sampler, blending, current shader). May be used to emit custom rendering commands (difficult to do correctly, but possible). // - May be used to alter render state (change sampler, blending, current shader). May be used to emit custom rendering commands (difficult to do correctly, but possible).
// - Use special ImDrawCallback_ResetRenderState callback to instruct backend to reset its render state to the default. // - Use special GetPlatformIO().DrawCallback_ResetRenderState callback to instruct backend to reset its render state to the default.
// - See other standard callbacks in GetPlatformIO(), which may or not be supported by your backend.
// - Your rendering loop must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. All standard backends are honoring this. // - Your rendering loop must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. All standard backends are honoring this.
// - For some backends, the callback may access selected render-states exposed by the backend in a ImGui_ImplXXXX_RenderState structure pointed to by platform_io.Renderer_RenderState. // - For some backends, the callback may access selected render-states exposed by the backend in a ImGui_ImplXXXX_RenderState structure pointed to by platform_io.Renderer_RenderState.
// - IMPORTANT: please be mindful of the different level of indirection between using size==0 (copying argument) and using size>0 (copying pointed data into a buffer). // - IMPORTANT: please be mindful of the different level of indirection between using size==0 (copying argument) and using size>0 (copying pointed data into a buffer).
// - If userdata_size == 0: we copy/store the 'userdata' argument as-is. It will be available unmodified in ImDrawCmd::UserCallbackData during render. // - If userdata_size == 0: we copy/store the 'userdata' argument as-is. It will be available unmodified in ImDrawCmd::UserCallbackData during render.
// - If userdata_size > 0, we copy/store 'userdata_size' bytes pointed to by 'userdata'. We store them in a buffer stored inside the drawlist. ImDrawCmd::UserCallbackData will point inside that buffer so you have to retrieve data from there. Your callback may need to use ImDrawCmd::UserCallbackDataSize if you expect dynamically-sized data. // - If userdata_size > 0, we copy/store 'userdata_size' bytes pointed to by 'userdata'. We store them in a buffer stored inside the drawlist. ImDrawCmd::UserCallbackData will point inside that buffer so you have to retrieve data from there. Your callback may need to use ImDrawCmd::UserCallbackDataSize if you expect dynamically-sized data.
// - Support for userdata_size > 0 was added in v1.91.4, October 2024. So earlier code always only allowed to copy/store a simple void*. // - Support for userdata_size > 0 was added in v1.91.4, October 2024. So earlier code always only allowed to copy/store a simple void*.
IMGUI_API void AddCallback(ImDrawCallback callback, void* userdata, size_t userdata_size = 0); IMGUI_API void AddCallback(ImDrawCallback callback, void* userdata = NULL, size_t userdata_size = 0);
// Advanced: Miscellaneous // Advanced: Miscellaneous
IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible
@@ -3547,8 +3559,15 @@ struct ImDrawList
// Obsolete names // Obsolete names
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
inline void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness) { AddRect(p_min, p_max, col, rounding, thickness, flags); } // OBSOLETED in 1.92.8: NEW FUNCTION SIGNATURE HAS 'thickness' AND 'flags' SWAPPED.
inline void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness) { AddPolyline(points, num_points, col, thickness, flags); } // OBSOLETED in 1.92.8: NEW FUNCTION SIGNATURE HAS 'thickness' AND 'flags' SWAPPED.
inline void PathStroke(ImU32 col, ImDrawFlags flags, float thickness) { PathStroke(col, thickness, flags); } // OBSOLETED in 1.92.8: NEW FUNCTION SIGNATURE HAS 'thickness' AND 'flags' SWAPPED.
inline void PushTextureID(ImTextureRef tex_ref) { PushTexture(tex_ref); } // RENAMED in 1.92.0 inline void PushTextureID(ImTextureRef tex_ref) { PushTexture(tex_ref); } // RENAMED in 1.92.0
inline void PopTextureID() { PopTexture(); } // RENAMED in 1.92.0 inline void PopTextureID() { PopTexture(); } // RENAMED in 1.92.0
#else
void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding /*= 0.0f*/, ImDrawFlags flags /*= 0*/, float thickness /*= 1.0f*/) = delete;
void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness) = delete;
inline void PathStroke(ImU32 col, ImDrawFlags flags /*= 0*/, float thickness /*= 1.0f*/) = delete;
#endif #endif
//inline void AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f) { AddEllipse(center, ImVec2(radius_x, radius_y), col, rot, num_segments, thickness); } // OBSOLETED in 1.90.5 (Mar 2024) //inline void AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f) { AddEllipse(center, ImVec2(radius_x, radius_y), col, rot, num_segments, thickness); } // OBSOLETED in 1.90.5 (Mar 2024)
//inline void AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0) { AddEllipseFilled(center, ImVec2(radius_x, radius_y), col, rot, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) //inline void AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0) { AddEllipseFilled(center, ImVec2(radius_x, radius_y), col, rot, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024)
@@ -3660,6 +3679,7 @@ struct ImTextureData
bool WantDestroyNextFrame; // rw - // [Internal] Queued to set ImTextureStatus_WantDestroy next frame. May still be used in the current frame. bool WantDestroyNextFrame; // rw - // [Internal] Queued to set ImTextureStatus_WantDestroy next frame. May still be used in the current frame.
// Functions // Functions
// - If GetPixels() functions asserts while being called by your render loop, it could be caused by calling ImFontAtlas::Clear()/ClearFonts()?
ImTextureData() { memset((void*)this, 0, sizeof(*this)); Status = ImTextureStatus_Destroyed; TexID = ImTextureID_Invalid; } ImTextureData() { memset((void*)this, 0, sizeof(*this)); Status = ImTextureStatus_Destroyed; TexID = ImTextureID_Invalid; }
~ImTextureData() { DestroyPixels(); } ~ImTextureData() { DestroyPixels(); }
IMGUI_API void Create(ImTextureFormat format, int w, int h); IMGUI_API void Create(ImTextureFormat format, int w, int h);
@@ -3813,15 +3833,17 @@ struct ImFontAtlas
IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed.
IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp.
IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter.
IMGUI_API void RemoveFont(ImFont* font); IMGUI_API void RemoveFont(ImFont* font); // Remove a font
IMGUI_API void CompactCache(); // Compact cached glyphs and texture.
IMGUI_API void Clear(); // Clear everything (input fonts, output glyphs/textures).
IMGUI_API void CompactCache(); // Compact cached glyphs and texture.
IMGUI_API void SetFontLoader(const ImFontLoader* font_loader); // Change font loader at runtime. IMGUI_API void SetFontLoader(const ImFontLoader* font_loader); // Change font loader at runtime.
// As we are transitioning toward a new font system, we expect to obsolete those soon: // Clearing the atlas/fonts has little use nowadays, unless you want to batch remove all fonts.
// - Since 1.92, you can call ClearFonts() mid-frame, if you load new fonts afterwards.
// - As we are transitioning toward our new font system the semantic for those functions gets increasingly misleading and are often a source of issues.
// TL;DR; most likely, don't use any of those functions. We expect to obsolete/rework them.
IMGUI_API void Clear(); // Clear everything (fonts + textures). Don't call mid-frame!
IMGUI_API void ClearFonts(); // Clear input+output font data/glyphs. New fonts and textures will be recreated afterwards.
IMGUI_API void ClearInputData(); // [OBSOLETE] Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts. IMGUI_API void ClearInputData(); // [OBSOLETE] Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts.
IMGUI_API void ClearFonts(); // [OBSOLETE] Clear input+output font data (same as ClearInputData() + glyphs storage, UV coordinates).
IMGUI_API void ClearTexData(); // [OBSOLETE] Clear CPU-side copy of the texture data. Saves RAM once the texture has been copied to graphics memory. IMGUI_API void ClearTexData(); // [OBSOLETE] Clear CPU-side copy of the texture data. Saves RAM once the texture has been copied to graphics memory.
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
@@ -3993,6 +4015,7 @@ enum ImFontFlags_
ImFontFlags_NoLoadError = 1 << 1, // Disable throwing an error/assert when calling AddFontXXX() with missing file/data. Calling code is expected to check AddFontXXX() return value. ImFontFlags_NoLoadError = 1 << 1, // Disable throwing an error/assert when calling AddFontXXX() with missing file/data. Calling code is expected to check AddFontXXX() return value.
ImFontFlags_NoLoadGlyphs = 1 << 2, // [Internal] Disable loading new glyphs. ImFontFlags_NoLoadGlyphs = 1 << 2, // [Internal] Disable loading new glyphs.
ImFontFlags_LockBakedSizes = 1 << 3, // [Internal] Disable loading new baked sizes, disable garbage collecting current ones. e.g. if you want to lock a font to a single size. Important: if you use this to preload given sizes, consider the possibility of multiple font density used on Retina display. ImFontFlags_LockBakedSizes = 1 << 3, // [Internal] Disable loading new baked sizes, disable garbage collecting current ones. e.g. if you want to lock a font to a single size. Important: if you use this to preload given sizes, consider the possibility of multiple font density used on Retina display.
ImFontFlags_ImplicitRefSize = 1 << 4, // [Internal] Reference size was not set explicitly.
}; };
// Font runtime data and rendering // Font runtime data and rendering
@@ -4038,7 +4061,7 @@ struct ImFont
IMGUI_API void RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c, const ImVec4* cpu_fine_clip = NULL); IMGUI_API void RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c, const ImVec4* cpu_fine_clip = NULL);
IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, ImDrawTextFlags flags = 0); IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, ImDrawTextFlags flags = 0);
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
inline const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) { return CalcWordWrapPosition(LegacySize * scale, text, text_end, wrap_width); } inline const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) { return CalcWordWrapPosition(LegacySize * scale, text, text_end, wrap_width); } // Obsoleted old name in 1.92.0. Note how `scale` was to `size`.
#endif #endif
// [Internal] Don't use! // [Internal] Don't use!
@@ -4121,6 +4144,7 @@ struct ImGuiViewport
// The library never uses those fields, they are merely storage to facilitate backend implementation. // The library never uses those fields, they are merely storage to facilitate backend implementation.
void* RendererUserData; // void* to hold custom data structure for the renderer (e.g. swap chain, framebuffers etc.). generally set by your Renderer_CreateWindow function. void* RendererUserData; // void* to hold custom data structure for the renderer (e.g. swap chain, framebuffers etc.). generally set by your Renderer_CreateWindow function.
void* PlatformUserData; // void* to hold custom data structure for the OS / platform (e.g. windowing info, render context). generally set by your Platform_CreateWindow function. void* PlatformUserData; // void* to hold custom data structure for the OS / platform (e.g. windowing info, render context). generally set by your Platform_CreateWindow function.
void* PlatformIconData; // void* to hold custom data structure for the OS / platform to specify an icon. Currently unused for exposed to allow experiments.
void* PlatformHandle; // void* to hold higher-level, platform window handle (e.g. HWND for Win32 backend, Uint32 WindowID for SDL, GLFWWindow* for GLFW), for FindViewportByPlatformHandle(). void* PlatformHandle; // void* to hold higher-level, platform window handle (e.g. HWND for Win32 backend, Uint32 WindowID for SDL, GLFWWindow* for GLFW), for FindViewportByPlatformHandle().
void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (always HWND on Win32 platform, unused for other platforms). void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (always HWND on Win32 platform, unused for other platforms).
bool PlatformWindowCreated; // Platform window has been created (Platform_CreateWindow() has been called). This is false during the first frame where a viewport is being created. bool PlatformWindowCreated; // Platform window has been created (Platform_CreateWindow() has been called). This is false during the first frame where a viewport is being created.
@@ -4228,6 +4252,12 @@ struct ImGuiPlatformIO
// Written by some backends during ImGui_ImplXXXX_RenderDrawData() call to point backend_specific ImGui_ImplXXXX_RenderState* structure. // Written by some backends during ImGui_ImplXXXX_RenderDrawData() call to point backend_specific ImGui_ImplXXXX_RenderState* structure.
void* Renderer_RenderState; void* Renderer_RenderState;
// Standard draw callbacks provided by renderer backend.
ImDrawCallback DrawCallback_ResetRenderState; // Request to reset the graphics/render state.
ImDrawCallback DrawCallback_SetSamplerLinear; // Request backend to set texture sampling to Linear.
ImDrawCallback DrawCallback_SetSamplerNearest; // Request backend to set texture sampling to Nearest/Point.
//ImDrawCallback DrawCallback_SetSamplerCustom; // Request backend to set texture sampling using Backend Specific data.
//------------------------------------------------------------------ //------------------------------------------------------------------
// Input - Interface with Platform & Renderer backends for Multi-Viewport support // Input - Interface with Platform & Renderer backends for Multi-Viewport support
//------------------------------------------------------------------ //------------------------------------------------------------------
@@ -4421,6 +4451,8 @@ namespace ImGui
//static inline void SetScrollPosHere() { SetScrollHere(); } // OBSOLETED in 1.42 //static inline void SetScrollPosHere() { SetScrollHere(); } // OBSOLETED in 1.42
} }
#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-8) // OBSOLETED in 1.92.8: Use ImGui::GetPlatformIO().DrawCallback_ResetRenderState
//-- OBSOLETED in 1.92.0: ImFontAtlasCustomRect becomes ImTextureRect //-- OBSOLETED in 1.92.0: ImFontAtlasCustomRect becomes ImTextureRect
// - ImFontAtlasCustomRect::X,Y --> ImTextureRect::x,y // - ImFontAtlasCustomRect::X,Y --> ImTextureRect::x,y
// - ImFontAtlasCustomRect::Width,Height --> ImTextureRect::w,h // - ImFontAtlasCustomRect::Width,Height --> ImTextureRect::w,h
+217 -92
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7 // dear imgui, v1.92.9 WIP
// (demo code) // (demo code)
// Help: // Help:
@@ -73,6 +73,7 @@ Index of this file:
// [SECTION] Demo Window / ShowDemoWindow() // [SECTION] Demo Window / ShowDemoWindow()
// [SECTION] DemoWindowMenuBar() // [SECTION] DemoWindowMenuBar()
// [SECTION] Helpers: ExampleTreeNode, ExampleMemberInfo (for use by Property Editor & Multi-Select demos) // [SECTION] Helpers: ExampleTreeNode, ExampleMemberInfo (for use by Property Editor & Multi-Select demos)
// [SECTION] Helpers: ExampleImageViewer
// [SECTION] DemoWindowWidgetsBasic() // [SECTION] DemoWindowWidgetsBasic()
// [SECTION] DemoWindowWidgetsBullets() // [SECTION] DemoWindowWidgetsBullets()
// [SECTION] DemoWindowWidgetsCollapsingHeaders() // [SECTION] DemoWindowWidgetsCollapsingHeaders()
@@ -108,6 +109,7 @@ Index of this file:
// [SECTION] User Guide / ShowUserGuide() // [SECTION] User Guide / ShowUserGuide()
// [SECTION] Example App: Main Menu Bar / ShowExampleAppMainMenuBar() // [SECTION] Example App: Main Menu Bar / ShowExampleAppMainMenuBar()
// [SECTION] Example App: Debug Console / ShowExampleAppConsole() // [SECTION] Example App: Debug Console / ShowExampleAppConsole()
// [SECTION] Example App: Image Viewer / ShowExampleAppImageViewer()
// [SECTION] Example App: Debug Log / ShowExampleAppLog() // [SECTION] Example App: Debug Log / ShowExampleAppLog()
// [SECTION] Example App: Simple Layout / ShowExampleAppLayout() // [SECTION] Example App: Simple Layout / ShowExampleAppLayout()
// [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() // [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor()
@@ -240,6 +242,7 @@ static void ShowExampleAppConsole(bool* p_open);
static void ShowExampleAppCustomRendering(bool* p_open); static void ShowExampleAppCustomRendering(bool* p_open);
static void ShowExampleAppDockSpace(bool* p_open); static void ShowExampleAppDockSpace(bool* p_open);
static void ShowExampleAppDocuments(bool* p_open); static void ShowExampleAppDocuments(bool* p_open);
static void ShowExampleAppImageViewer(bool* p_open);
static void ShowExampleAppLog(bool* p_open); static void ShowExampleAppLog(bool* p_open);
static void ShowExampleAppLayout(bool* p_open); static void ShowExampleAppLayout(bool* p_open);
static void ShowExampleAppPropertyEditor(bool* p_open, ImGuiDemoWindowData* demo_data); static void ShowExampleAppPropertyEditor(bool* p_open, ImGuiDemoWindowData* demo_data);
@@ -321,6 +324,7 @@ struct ImGuiDemoWindowData
bool ShowAppCustomRendering = false; bool ShowAppCustomRendering = false;
bool ShowAppDocuments = false; bool ShowAppDocuments = false;
bool ShowAppDockSpace = false; bool ShowAppDockSpace = false;
bool ShowAppImageViewer = false;
bool ShowAppLog = false; bool ShowAppLog = false;
bool ShowAppLayout = false; bool ShowAppLayout = false;
bool ShowAppPropertyEditor = false; bool ShowAppPropertyEditor = false;
@@ -367,6 +371,7 @@ void ImGui::ShowDemoWindow(bool* p_open)
if (demo_data.ShowAppAssetsBrowser) { ShowExampleAppAssetsBrowser(&demo_data.ShowAppAssetsBrowser); } if (demo_data.ShowAppAssetsBrowser) { ShowExampleAppAssetsBrowser(&demo_data.ShowAppAssetsBrowser); }
if (demo_data.ShowAppConsole) { ShowExampleAppConsole(&demo_data.ShowAppConsole); } if (demo_data.ShowAppConsole) { ShowExampleAppConsole(&demo_data.ShowAppConsole); }
if (demo_data.ShowAppCustomRendering) { ShowExampleAppCustomRendering(&demo_data.ShowAppCustomRendering); } if (demo_data.ShowAppCustomRendering) { ShowExampleAppCustomRendering(&demo_data.ShowAppCustomRendering); }
if (demo_data.ShowAppImageViewer) { ShowExampleAppImageViewer(&demo_data.ShowAppImageViewer); }
if (demo_data.ShowAppLog) { ShowExampleAppLog(&demo_data.ShowAppLog); } if (demo_data.ShowAppLog) { ShowExampleAppLog(&demo_data.ShowAppLog); }
if (demo_data.ShowAppLayout) { ShowExampleAppLayout(&demo_data.ShowAppLayout); } if (demo_data.ShowAppLayout) { ShowExampleAppLayout(&demo_data.ShowAppLayout); }
if (demo_data.ShowAppPropertyEditor) { ShowExampleAppPropertyEditor(&demo_data.ShowAppPropertyEditor, &demo_data); } if (demo_data.ShowAppPropertyEditor) { ShowExampleAppPropertyEditor(&demo_data.ShowAppPropertyEditor, &demo_data); }
@@ -748,6 +753,7 @@ static void DemoWindowMenuBar(ImGuiDemoWindowData* demo_data)
ImGui::MenuItem("Custom rendering", NULL, &demo_data->ShowAppCustomRendering); ImGui::MenuItem("Custom rendering", NULL, &demo_data->ShowAppCustomRendering);
ImGui::MenuItem("Documents", NULL, &demo_data->ShowAppDocuments); ImGui::MenuItem("Documents", NULL, &demo_data->ShowAppDocuments);
ImGui::MenuItem("Dockspace", NULL, &demo_data->ShowAppDockSpace); ImGui::MenuItem("Dockspace", NULL, &demo_data->ShowAppDockSpace);
ImGui::MenuItem("Image Viewer", NULL, &demo_data->ShowAppImageViewer);
ImGui::MenuItem("Log", NULL, &demo_data->ShowAppLog); ImGui::MenuItem("Log", NULL, &demo_data->ShowAppLog);
ImGui::MenuItem("Property editor", NULL, &demo_data->ShowAppPropertyEditor); ImGui::MenuItem("Property editor", NULL, &demo_data->ShowAppPropertyEditor);
ImGui::MenuItem("Simple layout", NULL, &demo_data->ShowAppLayout); ImGui::MenuItem("Simple layout", NULL, &demo_data->ShowAppLayout);
@@ -779,7 +785,7 @@ static void DemoWindowMenuBar(ImGuiDemoWindowData* demo_data)
ImGui::Checkbox("Highlight ID Conflicts", &io.ConfigDebugHighlightIdConflicts); ImGui::Checkbox("Highlight ID Conflicts", &io.ConfigDebugHighlightIdConflicts);
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::Checkbox("Assert on error recovery", &io.ConfigErrorRecoveryEnableAssert); ImGui::Checkbox("Assert on error recovery", &io.ConfigErrorRecoveryEnableAssert);
ImGui::TextDisabled("(see Demo->Configuration for details & more)"); ImGui::TextDisabled("(see Demo->Configuration for more)");
ImGui::EndMenu(); ImGui::EndMenu();
} }
ImGui::MenuItem("Debug Log", NULL, &demo_data->ShowDebugLog, has_debug_tools); ImGui::MenuItem("Debug Log", NULL, &demo_data->ShowDebugLog, has_debug_tools);
@@ -896,6 +902,87 @@ static ExampleTreeNode* ExampleTree_CreateDemoTree()
return node_L0; return node_L0;
} }
//-----------------------------------------------------------------------------
// [SECTION] Helpers: ExampleImageViewer
//-----------------------------------------------------------------------------
struct ExampleImageViewerData
{
ImU32 ImageBgColor = IM_COL32(100, 100, 100, 255);
ImU32 GridColor = IM_COL32(255, 255, 255, 100);
bool GridEnabled = true;
bool ViewReset = true;
ImVec2 ViewOffset; // in image space
float Zoom = 10.0f;
float ZoomMin = 1.0f;
float ZoomMax = 10000.0f;
};
static void ExampleImageViewer_DrawOptions(ExampleImageViewerData* data)
{
ImGui::SetNextItemShortcut(ImGuiKey_G, ImGuiInputFlags_Tooltip); // | ImGuiInputFlags_RouteGlobal
ImGui::Checkbox("Grid", &data->GridEnabled);
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10.0f);
float zoom_100 = data->Zoom * 100.0f;
if (ImGui::DragFloat("##Zoom", &zoom_100, 5.0f, data->ZoomMin * 100.0f, data->ZoomMax * 100.0f, "%.0f%%", ImGuiSliderFlags_AlwaysClamp))
data->Zoom = zoom_100 / 100.0f;
}
static void ExampleImageViewer_DrawCanvas(ExampleImageViewerData* data, ImVec2 canvas_size, ImTextureRef image_tex_ref, int image_w, int image_h)
{
ImGuiIO& io = ImGui::GetIO();
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImDrawList* draw_list = ImGui::GetWindowDrawList();
IM_ASSERT(canvas_size.x >= 0.0f && canvas_size.y >= 0.0f);
// Layout canvas
ImGui::InvisibleButton("##Canvas", canvas_size);
ImVec2 canvas_min = ImGui::GetItemRectMin();
ImVec2 canvas_max = ImGui::GetItemRectMax();
if (data->ViewReset)
data->ViewOffset = ImVec2((canvas_size.x * 0.5f / data->Zoom) - 0.5f, (canvas_size.y * 0.5f / data->Zoom) - 0.5f); // Add half a pixel padding
data->ViewReset = false;
// Handle inputs
if (ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY))
if (io.MouseWheel != 0.0f)
data->Zoom = IM_CLAMP(data->Zoom * (1.0f + io.MouseWheel * 0.10f), data->ZoomMin, data->ZoomMax);
float zoom = data->Zoom; // (float)(int)ViewZoom;
if (ImGui::IsItemActive() && ImGui::IsMouseDragging(0))
{
data->ViewOffset.x -= io.MouseDelta.x / zoom;
data->ViewOffset.y -= io.MouseDelta.y / zoom;
}
// Display image
ImVec2 image_min, image_max;
image_min.x = (float)(int)((canvas_min.x - (data->ViewOffset.x * zoom)) + (canvas_size.x * 0.5f));
image_min.y = (float)(int)((canvas_min.y - (data->ViewOffset.y * zoom)) + (canvas_size.y * 0.5f));
image_max.x = (float)(int)(image_min.x + image_w * zoom);
image_max.y = (float)(int)(image_min.y + image_h * zoom);
draw_list->AddRect(ImVec2(canvas_min.x - 1.0f, canvas_min.y - 1.0f), ImVec2(canvas_max.x + 1.0f, canvas_max.y + 1.0f), IM_COL32(255, 255, 255, 255));
draw_list->PushClipRect(canvas_min, canvas_max, true);
draw_list->AddRectFilled(image_min, image_max, data->ImageBgColor);
if (platform_io.DrawCallback_SetSamplerNearest != NULL)
draw_list->AddCallback(platform_io.DrawCallback_SetSamplerNearest);
draw_list->AddImage(image_tex_ref, image_min, image_max);
if (platform_io.DrawCallback_SetSamplerLinear != NULL)
draw_list->AddCallback(ImGui::GetPlatformIO().DrawCallback_SetSamplerLinear);
// Display grid lines for visible pixels
if (data->GridEnabled && zoom > 6.0f)
{
const float step = (float)zoom;
for (int px = (int)((canvas_min.x - image_min.x) / step); px <= (int)((canvas_max.x - image_min.x) / step); px++)
draw_list->AddLineV(image_min.x + px * step, canvas_min.y, canvas_max.y, data->GridColor, 1.0f);
for (int py = (int)((canvas_min.y - image_min.y) / step); py <= (int)((canvas_max.y - image_min.y) / step); py++)
draw_list->AddLineH(canvas_min.x, canvas_max.x, image_min.y + py * step, data->GridColor, 1.0f);
}
draw_list->PopClipRect();
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// [SECTION] DemoWindowWidgetsBasic() // [SECTION] DemoWindowWidgetsBasic()
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@@ -1873,40 +1960,29 @@ static void DemoWindowWidgetsImages()
// - Read https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples // - Read https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
// Grab the current texture identifier used by the font atlas. // Grab the current texture identifier used by the font atlas.
ImTextureRef my_tex_id = io.Fonts->TexRef; ImFontAtlas* atlas = io.Fonts;
ImTextureRef my_tex_id = atlas->TexRef;
float my_tex_w = (float)atlas->TexData->Width; // Regular user code should never have to care about TexData-> fields, but since we want to display the entire texture here, we pull Width/Height from it.
float my_tex_h = (float)atlas->TexData->Height;
ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h);
// Regular user code should never have to care about TexData-> fields, but since we want to display the entire texture here, we pull Width/Height from it. // Basic drawing
float my_tex_w = (float)io.Fonts->TexData->Width; ImGui::SeparatorText("Image()/ImageWithBg() function");
float my_tex_h = (float)io.Fonts->TexData->Height; ImVec2 uv_min = ImVec2(0.0f, 0.0f); // Top-left
ImVec2 uv_max = ImVec2(1.0f, 1.0f); // Lower-right
ImGui::PushStyleVar(ImGuiStyleVar_ImageBorderSize, IM_MAX(1.0f, ImGui::GetStyle().ImageBorderSize));
ImGui::ImageWithBg(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
ImGui::PopStyleVar();
{ // Fancy widget
ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h); ImGui::SeparatorText("Interactive Image Viewer");
ImVec2 pos = ImGui::GetCursorScreenPos(); static ExampleImageViewerData image_viewer;
ImVec2 uv_min = ImVec2(0.0f, 0.0f); // Top-left ImVec2 canvas_size(ImGui::GetContentRegionAvail().x, my_tex_h * 2.0f);
ImVec2 uv_max = ImVec2(1.0f, 1.0f); // Lower-right ExampleImageViewer_DrawOptions(&image_viewer);
ImGui::PushStyleVar(ImGuiStyleVar_ImageBorderSize, IM_MAX(1.0f, ImGui::GetStyle().ImageBorderSize)); ExampleImageViewer_DrawCanvas(&image_viewer, canvas_size, my_tex_id, (int)my_tex_w, (int)my_tex_h);
ImGui::ImageWithBg(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
if (ImGui::BeginItemTooltip())
{
float region_sz = 32.0f;
float region_x = io.MousePos.x - pos.x - region_sz * 0.5f;
float region_y = io.MousePos.y - pos.y - region_sz * 0.5f;
float zoom = 4.0f;
if (region_x < 0.0f) { region_x = 0.0f; }
else if (region_x > my_tex_w - region_sz) { region_x = my_tex_w - region_sz; }
if (region_y < 0.0f) { region_y = 0.0f; }
else if (region_y > my_tex_h - region_sz) { region_y = my_tex_h - region_sz; }
ImGui::Text("Min: (%.2f, %.2f)", region_x, region_y);
ImGui::Text("Max: (%.2f, %.2f)", region_x + region_sz, region_y + region_sz);
ImVec2 uv0 = ImVec2((region_x) / my_tex_w, (region_y) / my_tex_h);
ImVec2 uv1 = ImVec2((region_x + region_sz) / my_tex_w, (region_y + region_sz) / my_tex_h);
ImGui::ImageWithBg(my_tex_id, ImVec2(region_sz * zoom, region_sz * zoom), uv0, uv1, ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
ImGui::EndTooltip();
}
ImGui::PopStyleVar();
}
IMGUI_DEMO_MARKER("Widgets/Images/Textured buttons"); IMGUI_DEMO_MARKER("Widgets/Images/Textured buttons");
ImGui::SeparatorText("Textured Buttons");
ImGui::TextWrapped("And now some textured buttons.."); ImGui::TextWrapped("And now some textured buttons..");
static int pressed_count = 0; static int pressed_count = 0;
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
@@ -4131,7 +4207,7 @@ static void DemoWindowWidgetsTooltips()
ImGui::BeginDisabled(); ImGui::BeginDisabled();
ImGui::Button("Disabled item", sz); ImGui::Button("Disabled item", sz);
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip))
ImGui::SetTooltip("I am a a tooltip for a disabled item."); ImGui::SetTooltip("I am a tooltip for a disabled item.");
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::TreePop(); ImGui::TreePop();
@@ -4148,9 +4224,9 @@ static void DemoWindowWidgetsTreeNodes()
{ {
IMGUI_DEMO_MARKER("Widgets/Tree Nodes"); IMGUI_DEMO_MARKER("Widgets/Tree Nodes");
// See see "Examples -> Property Editor" (ShowExampleAppPropertyEditor() function) for a fancier, data-driven tree. // See see "Examples -> Property Editor" (ShowExampleAppPropertyEditor() function) for a fancier, data-driven tree.
if (ImGui::TreeNode("Basic trees")) if (ImGui::TreeNode("Basic Trees"))
{ {
IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Basic trees"); IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Basic Trees");
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
{ {
// Use SetNextItemOpen() so set the default state of a node to be open. We could // Use SetNextItemOpen() so set the default state of a node to be open. We could
@@ -4174,9 +4250,9 @@ static void DemoWindowWidgetsTreeNodes()
ImGui::TreePop(); ImGui::TreePop();
} }
if (ImGui::TreeNode("Hierarchy lines")) if (ImGui::TreeNode("Hierarchy Lines"))
{ {
IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Hierarchy lines"); IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Hierarchy Lines");
static ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DefaultOpen; static ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DefaultOpen;
HelpMarker("Default option for DrawLinesXXX is stored in style.TreeLinesFlags"); HelpMarker("Default option for DrawLinesXXX is stored in style.TreeLinesFlags");
ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesNone", &base_flags, ImGuiTreeNodeFlags_DrawLinesNone); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesNone", &base_flags, ImGuiTreeNodeFlags_DrawLinesNone);
@@ -4213,15 +4289,57 @@ static void DemoWindowWidgetsTreeNodes()
ImGui::TreePop(); ImGui::TreePop();
} }
if (ImGui::TreeNode("Advanced, with Selectable nodes")) if (ImGui::TreeNode("Selectable Nodes"))
{ {
IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Advanced, with Selectable nodes"); IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Selectable Nodes");
HelpMarker( HelpMarker(
"This is a more typical looking tree with selectable nodes.\n" "Manually implemented selectable nodes.\n"
"Click to select, Ctrl+Click to toggle, click on arrows or double-click to open."); "Click to select, Ctrl+Click to toggle, click on arrows or double-click to open.\n\n"
"You may also use the multi-select API (see 'Demo->Widgets->Selection State & Multi-Select') for more advanced multi-selection features.");
// Hold in 'selection_mask' a simple representation of what may be user-side selection state.
// - You may retain selection state inside or outside your objects in whatever format you see fit.
// You may use ImGuiSelectionBasicStorage which is conceptually close to a set<> of identifiers.
// - We record which node was clicked and then apply selection at the end of the loop.
// - This is a manual and simplified reimplementation of multi-selection, which the full
// BeginMultiSelect() API implements better, but which is not trivial to wire for trees.
static int selection_mask = 0x00;
int node_clicked_idx = -1;
for (int node_n = 0; node_n < 6; node_n++)
{
// Disable the default "open on single-click behavior" + set Selected flag according to our selection.
// To alter selection we use if 'IsItemClicked() && !IsItemToggledOpen()', so clicking on an arrow doesn't alter selection.
// In a BeginMultiSelect()/EndMultiSelect() we could use IsItemToggledSelection() but here we reimplement and use our own logic.
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_SpanAvailWidth;
if (selection_mask & (1 << node_n))
flags |= ImGuiTreeNodeFlags_Selected;
bool is_open = ImGui::TreeNodeEx((void*)(intptr_t)node_n, flags, "Selectable Node %d", node_n);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen())
node_clicked_idx = node_n;
if (is_open)
{
ImGui::BulletText("<Node contents here>");
ImGui::TreePop();
}
}
if (node_clicked_idx != -1)
{
// Update selection state (process outside of tree loop to avoid visual inconsistencies during the clicking frame)
if (ImGui::GetIO().KeyCtrl)
selection_mask ^= (1 << node_clicked_idx); // Ctrl+Click to toggle
else //if (!(selection_mask & (1 << node_clicked_idx))) // Depending on selection behavior you want, may want to preserve selection when clicking on item that is part of the selection
selection_mask = (1 << node_clicked_idx); // Click to single-select
}
ImGui::TreePop();
}
if (ImGui::TreeNode("Advanced"))
{
IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Advanced");
static ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_SpanAvailWidth; static ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_SpanAvailWidth;
static bool align_label_with_current_x_position = false; static bool align_label_with_current_x_position = false;
static bool test_drag_and_drop = false; static bool use_drag_and_drop = false;
ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnArrow", &base_flags, ImGuiTreeNodeFlags_OpenOnArrow); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnArrow", &base_flags, ImGuiTreeNodeFlags_OpenOnArrow);
ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnDoubleClick", &base_flags, ImGuiTreeNodeFlags_OpenOnDoubleClick); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnDoubleClick", &base_flags, ImGuiTreeNodeFlags_OpenOnDoubleClick);
ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAvailWidth", &base_flags, ImGuiTreeNodeFlags_SpanAvailWidth); ImGui::SameLine(); HelpMarker("Extend hit area to all available width instead of allowing more items to be laid out after the node."); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAvailWidth", &base_flags, ImGuiTreeNodeFlags_SpanAvailWidth); ImGui::SameLine(); HelpMarker("Extend hit area to all available width instead of allowing more items to be laid out after the node.");
@@ -4239,44 +4357,30 @@ static void DemoWindowWidgetsTreeNodes()
ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesToNodes", &base_flags, ImGuiTreeNodeFlags_DrawLinesToNodes); ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesToNodes", &base_flags, ImGuiTreeNodeFlags_DrawLinesToNodes);
ImGui::Checkbox("Align label with current X position", &align_label_with_current_x_position); ImGui::Checkbox("Align label with current X position", &align_label_with_current_x_position);
ImGui::Checkbox("Test tree node as drag source", &test_drag_and_drop); ImGui::Checkbox("Make Tree Nodes as drag & drop sources", &use_drag_and_drop);
ImGui::Text("Hello!");
if (align_label_with_current_x_position) if (align_label_with_current_x_position)
ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing()); ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing());
// 'selection_mask' is dumb representation of what may be user-side selection state. for (int node_n = 0; node_n < 6; node_n++)
// You may retain selection state inside or outside your objects in whatever format you see fit.
// 'node_clicked' is temporary storage of what node we have clicked to process selection at the end
/// of the loop. May be a pointer to your own node type, etc.
static int selection_mask = (1 << 2);
int node_clicked = -1;
for (int i = 0; i < 6; i++)
{ {
// Disable the default "open on single-click behavior" + set Selected flag according to our selection.
// To alter selection we use IsItemClicked() && !IsItemToggledOpen(), so clicking on an arrow doesn't alter selection.
ImGuiTreeNodeFlags node_flags = base_flags; ImGuiTreeNodeFlags node_flags = base_flags;
const bool is_selected = (selection_mask & (1 << i)) != 0; if (node_n < 3)
if (is_selected)
node_flags |= ImGuiTreeNodeFlags_Selected;
if (i < 3)
{ {
// Items 0..2 are Tree Node // Items 0..2 are Tree Node
bool node_open = ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Node %d", i); bool is_open = ImGui::TreeNodeEx((void*)(intptr_t)node_n, node_flags, "Selectable Node %d", node_n);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) if (use_drag_and_drop && ImGui::BeginDragDropSource())
node_clicked = i;
if (test_drag_and_drop && ImGui::BeginDragDropSource())
{ {
ImGui::SetDragDropPayload("_TREENODE", NULL, 0); ImGui::SetDragDropPayload("MY_TREENODE_PAYLOAD_TYPE", NULL, 0);
ImGui::Text("This is a drag and drop source"); ImGui::Text("This is a drag and drop source");
ImGui::EndDragDropSource(); ImGui::EndDragDropSource();
} }
if (i == 2 && (base_flags & ImGuiTreeNodeFlags_SpanLabelWidth)) if (node_n == 2 && (base_flags & ImGuiTreeNodeFlags_SpanLabelWidth))
{ {
// Item 2 has an additional inline button to help demonstrate SpanLabelWidth. // Item 2 has an additional inline button to help demonstrate SpanLabelWidth.
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("button")) {} if (ImGui::SmallButton("button")) {}
} }
if (node_open) if (is_open)
{ {
ImGui::BulletText("Blah blah\nBlah Blah"); ImGui::BulletText("Blah blah\nBlah Blah");
ImGui::SameLine(); ImGui::SameLine();
@@ -4290,26 +4394,15 @@ static void DemoWindowWidgetsTreeNodes()
// The only reason we use TreeNode at all is to allow selection of the leaf. Otherwise we can // The only reason we use TreeNode at all is to allow selection of the leaf. Otherwise we can
// use BulletText() or advance the cursor by GetTreeNodeToLabelSpacing() and call Text(). // use BulletText() or advance the cursor by GetTreeNodeToLabelSpacing() and call Text().
node_flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; // ImGuiTreeNodeFlags_Bullet node_flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; // ImGuiTreeNodeFlags_Bullet
ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Leaf %d", i); ImGui::TreeNodeEx((void*)(intptr_t)node_n, node_flags, "Selectable Leaf %d", node_n);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) if (use_drag_and_drop && ImGui::BeginDragDropSource())
node_clicked = i;
if (test_drag_and_drop && ImGui::BeginDragDropSource())
{ {
ImGui::SetDragDropPayload("_TREENODE", NULL, 0); ImGui::SetDragDropPayload("MY_TREENODE_PAYLOAD_TYPE", NULL, 0);
ImGui::Text("This is a drag and drop source"); ImGui::Text("This is a drag and drop source");
ImGui::EndDragDropSource(); ImGui::EndDragDropSource();
} }
} }
} }
if (node_clicked != -1)
{
// Update selection state
// (process outside of tree loop to avoid visual inconsistencies during the clicking frame)
if (ImGui::GetIO().KeyCtrl)
selection_mask ^= (1 << node_clicked); // Ctrl+Click to toggle
else //if (!(selection_mask & (1 << node_clicked))) // Depending on selection behavior you want, may want to preserve selection when clicking on item that is part of the selection
selection_mask = (1 << node_clicked); // Click to single-select
}
if (align_label_with_current_x_position) if (align_label_with_current_x_position)
ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing()); ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing());
ImGui::TreePop(); ImGui::TreePop();
@@ -8941,7 +9034,7 @@ static void ShowExampleMenuFile()
IMGUI_DEMO_MARKER("Examples/Menu/Options"); IMGUI_DEMO_MARKER("Examples/Menu/Options");
static bool enabled = true; static bool enabled = true;
ImGui::MenuItem("Enabled", "", &enabled); ImGui::MenuItem("Enabled", "", &enabled);
ImGui::BeginChild("child", ImVec2(0, 60), ImGuiChildFlags_Borders); ImGui::BeginChild("child", ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 5.0f), ImGuiChildFlags_Borders);
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
ImGui::Text("Scrolling Text %d", i); ImGui::Text("Scrolling Text %d", i);
ImGui::EndChild(); ImGui::EndChild();
@@ -9353,6 +9446,28 @@ static void ShowExampleAppConsole(bool* p_open)
console.Draw("Example: Console", p_open); console.Draw("Example: Console", p_open);
} }
//-----------------------------------------------------------------------------
// [SECTION] Example App: Image Viewer / ShowExampleAppImageViewer()
//-----------------------------------------------------------------------------
static void ShowExampleAppImageViewer(bool* p_open)
{
ImFontAtlas* atlas = ImGui::GetIO().Fonts;
ImTextureRef tex_ref = atlas->TexRef; // We don't have access to other textures in this demo!
int tex_w = atlas->TexData->Width;
int tex_h = atlas->TexData->Height;
if (ImGui::Begin("Example: Image Viewer", p_open))
{
static ExampleImageViewerData image_viewer;
ExampleImageViewer_DrawOptions(&image_viewer);
ImVec2 canvas_size = ImGui::GetContentRegionAvail();
ImVec2 canvas_min_size = ImGui::IsWindowAppearing() ? ImVec2(3.0f * tex_w, 4.0f * tex_h) : ImVec2(1.0f, 1.0f);
canvas_size = ImVec2(IM_MAX(canvas_size.x, canvas_min_size.x), IM_MAX(canvas_size.y, canvas_min_size.y));
ExampleImageViewer_DrawCanvas(&image_viewer, canvas_size, tex_ref, tex_w, tex_h);
}
ImGui::End();
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// [SECTION] Example App: Debug Log / ShowExampleAppLog() // [SECTION] Example App: Debug Log / ShowExampleAppLog()
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@@ -10248,20 +10363,20 @@ static void ShowExampleAppCustomRendering(bool* p_open)
draw_list->AddNgon(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, ngon_sides, th); x += sz + spacing; // N-gon draw_list->AddNgon(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, ngon_sides, th); x += sz + spacing; // N-gon
draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments, th); x += sz + spacing; // Circle draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments, th); x += sz + spacing; // Circle
draw_list->AddEllipse(ImVec2(x + sz*0.5f, y + sz*0.5f), ImVec2(sz*0.5f, sz*0.3f), col, -0.3f, circle_segments, th); x += sz + spacing; // Ellipse draw_list->AddEllipse(ImVec2(x + sz*0.5f, y + sz*0.5f), ImVec2(sz*0.5f, sz*0.3f), col, -0.3f, circle_segments, th); x += sz + spacing; // Ellipse
draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 0.0f, ImDrawFlags_None, th); x += sz + spacing; // Square draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 0.0f, th); x += sz + spacing; // Square
draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, ImDrawFlags_None, th); x += sz + spacing; // Square with all rounded corners draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, th); x += sz + spacing; // Square with all rounded corners
draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, corners_tl_br, th); x += sz + spacing; // Square with two rounded corners draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, th, corners_tl_br); x += sz + spacing; // Square with two rounded corners
draw_list->AddTriangle(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col, th);x += sz + spacing; // Triangle draw_list->AddTriangle(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col, th);x += sz + spacing; // Triangle
//draw_list->AddTriangle(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col, th);x+= sz*0.4f + spacing; // Thin triangle //draw_list->AddTriangle(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col, th);x+= sz*0.4f + spacing; // Thin triangle
PathConcaveShape(draw_list, x, y, sz); draw_list->PathStroke(col, ImDrawFlags_Closed, th); x += sz + spacing; // Concave Shape PathConcaveShape(draw_list, x, y, sz); draw_list->PathStroke(col, th, ImDrawFlags_Closed); x += sz + spacing; // Concave Shape
//draw_list->AddPolyline(concave_shape, IM_COUNTOF(concave_shape), col, ImDrawFlags_Closed, th); //draw_list->AddPolyline(concave_shape, IM_COUNTOF(concave_shape), col, ImDrawFlags_Closed, th);
draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y), col, th); x += sz + spacing; // Horizontal line (note: drawing a filled rectangle will be faster!) draw_list->AddLineH(x, x + sz, y, col, th); x += sz + spacing; // Horizontal line (note: drawing a filled rectangle will be faster!)
draw_list->AddLine(ImVec2(x, y), ImVec2(x, y + sz), col, th); x += spacing; // Vertical line (note: drawing a filled rectangle will be faster!) draw_list->AddLineV(x, y, y + sz, col, th); x += spacing; // Vertical line (note: drawing a filled rectangle will be faster!)
draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y + sz), col, th); x += sz + spacing; // Diagonal line draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y + sz), col, th); x += sz + spacing; // Diagonal line
// Path // Path
draw_list->PathArcTo(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, 3.141592f, 3.141592f * -0.5f); draw_list->PathArcTo(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, 3.141592f, 3.141592f * -0.5f);
draw_list->PathStroke(col, ImDrawFlags_None, th); draw_list->PathStroke(col, th);
x += sz + spacing; x += sz + spacing;
// Quadratic Bezier Curve (3 control points) // Quadratic Bezier Curve (3 control points)
@@ -10395,9 +10510,9 @@ static void ShowExampleAppCustomRendering(bool* p_open)
{ {
const float GRID_STEP = 64.0f; const float GRID_STEP = 64.0f;
for (float x = fmodf(scrolling.x, GRID_STEP); x < canvas_sz.x; x += GRID_STEP) for (float x = fmodf(scrolling.x, GRID_STEP); x < canvas_sz.x; x += GRID_STEP)
draw_list->AddLine(ImVec2(canvas_p0.x + x, canvas_p0.y), ImVec2(canvas_p0.x + x, canvas_p1.y), IM_COL32(200, 200, 200, 40)); draw_list->AddLineV(canvas_p0.x + x, canvas_p0.y, canvas_p1.y, IM_COL32(200, 200, 200, 40));
for (float y = fmodf(scrolling.y, GRID_STEP); y < canvas_sz.y; y += GRID_STEP) for (float y = fmodf(scrolling.y, GRID_STEP); y < canvas_sz.y; y += GRID_STEP)
draw_list->AddLine(ImVec2(canvas_p0.x, canvas_p0.y + y), ImVec2(canvas_p1.x, canvas_p0.y + y), IM_COL32(200, 200, 200, 40)); draw_list->AddLineH(canvas_p0.x, canvas_p1.x, canvas_p0.y + y, IM_COL32(200, 200, 200, 40));
} }
for (int n = 0; n < points.Size; n += 2) for (int n = 0; n < points.Size; n += 2)
draw_list->AddLine(ImVec2(origin.x + points[n].x, origin.y + points[n].y), ImVec2(origin.x + points[n + 1].x, origin.y + points[n + 1].y), IM_COL32(255, 255, 0, 255), 2.0f); draw_list->AddLine(ImVec2(origin.x + points[n].x, origin.y + points[n].y), ImVec2(origin.x + points[n + 1].x, origin.y + points[n + 1].y), IM_COL32(255, 255, 0, 255), 2.0f);
@@ -11067,10 +11182,11 @@ struct ExampleAssetsBrowser
bool AllowBoxSelect = true; // Will set ImGuiMultiSelectFlags_BoxSelect2d bool AllowBoxSelect = true; // Will set ImGuiMultiSelectFlags_BoxSelect2d
bool AllowBoxSelectInsideSelection = false; // Will set ImGuiMultiSelectFlags_SelectOnClickAlways bool AllowBoxSelectInsideSelection = false; // Will set ImGuiMultiSelectFlags_SelectOnClickAlways
bool AllowDragUnselected = false; // Will set ImGuiMultiSelectFlags_SelectOnClickRelease bool AllowDragUnselected = false; // Will set ImGuiMultiSelectFlags_SelectOnClickRelease
float IconSize = 32.0f; float IconSize = 0;
int IconSpacing = 10; int IconSpacing = 10;
int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer.
bool StretchSpacing = true; bool StretchSpacing = true;
bool UseScrollX = false; // Debug: submit twice the number of items per line (overflow horizontally to exercise ScrollX + box-select)
// State // State
ImVector<ExampleAsset> Items; // Our items ImVector<ExampleAsset> Items; // Our items
@@ -11121,12 +11237,15 @@ struct ExampleAssetsBrowser
// Layout: calculate number of icon per line and number of lines // Layout: calculate number of icon per line and number of lines
LayoutItemSize = ImVec2(floorf(IconSize), floorf(IconSize)); LayoutItemSize = ImVec2(floorf(IconSize), floorf(IconSize));
LayoutColumnCount = IM_MAX((int)(avail_width / (LayoutItemSize.x + LayoutItemSpacing)), 1); LayoutColumnCount = IM_MAX((int)(avail_width / (LayoutItemSize.x + LayoutItemSpacing)), 1);
LayoutLineCount = (Items.Size + LayoutColumnCount - 1) / LayoutColumnCount;
// Layout: when stretching: allocate remaining space to more spacing. Round before division, so item_spacing may be non-integer. // Layout: when stretching: allocate remaining space to more spacing. Round before division, so item_spacing may be non-integer.
if (StretchSpacing && LayoutColumnCount > 1) if (StretchSpacing && LayoutColumnCount > 1)
LayoutItemSpacing = floorf(avail_width - LayoutItemSize.x * LayoutColumnCount) / LayoutColumnCount; LayoutItemSpacing = floorf(avail_width - LayoutItemSize.x * LayoutColumnCount) / LayoutColumnCount;
if (UseScrollX)
LayoutColumnCount *= 2;
LayoutLineCount = (Items.Size + LayoutColumnCount - 1) / LayoutColumnCount;
LayoutItemStep = ImVec2(LayoutItemSize.x + LayoutItemSpacing, LayoutItemSize.y + LayoutItemSpacing); LayoutItemStep = ImVec2(LayoutItemSize.x + LayoutItemSpacing, LayoutItemSize.y + LayoutItemSpacing);
LayoutSelectableSpacing = IM_MAX(floorf(LayoutItemSpacing) - IconHitSpacing, 0.0f); LayoutSelectableSpacing = IM_MAX(floorf(LayoutItemSpacing) - IconHitSpacing, 0.0f);
LayoutOuterPadding = floorf(LayoutItemSpacing * 0.5f); LayoutOuterPadding = floorf(LayoutItemSpacing * 0.5f);
@@ -11134,6 +11253,9 @@ struct ExampleAssetsBrowser
void Draw(const char* title, bool* p_open) void Draw(const char* title, bool* p_open)
{ {
if (IconSize <= 0.0f)
IconSize = ImGui::CalcTextSize("99999").x;
ImGui::SetNextWindowSize(ImVec2(IconSize * 25, IconSize * 15), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(IconSize * 25, IconSize * 15), ImGuiCond_FirstUseEver);
if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_MenuBar)) if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_MenuBar))
{ {
@@ -11183,6 +11305,7 @@ struct ExampleAssetsBrowser
ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32); ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32);
ImGui::SliderInt("Icon Hit Spacing", &IconHitSpacing, 0, 32); ImGui::SliderInt("Icon Hit Spacing", &IconHitSpacing, 0, 32);
ImGui::Checkbox("Stretch Spacing", &StretchSpacing); ImGui::Checkbox("Stretch Spacing", &StretchSpacing);
ImGui::Checkbox("Use ScrollX", &UseScrollX);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::EndMenu(); ImGui::EndMenu();
} }
@@ -11212,7 +11335,7 @@ struct ExampleAssetsBrowser
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowContentSize(ImVec2(0.0f, LayoutOuterPadding + LayoutLineCount * (LayoutItemSize.y + LayoutItemSpacing))); ImGui::SetNextWindowContentSize(ImVec2(0.0f, LayoutOuterPadding + LayoutLineCount * (LayoutItemSize.y + LayoutItemSpacing)));
if (ImGui::BeginChild("Assets", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing()), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove)) if (ImGui::BeginChild("Assets", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing()), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_HorizontalScrollbar))
{ {
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList* draw_list = ImGui::GetWindowDrawList();
@@ -11356,6 +11479,8 @@ struct ExampleAssetsBrowser
} }
} }
clipper.End(); clipper.End();
if (Items.Size == 0)
ImGui::Dummy(ImVec2(0, 0));
ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing
// Context menu // Context menu
+232 -164
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7 // dear imgui, v1.92.9 WIP
// (drawing and font code) // (drawing and font code)
/* /*
@@ -208,6 +208,7 @@ void ImGui::StyleColorsDark(ImGuiStyle* dst)
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f); colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f); colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f);
colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
colors[ImGuiCol_CheckboxSelectedBg] = ImLerp(colors[ImGuiCol_FrameBg], colors[ImGuiCol_FrameBgHovered], 0.65f);
colors[ImGuiCol_SliderGrab] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f); colors[ImGuiCol_SliderGrab] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f);
@@ -277,6 +278,7 @@ void ImGui::StyleColorsClassic(ImGuiStyle* dst)
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.80f, 0.40f); colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.80f, 0.40f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f);
colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f); colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f);
colors[ImGuiCol_CheckboxSelectedBg] = ImLerp(colors[ImGuiCol_FrameBg], colors[ImGuiCol_FrameBgActive], 0.65f);
colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f); colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); colors[ImGuiCol_SliderGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f);
colors[ImGuiCol_Button] = ImVec4(0.35f, 0.40f, 0.61f, 0.62f); colors[ImGuiCol_Button] = ImVec4(0.35f, 0.40f, 0.61f, 0.62f);
@@ -347,6 +349,7 @@ void ImGui::StyleColorsLight(ImGuiStyle* dst)
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.49f, 0.49f, 0.49f, 0.80f); colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.49f, 0.49f, 0.49f, 0.80f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f); colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f);
colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
colors[ImGuiCol_CheckboxSelectedBg] = ImVec4(0.95f, 0.97f, 1.00f, 1.00f);
colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.59f, 0.98f, 0.78f); colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.59f, 0.98f, 0.78f);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.46f, 0.54f, 0.80f, 0.60f); colors[ImGuiCol_SliderGrabActive] = ImVec4(0.46f, 0.54f, 0.80f, 0.60f);
colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f);
@@ -405,7 +408,6 @@ ImDrawListSharedData::ImDrawListSharedData()
const float a = ((float)i * 2 * IM_PI) / (float)IM_COUNTOF(ArcFastVtx); const float a = ((float)i * 2 * IM_PI) / (float)IM_COUNTOF(ArcFastVtx);
ArcFastVtx[i] = ImVec2(ImCos(a), ImSin(a)); ArcFastVtx[i] = ImVec2(ImCos(a), ImSin(a));
} }
ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError);
} }
ImDrawListSharedData::~ImDrawListSharedData() ImDrawListSharedData::~ImDrawListSharedData()
@@ -415,17 +417,17 @@ ImDrawListSharedData::~ImDrawListSharedData()
void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error) void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error)
{ {
if (CircleSegmentMaxError == max_error) if (CircleTessellationMaxError == max_error)
return; return;
IM_ASSERT(max_error > 0.0f); IM_ASSERT(max_error > 0.0f);
CircleSegmentMaxError = max_error; CircleTessellationMaxError = max_error;
for (int i = 0; i < IM_COUNTOF(CircleSegmentCounts); i++) for (int i = 0; i < IM_COUNTOF(CircleSegmentCounts); i++)
{ {
const float radius = (float)i; const float radius = (float)i;
CircleSegmentCounts[i] = (ImU8)((i > 0) ? IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError) : IM_DRAWLIST_ARCFAST_SAMPLE_MAX); CircleSegmentCounts[i] = (ImU8)((i > 0) ? IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleTessellationMaxError) : IM_DRAWLIST_ARCFAST_SAMPLE_MAX);
} }
ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleTessellationMaxError);
} }
ImDrawList::ImDrawList(ImDrawListSharedData* shared_data) ImDrawList::ImDrawList(ImDrawListSharedData* shared_data)
@@ -534,9 +536,14 @@ void ImDrawList::_PopUnusedDrawCmd()
void ImDrawList::AddCallback(ImDrawCallback callback, void* userdata, size_t userdata_size) void ImDrawList::AddCallback(ImDrawCallback callback, void* userdata, size_t userdata_size)
{ {
IM_ASSERT(callback != NULL);
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
if (callback == ImDrawCallback_ResetRenderState && _Data->Context != NULL && _Data->Context->PlatformIO.DrawCallback_ResetRenderState != NULL)
callback = _Data->Context->PlatformIO.DrawCallback_ResetRenderState; // == ImGui::GetPlatformIO().DrawCallback_ResetRenderState
#endif
IM_ASSERT_PARANOID(CmdBuffer.Size > 0); IM_ASSERT_PARANOID(CmdBuffer.Size > 0);
ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1];
IM_ASSERT(callback != NULL);
IM_ASSERT(curr_cmd->UserCallback == NULL); IM_ASSERT(curr_cmd->UserCallback == NULL);
if (curr_cmd->ElemCount != 0) if (curr_cmd->ElemCount != 0)
{ {
@@ -654,11 +661,11 @@ void ImDrawList::_OnChangedVtxOffset()
int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const
{ {
// Automatic segment count // Automatic segment count
const int radius_idx = (int)(radius + 0.999999f); // ceil to never reduce accuracy const int radius_idx = (int)(radius + 0.999f); // ceil to never reduce accuracy
if (radius_idx >= 0 && radius_idx < IM_COUNTOF(_Data->CircleSegmentCounts)) if (radius_idx >= 0 && radius_idx < IM_COUNTOF(_Data->CircleSegmentCounts))
return _Data->CircleSegmentCounts[radius_idx]; // Use cached value return _Data->CircleSegmentCounts[radius_idx]; // Use cached value
else else
return IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError); return IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleTessellationMaxError);
} }
// Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling)
@@ -668,7 +675,7 @@ void ImDrawList::PushClipRect(const ImVec2& cr_min, const ImVec2& cr_max, bool i
if (intersect_with_current_clip_rect) if (intersect_with_current_clip_rect)
{ {
ImVec4 current = _CmdHeader.ClipRect; ImVec4 current = _CmdHeader.ClipRect;
if (cr.x < current.x) cr.x = current.x; if (cr.x < current.x) cr.x = current.x; // = ClipWith(). Note that passing inverted range wouldn't be fixed here.
if (cr.y < current.y) cr.y = current.y; if (cr.y < current.y) cr.y = current.y;
if (cr.z > current.z) cr.z = current.z; if (cr.z > current.z) cr.z = current.z;
if (cr.w > current.w) cr.w = current.w; if (cr.w > current.w) cr.w = current.w;
@@ -812,7 +819,7 @@ void ImDrawList::PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, c
// TODO: Thickness anti-aliased lines cap are missing their AA fringe. // TODO: Thickness anti-aliased lines cap are missing their AA fringe.
// We avoid using the ImVec2 math operators here to reduce cost to a minimum for debug/non-inlined builds. // We avoid using the ImVec2 math operators here to reduce cost to a minimum for debug/non-inlined builds.
void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, ImDrawFlags flags, float thickness) void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, float thickness, ImDrawFlags flags)
{ {
if (points_count < 2 || (col & IM_COL32_A_MASK) == 0) if (points_count < 2 || (col & IM_COL32_A_MASK) == 0)
return; return;
@@ -822,6 +829,12 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32
const int count = closed ? points_count : points_count - 1; // The number of line segments we need to draw const int count = closed ? points_count : points_count - 1; // The number of line segments we need to draw
const bool thick_line = (thickness > _FringeScale); const bool thick_line = (thickness > _FringeScale);
// If this assert triggers on legacy code:
// - 1.92.8 (2025/05): swapped two last parameters order: flags, thickness --> thickness, flags. This should normally be caught by compile-time type-checking.
// - 1.92.8 (2025/05): changed value of ImDrawList_Closed which was previously guaranteed to be == 1. Hardcoded use of 1 or true should be replaced.
// Read more details near AddRect() + see "API BREAKING CHANGES" section for 1.82, 1.90 and 1.92.8.
IM_ASSERT_USER_ERROR_RET((flags & ImDrawFlags_InvalidMask_) == 0, "Incorrect parameter. Did you swap 'thickness' and 'flags'?");
if (Flags & ImDrawListFlags_AntiAliasedLines) if (Flags & ImDrawListFlags_AntiAliasedLines)
{ {
// Anti-aliased stroke // Anti-aliased stroke
@@ -1438,35 +1451,13 @@ void ImDrawList::PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3,
} }
} }
static inline ImDrawFlags FixRectCornerFlags(ImDrawFlags flags)
{
/*
IM_STATIC_ASSERT(ImDrawFlags_RoundCornersTopLeft == (1 << 4));
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
// Obsoleted in 1.82 (from February 2021). This code was stripped/simplified and mostly commented in 1.90 (from September 2023)
// - Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All)
if (flags == ~0) { return ImDrawFlags_RoundCornersAll; }
// - Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations). Read details in older version of this code.
if (flags >= 0x01 && flags <= 0x0F) { return (flags << 4); }
// We cannot support hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f'
#endif
*/
// If this assert triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values.
// Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc. anyway.
// See details in 1.82 Changelog as well as 2021/03/12 and 2023/09/08 entries in "API BREAKING CHANGES" section.
IM_ASSERT((flags & 0x0F) == 0 && "Misuse of legacy hardcoded ImDrawCornerFlags values!");
if ((flags & ImDrawFlags_RoundCornersMask_) == 0)
flags |= ImDrawFlags_RoundCornersAll;
return flags;
}
void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDrawFlags flags) void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDrawFlags flags)
{ {
if (rounding >= 0.5f) if (rounding >= 0.5f)
{ {
flags = FixRectCornerFlags(flags); if ((flags & ImDrawFlags_RoundCornersMask_) == 0)
flags |= ImDrawFlags_RoundCornersAll;
rounding = ImMin(rounding, ImFabs(b.x - a.x) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f); rounding = ImMin(rounding, ImFabs(b.x - a.x) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f);
rounding = ImMin(rounding, ImFabs(b.y - a.y) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f); rounding = ImMin(rounding, ImFabs(b.y - a.y) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f);
} }
@@ -1494,22 +1485,48 @@ void ImDrawList::AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float th
{ {
if ((col & IM_COL32_A_MASK) == 0) if ((col & IM_COL32_A_MASK) == 0)
return; return;
PathLineTo(p1 + ImVec2(0.5f, 0.5f)); const ImVec2 points[2] = { ImVec2(p1.x + 0.5f, p1.y + 0.5f), ImVec2(p2.x + 0.5f, p2.y + 0.5f) };
PathLineTo(p2 + ImVec2(0.5f, 0.5f)); AddPolyline(points, 2, col, thickness);
PathStroke(col, 0, thickness); }
void ImDrawList::AddLineH(float min_x, float max_x, float y, ImU32 col, float thickness)
{
if ((col & IM_COL32_A_MASK) == 0)
return;
const ImVec2 points[2] = { ImVec2(min_x + 0.5f, y + 0.5f), ImVec2(max_x + 0.5f, y + 0.5f) }; // Same as AddLine() above.
AddPolyline(points, 2, col, thickness);
}
void ImDrawList::AddLineV(float x, float min_y, float max_y, ImU32 col, float thickness)
{
if ((col & IM_COL32_A_MASK) == 0)
return;
const ImVec2 points[2] = { ImVec2(x + 0.5f, min_y + 0.5f), ImVec2(x + 0.5f, max_y + 0.5f) }; // Same as AddLine() above.
AddPolyline(points, 2, col, thickness);
} }
// p_min = upper-left, p_max = lower-right // p_min = upper-left, p_max = lower-right
// Note we don't render 1 pixels sized rectangles properly. // Note we don't render 1 pixels sized rectangles properly.
void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness) void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, float thickness, ImDrawFlags flags)
{ {
// If this assert triggers on legacy code:
// - 1.92.8 (2025/05): swapped two last parameters order: flags, thickness --> thickness, flags. This should normally be caught by compile-time type-checking.
// - 1.92.8 (2025/05): changed value of ImDrawList_Closed which was previously guaranteed to be == 1. Hardcoded use of 1 or true should be replaced.
// - 1.82.0 (2021/03): changed ImDrawCornerFlags to ImDrawFlags_RoundCornersXXX values.
// If you used hard-coded 1 to 15 or ~0 in flags to configure corner rounding use the new flags!
// - Hard coded support for ~0 == ImDrawFlags_RoundCornersAll.
// - Hard coded support for values 0x01 to 0x0F (matching 15 out of 16 old flags combinations) --> see FixRectCornerFlags() in <1.90 code.
// - Hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f'.
// See "API BREAKING CHANGES" section for 1.82, 1.90 and 1.92.8.
IM_ASSERT_USER_ERROR_RET((flags & ImDrawFlags_InvalidMask_) == 0, "Incorrect parameter. Did you swap 'thickness' and 'flags'?"); // Or misuse of legacy hard-coded ImDrawCornerFlags values
if ((col & IM_COL32_A_MASK) == 0) if ((col & IM_COL32_A_MASK) == 0)
return; return;
if (Flags & ImDrawListFlags_AntiAliasedLines) if (Flags & ImDrawListFlags_AntiAliasedLines)
PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.50f, 0.50f), rounding, flags); PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.50f, 0.50f), rounding, flags);
else else
PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.49f, 0.49f), rounding, flags); // Better looking lower-right corner and rounded non-AA shapes. PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.49f, 0.49f), rounding, flags); // Better looking lower-right corner and rounded non-AA shapes.
PathStroke(col, ImDrawFlags_Closed, thickness); PathStroke(col, thickness, ImDrawFlags_Closed);
} }
void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags) void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags)
@@ -1553,7 +1570,7 @@ void ImDrawList::AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, c
PathLineTo(p2); PathLineTo(p2);
PathLineTo(p3); PathLineTo(p3);
PathLineTo(p4); PathLineTo(p4);
PathStroke(col, ImDrawFlags_Closed, thickness); PathStroke(col, thickness, ImDrawFlags_Closed);
} }
void ImDrawList::AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col) void ImDrawList::AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col)
@@ -1576,7 +1593,7 @@ void ImDrawList::AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p
PathLineTo(p1); PathLineTo(p1);
PathLineTo(p2); PathLineTo(p2);
PathLineTo(p3); PathLineTo(p3);
PathStroke(col, ImDrawFlags_Closed, thickness); PathStroke(col, thickness, ImDrawFlags_Closed);
} }
void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col) void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col)
@@ -1611,7 +1628,7 @@ void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int nu
PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1);
} }
PathStroke(col, ImDrawFlags_Closed, thickness); PathStroke(col, thickness, ImDrawFlags_Closed);
} }
void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments)
@@ -1647,7 +1664,7 @@ void ImDrawList::AddNgon(const ImVec2& center, float radius, ImU32 col, int num_
// Because we are filling a closed shape we remove 1 from the count of segments/points // Because we are filling a closed shape we remove 1 from the count of segments/points
const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments;
PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1);
PathStroke(col, ImDrawFlags_Closed, thickness); PathStroke(col, thickness, ImDrawFlags_Closed);
} }
// Guaranteed to honor 'num_segments' // Guaranteed to honor 'num_segments'
@@ -1674,7 +1691,7 @@ void ImDrawList::AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 co
// Because we are filling a closed shape we remove 1 from the count of segments/points // Because we are filling a closed shape we remove 1 from the count of segments/points
const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments;
PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1);
PathStroke(col, true, thickness); PathStroke(col, thickness, ImDrawFlags_Closed);
} }
void ImDrawList::AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments) void ImDrawList::AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments)
@@ -1699,7 +1716,7 @@ void ImDrawList::AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2
PathLineTo(p1); PathLineTo(p1);
PathBezierCubicCurveTo(p2, p3, p4, num_segments); PathBezierCubicCurveTo(p2, p3, p4, num_segments);
PathStroke(col, 0, thickness); PathStroke(col, thickness);
} }
// Quadratic Bezier takes 3 controls points // Quadratic Bezier takes 3 controls points
@@ -1710,7 +1727,7 @@ void ImDrawList::AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const Im
PathLineTo(p1); PathLineTo(p1);
PathBezierQuadraticCurveTo(p2, p3, num_segments); PathBezierQuadraticCurveTo(p2, p3, num_segments);
PathStroke(col, 0, thickness); PathStroke(col, thickness);
} }
void ImDrawList::AddText(ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) void ImDrawList::AddText(ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect)
@@ -1782,7 +1799,10 @@ void ImDrawList::AddImageRounded(ImTextureRef tex_ref, const ImVec2& p_min, cons
if ((col & IM_COL32_A_MASK) == 0) if ((col & IM_COL32_A_MASK) == 0)
return; return;
flags = FixRectCornerFlags(flags); IM_ASSERT((flags & 0x0F) == 0 && "Misuse of legacy hardcoded ImDrawCornerFlags values!"); // If this assert triggers on legacy code: see comments in ImDrawList::PathRect().
if ((flags & ImDrawFlags_RoundCornersMask_) == 0)
flags |= ImDrawFlags_RoundCornersAll;
if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone)
{ {
AddImage(tex_ref, p_min, p_max, uv_min, uv_max, col); AddImage(tex_ref, p_min, p_max, uv_min, uv_max, col);
@@ -2507,10 +2527,11 @@ void ImTextureData::DestroyPixels()
// - Default texture data encoded in ASCII // - Default texture data encoded in ASCII
// - ImFontAtlas() // - ImFontAtlas()
// - ImFontAtlas::Clear() // - ImFontAtlas::Clear()
// - ImFontAtlas::CompactCache() // - ImFontAtlas::ClearFonts()
// - ImFontAtlas::ClearInputData() // - ImFontAtlas::ClearInputData()
// - ImFontAtlas::ClearTexData() // - ImFontAtlas::ClearTexData()
// - ImFontAtlas::ClearFonts() // - ImFontAtlas::CompactCache()
// - ImFontAtlas::SetFontLoader()
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// - ImFontAtlasUpdateNewFrame() // - ImFontAtlasUpdateNewFrame()
// - ImFontAtlasTextureBlockConvert() // - ImFontAtlasTextureBlockConvert()
@@ -2547,8 +2568,6 @@ void ImTextureData::DestroyPixels()
// - ImFontAtlasBuildPreloadAllGlyphRanges() // - ImFontAtlasBuildPreloadAllGlyphRanges()
// - ImFontAtlasBuildUpdatePointers() // - ImFontAtlasBuildUpdatePointers()
// - ImFontAtlasBuildRenderBitmapFromString() // - ImFontAtlasBuildRenderBitmapFromString()
// - ImFontAtlasBuildUpdateBasicTexData()
// - ImFontAtlasBuildUpdateLinesTexData()
// - ImFontAtlasBuildAddFont() // - ImFontAtlasBuildAddFont()
// - ImFontAtlasBuildSetupFontBakedEllipsis() // - ImFontAtlasBuildSetupFontBakedEllipsis()
// - ImFontAtlasBuildSetupFontBakedBlanks() // - ImFontAtlasBuildSetupFontBakedBlanks()
@@ -2565,13 +2584,14 @@ void ImTextureData::DestroyPixels()
// - ImFontAtlasUpdateDrawListsSharedData() // - ImFontAtlasUpdateDrawListsSharedData()
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// - ImFontAtlasBuildSetTexture() // - ImFontAtlasBuildSetTexture()
// - ImFontAtlasBuildAddTexture() // - ImFontAtlasBuildUpdateTexData()
// - ImFontAtlasBuildMakeSpace() // - ImFontAtlasTextureAdd()
// - ImFontAtlasBuildRepackTexture() // - ImFontAtlasTextureRepack()
// - ImFontAtlasBuildGrowTexture() // - ImFontAtlasTextureGrow()
// - ImFontAtlasBuildRepackOrGrowTexture() // - ImFontAtlasTextureMakeSpace()
// - ImFontAtlasBuildGetTextureSizeEstimate() // - ImFontAtlasTextureGetSizeEstimate()
// - ImFontAtlasBuildCompactTexture() // - ImFontAtlasBuildClear()
// - ImFontAtlasTextureCompact()
// - ImFontAtlasBuildInit() // - ImFontAtlasBuildInit()
// - ImFontAtlasBuildDestroy() // - ImFontAtlasBuildDestroy()
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@@ -2671,9 +2691,12 @@ ImFontAtlas::~ImFontAtlas()
TexData = NULL; TexData = NULL;
} }
// If you call this mid-frame, you would need to add new font and bind them! // You probably should not call this directly. It is not well specified.
// If you want to replace all your fonts mid-frame, most likely you should instead call ClearFonts() then load the new fonts.
// Calling this mid-frame will discard the CPU-side copy of the texture data which is generally unreliable as you may have textures queued for creation or updates.
void ImFontAtlas::Clear() void ImFontAtlas::Clear()
{ {
IMGUI_DEBUG_LOG_FONT("[font] ImFontAtlas::Clear()\n");
bool backup_renderer_has_textures = RendererHasTextures; bool backup_renderer_has_textures = RendererHasTextures;
RendererHasTextures = false; // Full Clear() is supported, but ClearTexData() only isn't. RendererHasTextures = false; // Full Clear() is supported, but ClearTexData() only isn't.
ClearFonts(); ClearFonts();
@@ -2681,20 +2704,28 @@ void ImFontAtlas::Clear()
RendererHasTextures = backup_renderer_has_textures; RendererHasTextures = backup_renderer_has_textures;
} }
void ImFontAtlas::CompactCache() void ImFontAtlas::ClearFonts()
{ {
ImFontAtlasTextureCompact(this); // FIXME-NEWATLAS: Illegal to remove currently bound font.
} IMGUI_DEBUG_LOG_FONT("[font] ImFontAtlas::ClearFonts()\n");
IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
void ImFontAtlas::SetFontLoader(const ImFontLoader* font_loader) for (ImFont* font : Fonts)
{ ImFontAtlasBuildNotifySetFont(this, font, NULL);
ImFontAtlasBuildSetupFontLoader(this, font_loader); ImFontAtlasBuildDestroy(this);
ClearInputData();
Fonts.clear_delete();
TexIsBuilt = false;
for (ImDrawListSharedData* shared_data : DrawListSharedDatas)
if (shared_data->FontAtlas == this)
{
shared_data->Font = NULL;
shared_data->FontScale = shared_data->FontSize = 0.0f;
}
} }
void ImFontAtlas::ClearInputData() void ImFontAtlas::ClearInputData()
{ {
IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!"); IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
for (ImFont* font : Fonts) for (ImFont* font : Fonts)
ImFontAtlasFontDestroyOutput(this, font); ImFontAtlasFontDestroyOutput(this, font);
for (ImFontConfig& font_cfg : Sources) for (ImFontConfig& font_cfg : Sources)
@@ -2718,22 +2749,14 @@ void ImFontAtlas::ClearTexData()
//Locked = true; // Hoped to be able to lock this down but some reload patterns may not be happy with it. //Locked = true; // Hoped to be able to lock this down but some reload patterns may not be happy with it.
} }
void ImFontAtlas::ClearFonts() void ImFontAtlas::CompactCache()
{ {
// FIXME-NEWATLAS: Illegal to remove currently bound font. ImFontAtlasTextureCompact(this);
IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!"); }
for (ImFont* font : Fonts)
ImFontAtlasBuildNotifySetFont(this, font, NULL); void ImFontAtlas::SetFontLoader(const ImFontLoader* font_loader)
ImFontAtlasBuildDestroy(this); {
ClearInputData(); ImFontAtlasBuildSetupFontLoader(this, font_loader);
Fonts.clear_delete();
TexIsBuilt = false;
for (ImDrawListSharedData* shared_data : DrawListSharedDatas)
if (shared_data->FontAtlas == this)
{
shared_data->Font = NULL;
shared_data->FontScale = shared_data->FontSize = 0.0f;
}
} }
static void ImFontAtlasBuildUpdateRendererHasTexturesFromContext(ImFontAtlas* atlas) static void ImFontAtlasBuildUpdateRendererHasTexturesFromContext(ImFontAtlas* atlas)
@@ -2760,6 +2783,28 @@ void ImFontAtlasUpdateNewFrame(ImFontAtlas* atlas, int frame_count, bool rendere
IM_ASSERT(atlas->Builder == NULL || atlas->Builder->FrameCount < frame_count); // Protection against being called twice. IM_ASSERT(atlas->Builder == NULL || atlas->Builder->FrameCount < frame_count); // Protection against being called twice.
atlas->RendererHasTextures = renderer_has_textures; atlas->RendererHasTextures = renderer_has_textures;
// Update texture status and discard old textures.
// (we do this first thing to handle an edge case: if user mistakenly calls ClearFonts()+SetStatus(OK) during
// rendering, it would ImFontAtlasBuildMain() rebuilding before tex->Updates[] gets a chance to be cleared)
// (if somehow we need to move this back lower in the function, we could manually call the code to clear Updates[]).
for (int tex_n = 0; tex_n < atlas->TexList.Size; tex_n++)
{
// Update and remove if requested
ImTextureData* tex = atlas->TexList[tex_n];
if (tex->Status == ImTextureStatus_WantCreate && atlas->RendererHasTextures)
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL && "Backend set texture's TexID/BackendUserData but did not update Status to OK.");
bool remove_from_list = ImTextureDataUpdateNewFrame(tex);
if (remove_from_list)
{
IM_ASSERT(atlas->TexData != tex);
tex->DestroyPixels();
IM_DELETE(tex);
atlas->TexList.erase(atlas->TexList.begin() + tex_n);
tex_n--;
}
}
// Check that font atlas was built or backend support texture reload in which case we can build now // Check that font atlas was built or backend support texture reload in which case we can build now
if (atlas->RendererHasTextures) if (atlas->RendererHasTextures)
{ {
@@ -2799,61 +2844,49 @@ void ImFontAtlasUpdateNewFrame(ImFontAtlas* atlas, int frame_count, bool rendere
builder->BakedPool.Size -= builder->BakedDiscardedCount; builder->BakedPool.Size -= builder->BakedDiscardedCount;
builder->BakedDiscardedCount = 0; builder->BakedDiscardedCount = 0;
} }
}
// Update texture status bool ImTextureDataUpdateNewFrame(ImTextureData* tex)
for (int tex_n = 0; tex_n < atlas->TexList.Size; tex_n++) {
bool remove_from_list = false;
if (tex->Status == ImTextureStatus_OK)
{ {
ImTextureData* tex = atlas->TexList[tex_n]; tex->Updates.resize(0);
bool remove_from_list = false; tex->UpdateRect.x = tex->UpdateRect.y = (unsigned short)~0;
if (tex->Status == ImTextureStatus_OK) tex->UpdateRect.w = tex->UpdateRect.h = 0;
{
tex->Updates.resize(0);
tex->UpdateRect.x = tex->UpdateRect.y = (unsigned short)~0;
tex->UpdateRect.w = tex->UpdateRect.h = 0;
}
if (tex->Status == ImTextureStatus_WantCreate && atlas->RendererHasTextures)
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL && "Backend set texture's TexID/BackendUserData but did not update Status to OK.");
// Request destroy
// - Keep bool to true in order to differentiate a planned destroy vs a destroy decided by the backend.
// - We don't destroy pixels right away, as backend may have an in-flight copy from RAM.
if (tex->WantDestroyNextFrame && tex->Status != ImTextureStatus_Destroyed && tex->Status != ImTextureStatus_WantDestroy)
{
IM_ASSERT(tex->Status == ImTextureStatus_OK || tex->Status == ImTextureStatus_WantCreate || tex->Status == ImTextureStatus_WantUpdates);
tex->Status = ImTextureStatus_WantDestroy;
}
// If a texture has never reached the backend, they don't need to know about it.
// (note: backends between 1.92.0 and 1.92.4 could set an already destroyed texture to ImTextureStatus_WantDestroy
// when invalidating graphics objects twice, which would previously remove it from the list and crash.)
if (tex->Status == ImTextureStatus_WantDestroy && tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL)
tex->Status = ImTextureStatus_Destroyed;
// Process texture being destroyed
if (tex->Status == ImTextureStatus_Destroyed)
{
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL && "Backend set texture Status to Destroyed but did not clear TexID/BackendUserData!");
if (tex->WantDestroyNextFrame)
remove_from_list = true; // Destroy was scheduled by us
else
tex->Status = ImTextureStatus_WantCreate; // Destroy was done was backend: recreate it (e.g. freed resources mid-run)
}
// The backend may need defer destroying by a few frames, to handle texture used by previous in-flight rendering.
// We allow the texture staying in _WantDestroy state and increment a counter which the backend can use to take its decision.
if (tex->Status == ImTextureStatus_WantDestroy)
tex->UnusedFrames++;
// Destroy and remove
if (remove_from_list)
{
IM_ASSERT(atlas->TexData != tex);
tex->DestroyPixels();
IM_DELETE(tex);
atlas->TexList.erase(atlas->TexList.begin() + tex_n);
tex_n--;
}
} }
// Request destroy
// - Keep bool to true in order to differentiate a planned destroy vs a destroy decided by the backend.
// - We don't destroy pixels right away, as backend may have an in-flight copy from RAM.
if (tex->WantDestroyNextFrame && tex->Status != ImTextureStatus_Destroyed && tex->Status != ImTextureStatus_WantDestroy)
{
IM_ASSERT(tex->Status == ImTextureStatus_OK || tex->Status == ImTextureStatus_WantCreate || tex->Status == ImTextureStatus_WantUpdates);
tex->Status = ImTextureStatus_WantDestroy;
}
// If a texture has never reached the backend, they don't need to know about it.
// (note: backends between 1.92.0 and 1.92.4 could set an already destroyed texture to ImTextureStatus_WantDestroy
// when invalidating graphics objects twice, which would previously remove it from the list and crash.)
if (tex->Status == ImTextureStatus_WantDestroy && tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL)
tex->Status = ImTextureStatus_Destroyed;
// Process texture being destroyed
if (tex->Status == ImTextureStatus_Destroyed)
{
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL && "Backend set texture Status to Destroyed but did not clear TexID/BackendUserData!");
if (tex->WantDestroyNextFrame)
remove_from_list = true; // Destroy was scheduled by us
else
tex->Status = ImTextureStatus_WantCreate; // Destroy was done was backend: recreate it (e.g. freed resources mid-run)
}
// The backend may need defer destroying by a few frames, to handle texture used by previous in-flight rendering.
// We allow the texture staying in _WantDestroy state and increment a counter which the backend can use to take its decision.
if (tex->Status == ImTextureStatus_WantDestroy)
tex->UnusedFrames++;
return remove_from_list;
} }
void ImFontAtlasTextureBlockConvert(const unsigned char* src_pixels, ImTextureFormat src_fmt, int src_pitch, unsigned char* dst_pixels, ImTextureFormat dst_fmt, int dst_pitch, int w, int h) void ImFontAtlasTextureBlockConvert(const unsigned char* src_pixels, ImTextureFormat src_fmt, int src_pitch, unsigned char* dst_pixels, ImTextureFormat dst_fmt, int dst_pitch, int w, int h)
@@ -2967,12 +3000,17 @@ void ImFontAtlasTextureBlockCopy(ImTextureData* src_tex, int src_x, int src_y, I
memcpy(dst_tex->GetPixelsAt(dst_x, dst_y + y), src_tex->GetPixelsAt(src_x, src_y + y), w * dst_tex->BytesPerPixel); memcpy(dst_tex->GetPixelsAt(dst_x, dst_y + y), src_tex->GetPixelsAt(src_x, src_y + y), w * dst_tex->BytesPerPixel);
} }
// Queue texture block update for renderer backend
void ImFontAtlasTextureBlockQueueUpload(ImFontAtlas* atlas, ImTextureData* tex, int x, int y, int w, int h) void ImFontAtlasTextureBlockQueueUpload(ImFontAtlas* atlas, ImTextureData* tex, int x, int y, int w, int h)
{
ImTextureDataQueueUpload(tex, x, y, w, h);
atlas->TexIsBuilt = false;
}
// Queue texture block update for renderer backend
void ImTextureDataQueueUpload(ImTextureData* tex, int x, int y, int w, int h)
{ {
IM_ASSERT(tex->Status != ImTextureStatus_WantDestroy && tex->Status != ImTextureStatus_Destroyed); IM_ASSERT(tex->Status != ImTextureStatus_WantDestroy && tex->Status != ImTextureStatus_Destroyed);
IM_ASSERT(x >= 0 && x <= 0xFFFF && y >= 0 && y <= 0xFFFF && w >= 0 && x + w <= 0x10000 && h >= 0 && y + h <= 0x10000); IM_ASSERT(x >= 0 && x <= 0xFFFF && y >= 0 && y <= 0xFFFF && w >= 0 && x + w <= 0x10000 && h >= 0 && y + h <= 0x10000);
IM_UNUSED(atlas);
ImTextureRect req = { (unsigned short)x, (unsigned short)y, (unsigned short)w, (unsigned short)h }; ImTextureRect req = { (unsigned short)x, (unsigned short)y, (unsigned short)w, (unsigned short)h };
int new_x1 = ImMax(tex->UpdateRect.w == 0 ? 0 : tex->UpdateRect.x + tex->UpdateRect.w, req.x + req.w); int new_x1 = ImMax(tex->UpdateRect.w == 0 ? 0 : tex->UpdateRect.x + tex->UpdateRect.w, req.x + req.w);
@@ -2985,7 +3023,6 @@ void ImFontAtlasTextureBlockQueueUpload(ImFontAtlas* atlas, ImTextureData* tex,
tex->UsedRect.y = ImMin(tex->UsedRect.y, req.y); tex->UsedRect.y = ImMin(tex->UsedRect.y, req.y);
tex->UsedRect.w = (unsigned short)(ImMax(tex->UsedRect.x + tex->UsedRect.w, req.x + req.w) - tex->UsedRect.x); tex->UsedRect.w = (unsigned short)(ImMax(tex->UsedRect.x + tex->UsedRect.w, req.x + req.w) - tex->UsedRect.x);
tex->UsedRect.h = (unsigned short)(ImMax(tex->UsedRect.y + tex->UsedRect.h, req.y + req.h) - tex->UsedRect.y); tex->UsedRect.h = (unsigned short)(ImMax(tex->UsedRect.y + tex->UsedRect.h, req.y + req.h) - tex->UsedRect.y);
atlas->TexIsBuilt = false;
// No need to queue if status is == ImTextureStatus_WantCreate // No need to queue if status is == ImTextureStatus_WantCreate
if (tex->Status == ImTextureStatus_OK || tex->Status == ImTextureStatus_WantUpdates) if (tex->Status == ImTextureStatus_OK || tex->Status == ImTextureStatus_WantUpdates)
@@ -3056,7 +3093,7 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg_in)
} }
else else
{ {
IM_ASSERT(Fonts.Size > 0 && "Cannot use MergeMode for the first font"); // When using MergeMode make sure that a font has already been added before. IM_ASSERT(Fonts.Size > 0 && "Cannot use MergeMode for the first font!"); // When using MergeMode make sure that a font has already been added before.
font = font_cfg_in->DstFont ? font_cfg_in->DstFont : Fonts.back(); font = font_cfg_in->DstFont ? font_cfg_in->DstFont : Fonts.back();
ImFontAtlasFontDiscardBakes(this, font, 0); // Need to discard bakes if the font was already used, because baked->FontLoaderDatas[] will change size. (#9162) ImFontAtlasFontDiscardBakes(this, font, 0); // Need to discard bakes if the font was already used, because baked->FontLoaderDatas[] will change size. (#9162)
} }
@@ -3084,6 +3121,11 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg_in)
IM_ASSERT(font_cfg->FontLoader->FontBakedLoadGlyph != NULL); IM_ASSERT(font_cfg->FontLoader->FontBakedLoadGlyph != NULL);
IM_ASSERT(font_cfg->FontLoader->LoaderInit == NULL && font_cfg->FontLoader->LoaderShutdown == NULL); // FIXME-NEWATLAS: Unsupported yet. IM_ASSERT(font_cfg->FontLoader->LoaderInit == NULL && font_cfg->FontLoader->LoaderShutdown == NULL); // FIXME-NEWATLAS: Unsupported yet.
} }
// | Target w/ Implicit RefSize | Target w/ Explicit RefSize |
// Adding w/ Implicit RefSize: | OK (same scale) | OK (same scale) |
// Adding w/ Explicit RefSize: | KO | OK (custom scale) |
if (font_cfg_in->MergeMode && font_cfg_in->SizePixels > 0)
IM_ASSERT((font->Flags & ImFontFlags_ImplicitRefSize) == 0 && "Cannot use MergeMode with an explicit reference size when the destination font used an implicit reference size!");
IM_ASSERT(font_cfg->FontLoaderData == NULL); IM_ASSERT(font_cfg->FontLoaderData == NULL);
if (!ImFontAtlasFontSourceInit(this, font_cfg)) if (!ImFontAtlasFontSourceInit(this, font_cfg))
@@ -3120,8 +3162,10 @@ static void Decode85(const unsigned char* src, unsigned char* dst)
dst += 4; dst += 4;
} }
} }
#ifndef IMGUI_DISABLE_DEFAULT_FONT #if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_BITMAP)
static const char* GetDefaultCompressedFontDataProggyClean(int* out_size); static const char* GetDefaultCompressedFontDataProggyClean(int* out_size);
#endif
#if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_VECTOR)
static const char* GetDefaultCompressedFontDataProggyForever(int* out_size); static const char* GetDefaultCompressedFontDataProggyForever(int* out_size);
#endif #endif
@@ -3146,12 +3190,15 @@ ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg)
// If you want a similar font which may be better scaled, consider using AddFontDefaultVector(). // If you want a similar font which may be better scaled, consider using AddFontDefaultVector().
ImFont* ImFontAtlas::AddFontDefaultBitmap(const ImFontConfig* font_cfg_template) ImFont* ImFontAtlas::AddFontDefaultBitmap(const ImFontConfig* font_cfg_template)
{ {
#ifndef IMGUI_DISABLE_DEFAULT_FONT #if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_BITMAP)
ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig();
if (!font_cfg_template) if (!font_cfg_template)
font_cfg.PixelSnapH = true; // Prevents sub-integer scaling factors at lower-level layers. font_cfg.PixelSnapH = true; // Prevents sub-integer scaling factors at lower-level layers.
if (font_cfg.SizePixels <= 0.0f) if (font_cfg.SizePixels <= 0.0f)
{
font_cfg.SizePixels = 13.0f; // This only serves (1) as a reference for GlyphOffset.y setting and (2) as a default for pre-1.92 backend. font_cfg.SizePixels = 13.0f; // This only serves (1) as a reference for GlyphOffset.y setting and (2) as a default for pre-1.92 backend.
font_cfg.Flags |= ImFontFlags_ImplicitRefSize;
}
if (font_cfg.Name[0] == '\0') if (font_cfg.Name[0] == '\0')
ImFormatString(font_cfg.Name, IM_COUNTOF(font_cfg.Name), "ProggyClean.ttf"); ImFormatString(font_cfg.Name, IM_COUNTOF(font_cfg.Name), "ProggyClean.ttf");
font_cfg.EllipsisChar = (ImWchar)0x0085; font_cfg.EllipsisChar = (ImWchar)0x0085;
@@ -3164,19 +3211,22 @@ ImFont* ImFontAtlas::AddFontDefaultBitmap(const ImFontConfig* font_cfg_template)
IM_ASSERT(0 && "Function is disabled in this build."); IM_ASSERT(0 && "Function is disabled in this build.");
IM_UNUSED(font_cfg_template); IM_UNUSED(font_cfg_template);
return NULL; return NULL;
#endif // #ifndef IMGUI_DISABLE_DEFAULT_FONT #endif
} }
// Load a minimal version of ProggyForever, designed to match our good old ProggyClean, but nicely scalable. // Load a minimal version of ProggyForever, designed to match our good old ProggyClean, but nicely scalable.
// (See build script in https://github.com/ocornut/proggyforever for details) // (See build script in https://github.com/ocornut/proggyforever for details)
ImFont* ImFontAtlas::AddFontDefaultVector(const ImFontConfig* font_cfg_template) ImFont* ImFontAtlas::AddFontDefaultVector(const ImFontConfig* font_cfg_template)
{ {
#ifndef IMGUI_DISABLE_DEFAULT_FONT #if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_VECTOR)
ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig();
if (!font_cfg_template) if (!font_cfg_template)
font_cfg.PixelSnapH = true; // Precisely match ProggyClean, but prevents sub-integer scaling factors at lower-level layers. font_cfg.PixelSnapH = true; // Precisely match ProggyClean, but prevents sub-integer scaling factors at lower-level layers.
if (font_cfg.SizePixels <= 0.0f) if (font_cfg.SizePixels <= 0.0f)
{
font_cfg.SizePixels = 13.0f; font_cfg.SizePixels = 13.0f;
font_cfg.Flags |= ImFontFlags_ImplicitRefSize;
}
if (font_cfg.Name[0] == '\0') if (font_cfg.Name[0] == '\0')
ImFormatString(font_cfg.Name, IM_COUNTOF(font_cfg.Name), "ProggyForever.ttf"); ImFormatString(font_cfg.Name, IM_COUNTOF(font_cfg.Name), "ProggyForever.ttf");
font_cfg.ExtraSizeScale *= 1.015f; // Match ProggyClean font_cfg.ExtraSizeScale *= 1.015f; // Match ProggyClean
@@ -3189,7 +3239,7 @@ ImFont* ImFontAtlas::AddFontDefaultVector(const ImFontConfig* font_cfg_template)
IM_ASSERT(0 && "Function is disabled in this build."); IM_ASSERT(0 && "Function is disabled in this build.");
IM_UNUSED(font_cfg_template); IM_UNUSED(font_cfg_template);
return NULL; return NULL;
#endif // #ifndef IMGUI_DISABLE_DEFAULT_FONT #endif
} }
ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges)
@@ -3545,7 +3595,7 @@ void ImFontAtlasBuildRenderBitmapFromString(ImFontAtlas* atlas, int x, int y, in
} }
} }
static void ImFontAtlasBuildUpdateBasicTexData(ImFontAtlas* atlas) static void ImFontAtlasBuildUpdateTexDataBasic(ImFontAtlas* atlas)
{ {
// Pack and store identifier so we can refresh UV coordinates on texture resize. // Pack and store identifier so we can refresh UV coordinates on texture resize.
// FIXME-NEWATLAS: User/custom rects where user code wants to store UV coordinates will need to do the same thing. // FIXME-NEWATLAS: User/custom rects where user code wants to store UV coordinates will need to do the same thing.
@@ -3579,7 +3629,7 @@ static void ImFontAtlasBuildUpdateBasicTexData(ImFontAtlas* atlas)
atlas->TexUvWhitePixel = ImVec2((r.x + 0.5f) * atlas->TexUvScale.x, (r.y + 0.5f) * atlas->TexUvScale.y); atlas->TexUvWhitePixel = ImVec2((r.x + 0.5f) * atlas->TexUvScale.x, (r.y + 0.5f) * atlas->TexUvScale.y);
} }
static void ImFontAtlasBuildUpdateLinesTexData(ImFontAtlas* atlas) static void ImFontAtlasBuildUpdateTexDataLines(ImFontAtlas* atlas)
{ {
if (atlas->Flags & ImFontAtlasFlags_NoBakedLines) if (atlas->Flags & ImFontAtlasFlags_NoBakedLines)
return; return;
@@ -4028,6 +4078,12 @@ static void ImFontAtlasBuildSetTexture(ImFontAtlas* atlas, ImTextureData* tex)
ImFontAtlasUpdateDrawListsTextures(atlas, old_tex_ref, atlas->TexRef); ImFontAtlasUpdateDrawListsTextures(atlas, old_tex_ref, atlas->TexRef);
} }
static void ImFontAtlasBuildUpdateTexData(ImFontAtlas* atlas)
{
ImFontAtlasBuildUpdateTexDataBasic(atlas);
ImFontAtlasBuildUpdateTexDataLines(atlas);
}
// Create a new texture, discard previous one // Create a new texture, discard previous one
ImTextureData* ImFontAtlasTextureAdd(ImFontAtlas* atlas, int w, int h) ImTextureData* ImFontAtlasTextureAdd(ImFontAtlas* atlas, int w, int h)
{ {
@@ -4142,8 +4198,7 @@ void ImFontAtlasTextureRepack(ImFontAtlas* atlas, int w, int h)
} }
// Update other cached UV // Update other cached UV
ImFontAtlasBuildUpdateLinesTexData(atlas); ImFontAtlasBuildUpdateTexData(atlas);
ImFontAtlasBuildUpdateBasicTexData(atlas);
builder->LockDisableResize = false; builder->LockDisableResize = false;
ImFontAtlasUpdateDrawListsSharedData(atlas); ImFontAtlasUpdateDrawListsSharedData(atlas);
@@ -4292,8 +4347,7 @@ void ImFontAtlasBuildInit(ImFontAtlas* atlas)
ImFontAtlasPackInit(atlas); ImFontAtlasPackInit(atlas);
// Add required texture data // Add required texture data
ImFontAtlasBuildUpdateLinesTexData(atlas); ImFontAtlasBuildUpdateTexData(atlas);
ImFontAtlasBuildUpdateBasicTexData(atlas);
// Register fonts // Register fonts
ImFontAtlasBuildUpdatePointers(atlas); ImFontAtlasBuildUpdatePointers(atlas);
@@ -4322,6 +4376,8 @@ void ImFontAtlasBuildDestroy(ImFontAtlas* atlas)
atlas->Builder = NULL; atlas->Builder = NULL;
} }
//-----------------------------------------------------------------------------
void ImFontAtlasPackInit(ImFontAtlas * atlas) void ImFontAtlasPackInit(ImFontAtlas * atlas)
{ {
ImTextureData* tex = atlas->TexData; ImTextureData* tex = atlas->TexData;
@@ -5718,8 +5774,13 @@ void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, Im
if (glyph->Colored) if (glyph->Colored)
col |= ~IM_COL32_A_MASK; col |= ~IM_COL32_A_MASK;
float scale = (size >= 0.0f) ? (size / baked->Size) : 1.0f; float scale = (size >= 0.0f) ? (size / baked->Size) : 1.0f;
float x = IM_TRUNC(pos.x); float x = pos.x;
float y = IM_TRUNC(pos.y); float y = pos.y;
if ((draw_list->Flags & ImDrawListFlags_TextNoPixelSnap) == 0)
{
x = IM_TRUNC(x);
y = IM_TRUNC(y);
}
float x1 = x + glyph->X0 * scale; float x1 = x + glyph->X0 * scale;
float x2 = x + glyph->X1 * scale; float x2 = x + glyph->X1 * scale;
@@ -5751,12 +5812,17 @@ void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, Im
// DO NOT CALL DIRECTLY THIS WILL CHANGE WILDLY IN 2026. Use ImDrawList::AddText(). // DO NOT CALL DIRECTLY THIS WILL CHANGE WILDLY IN 2026. Use ImDrawList::AddText().
void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, ImDrawTextFlags flags) void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, ImDrawTextFlags flags)
{ {
// Align to be pixel perfect
begin: begin:
float x = IM_TRUNC(pos.x); // Align to be pixel perfect
float y = IM_TRUNC(pos.y); float x = pos.x;
float y = pos.y;
if (y > clip_rect.w) if (y > clip_rect.w)
return; return;
if ((draw_list->Flags & ImDrawListFlags_TextNoPixelSnap) == 0)
{
x = IM_TRUNC(x);
y = IM_TRUNC(y);
}
if (!text_end) if (!text_end)
text_end = text_begin + ImStrlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls. text_end = text_begin + ImStrlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls.
@@ -6021,7 +6087,7 @@ void ImGui::RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float
draw_list->PathLineTo(ImVec2(bx - third, by - third)); draw_list->PathLineTo(ImVec2(bx - third, by - third));
draw_list->PathLineTo(ImVec2(bx, by)); draw_list->PathLineTo(ImVec2(bx, by));
draw_list->PathLineTo(ImVec2(bx + third * 2.0f, by - third * 2.0f)); draw_list->PathLineTo(ImVec2(bx + third * 2.0f, by - third * 2.0f));
draw_list->PathStroke(col, 0, thickness); draw_list->PathStroke(col, thickness);
} }
// Render an arrow. 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. // Render an arrow. 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side.
@@ -6151,8 +6217,8 @@ void ImGui::RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p
flags = ImDrawFlags_RoundCornersDefault_; flags = ImDrawFlags_RoundCornersDefault_;
if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF)
{ {
ImU32 col_bg1 = GetColorU32(ImAlphaBlendColors(IM_COL32(204, 204, 204, 255), col)); ImU32 col_bg1 = GetColorU32(ImAlphaBlendColors(IM_COL32(128, 128, 128, 255), col));
ImU32 col_bg2 = GetColorU32(ImAlphaBlendColors(IM_COL32(128, 128, 128, 255), col)); ImU32 col_bg2 = GetColorU32(ImAlphaBlendColors(IM_COL32(204, 204, 204, 255), col));
draw_list->AddRectFilled(p_min, p_max, col_bg1, rounding, flags); draw_list->AddRectFilled(p_min, p_max, col_bg1, rounding, flags);
int yi = 0; int yi = 0;
@@ -6161,12 +6227,12 @@ void ImGui::RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p
float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y);
if (y2 <= y1) if (y2 <= y1)
continue; continue;
for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) for (float x = p_min.x + grid_off.x + ((yi ^ 1) & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f)
{ {
float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x);
if (x2 <= x1) if (x2 <= x1)
continue; continue;
ImDrawFlags cell_flags = ImDrawFlags_RoundCornersNone; ImDrawFlags cell_flags = ImDrawFlags_RoundCornersNone; // FIXME: Could use CalcRoundingFlagsForRectInRect()
if (y1 <= p_min.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersTopLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersTopRight; } if (y1 <= p_min.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersTopLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersTopRight; }
if (y2 >= p_max.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersBottomLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersBottomRight; } if (y2 >= p_max.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersBottomLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersBottomRight; }
@@ -6308,7 +6374,7 @@ static unsigned int stb_decompress(unsigned char *output, const unsigned char *i
// Download and more information at https://github.com/bluescan/proggyfonts // Download and more information at https://github.com/bluescan/proggyfonts
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef IMGUI_DISABLE_DEFAULT_FONT #if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_BITMAP)
// File: 'ProggyClean.ttf' (41208 bytes) // File: 'ProggyClean.ttf' (41208 bytes)
// Exported using binary_to_compressed_c.exe -u8 "ProggyClean.ttf" proggy_clean_ttf // Exported using binary_to_compressed_c.exe -u8 "ProggyClean.ttf" proggy_clean_ttf
@@ -6488,6 +6554,7 @@ static const char* GetDefaultCompressedFontDataProggyClean(int* out_size)
*out_size = proggy_clean_ttf_compressed_size; *out_size = proggy_clean_ttf_compressed_size;
return (const char*)proggy_clean_ttf_compressed_data; return (const char*)proggy_clean_ttf_compressed_data;
} }
#endif // #if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_BITMAP)
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// [SECTION] Default font data (ProggyForever-Regular-minimal.ttf) // [SECTION] Default font data (ProggyForever-Regular-minimal.ttf)
@@ -6496,6 +6563,8 @@ static const char* GetDefaultCompressedFontDataProggyClean(int* out_size)
// MIT license / Copyright (c) 2026 Disco Hello, Copyright (c) 2019,2023 Tristan Grimmer // MIT license / Copyright (c) 2026 Disco Hello, Copyright (c) 2019,2023 Tristan Grimmer
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_VECTOR)
// File: 'output/ProggyForever-Regular-minimal.ttf' (18556 bytes) // File: 'output/ProggyForever-Regular-minimal.ttf' (18556 bytes)
// Exported using binary_to_compressed_c.exe -u8 "output/ProggyForever-Regular-minimal.ttf" proggy_forever_minimal_ttf // Exported using binary_to_compressed_c.exe -u8 "output/ProggyForever-Regular-minimal.ttf" proggy_forever_minimal_ttf
static const unsigned int proggy_forever_minimal_ttf_compressed_size = 14562; static const unsigned int proggy_forever_minimal_ttf_compressed_size = 14562;
@@ -6750,7 +6819,6 @@ static const char* GetDefaultCompressedFontDataProggyForever(int* out_size)
*out_size = proggy_forever_minimal_ttf_compressed_size; *out_size = proggy_forever_minimal_ttf_compressed_size;
return (const char*)proggy_forever_minimal_ttf_compressed_data; return (const char*)proggy_forever_minimal_ttf_compressed_data;
} }
#endif // #if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_VECTOR)
#endif // #ifndef IMGUI_DISABLE_DEFAULT_FONT
#endif // #ifndef IMGUI_DISABLE #endif // #ifndef IMGUI_DISABLE
+28 -9
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7 // dear imgui, v1.92.9 WIP
// (internal structures/api) // (internal structures/api)
// You may use this file to debug, understand or extend Dear ImGui features but we don't provide any guarantee of forward compatibility. // You may use this file to debug, understand or extend Dear ImGui features but we don't provide any guarantee of forward compatibility.
@@ -263,6 +263,9 @@ extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer
#define IMGUI_DEBUG_LOG_DOCKING(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventDocking) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) #define IMGUI_DEBUG_LOG_DOCKING(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventDocking) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0)
#define IMGUI_DEBUG_LOG_VIEWPORT(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventViewport) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) #define IMGUI_DEBUG_LOG_VIEWPORT(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventViewport) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0)
// Debug options (also see ones on top of imgui.cpp)
//#define IMGUI_DEBUG_BOXSELECT
// Static Asserts // Static Asserts
#define IM_STATIC_ASSERT(_COND) static_assert(_COND, "") #define IM_STATIC_ASSERT(_COND) static_assert(_COND, "")
@@ -517,7 +520,8 @@ inline double ImRsqrt(double x) { return 1.0 / sqrt(x); }
template<typename T> T ImMin(T lhs, T rhs) { return lhs < rhs ? lhs : rhs; } template<typename T> T ImMin(T lhs, T rhs) { return lhs < rhs ? lhs : rhs; }
template<typename T> T ImMax(T lhs, T rhs) { return lhs >= rhs ? lhs : rhs; } template<typename T> T ImMax(T lhs, T rhs) { return lhs >= rhs ? lhs : rhs; }
template<typename T> T ImClamp(T v, T mn, T mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } template<typename T> T ImClamp(T v, T mn, T mx) { return (v < mn) ? mn : (v > mx) ? mx : v; }
template<typename T> T ImLerp(T a, T b, float t) { return (T)(a + (b - a) * t); } template<typename T> T ImLerp(double a, double b, float t) { return (T)(a + (b - a) * (double)t); }
template<typename T> T ImLerp(T a, T b, float t) { return (T)((float)a + (float)(b - a) * t); }
template<typename T> void ImSwap(T& a, T& b) { T tmp = a; a = b; b = tmp; } template<typename T> void ImSwap(T& a, T& b) { T tmp = a; a = b; b = tmp; }
template<typename T> T ImAddClampOverflow(T a, T b, T mn, T mx) { if (b < 0 && (a < mn - b)) return mn; if (b > 0 && (a > mx - b)) return mx; return a + b; } template<typename T> T ImAddClampOverflow(T a, T b, T mn, T mx) { if (b < 0 && (a < mn - b)) return mn; if (b > 0 && (a > mx - b)) return mx; return a + b; }
template<typename T> T ImSubClampOverflow(T a, T b, T mn, T mx) { if (b > 0 && (a < mn + b)) return mn; if (b < 0 && (a > mx + b)) return mx; return a - b; } template<typename T> T ImSubClampOverflow(T a, T b, T mn, T mx) { if (b > 0 && (a < mn + b)) return mn; if (b < 0 && (a > mx + b)) return mx; return a - b; }
@@ -538,6 +542,7 @@ inline float ImFloor(float f) { return
inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2(ImFloor(v.x), ImFloor(v.y)); } inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2(ImFloor(v.x), ImFloor(v.y)); }
inline float ImTrunc64(float f) { return (float)(ImS64)(f); } inline float ImTrunc64(float f) { return (float)(ImS64)(f); }
inline float ImRound64(float f) { return (float)(ImS64)(f + 0.5f); } // FIXME: Positive values only. inline float ImRound64(float f) { return (float)(ImS64)(f + 0.5f); } // FIXME: Positive values only.
inline float ImCeilFast(float f) { int i = (int)f; return (float)(i + (f > (float)i)); } // Consider using the the bit-hack version (search for "0x1p120f").
inline int ImModPositive(int a, int b) { return (a + b) % b; } inline int ImModPositive(int a, int b) { return (a + b) % b; }
inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; }
inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); }
@@ -614,6 +619,8 @@ struct IMGUI_API ImRect
bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; }
void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; }
void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; }
void AddX(float x) { if (Min.x > x) Min.x = x; if (Max.x < x) Max.x = x; }
void AddY(float y) { if (Min.y > y) Min.y = y; if (Max.y < y) Max.y = y; }
void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; } void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; }
void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; } void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; }
void Translate(const ImVec2& d) { Min.x += d.x; Min.y += d.y; Max.x += d.x; Max.y += d.y; } void Translate(const ImVec2& d) { Min.x += d.x; Min.y += d.y; Max.x += d.x; Max.y += d.y; }
@@ -877,7 +884,7 @@ struct IMGUI_API ImDrawListSharedData
float FontSize; // Current font size (used for for simplified AddText overload) float FontSize; // Current font size (used for for simplified AddText overload)
float FontScale; // Current font scale (== FontSize / Font->FontSize) float FontScale; // Current font scale (== FontSize / Font->FontSize)
float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo()
float CircleSegmentMaxError; // Number of circle segments to use per pixel of radius for AddCircle() etc float CircleTessellationMaxError; // Number of circle segments to use per pixel of radius for AddCircle() etc
float InitialFringeScale; // Initial scale to apply to AA fringe float InitialFringeScale; // Initial scale to apply to AA fringe
ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards) ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards)
ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen()
@@ -1010,6 +1017,7 @@ enum ImGuiItemStatusFlags_
ImGuiItemStatusFlags_HasClipRect = 1 << 9, // g.LastItemData.ClipRect is valid. ImGuiItemStatusFlags_HasClipRect = 1 << 9, // g.LastItemData.ClipRect is valid.
ImGuiItemStatusFlags_HasShortcut = 1 << 10, // g.LastItemData.Shortcut valid. Set by SetNextItemShortcut() -> ItemAdd(). ImGuiItemStatusFlags_HasShortcut = 1 << 10, // g.LastItemData.Shortcut valid. Set by SetNextItemShortcut() -> ItemAdd().
//ImGuiItemStatusFlags_FocusedByTabbing = 1 << 8, // Removed IN 1.90.1 (Dec 2023). The trigger is part of g.NavActivateId. See commit 54c1bdeceb. //ImGuiItemStatusFlags_FocusedByTabbing = 1 << 8, // Removed IN 1.90.1 (Dec 2023). The trigger is part of g.NavActivateId. See commit 54c1bdeceb.
ImGuiItemStatusFlags_EditedInternal = 1 << 11, // Similar to ImGuiItemStatusFlags_Edited but bypassing ImGuiItemFlags_NoMarkEdited.
// Additional status + semantic for ImGuiTestEngine // Additional status + semantic for ImGuiTestEngine
#ifdef IMGUI_ENABLE_TEST_ENGINE #ifdef IMGUI_ENABLE_TEST_ENGINE
@@ -1215,6 +1223,7 @@ struct IMGUI_API ImGuiMenuColumns
}; };
// Internal temporary state for deactivating InputText() instances. // Internal temporary state for deactivating InputText() instances.
// Store as part of ImGuiDeactivatedItemData?
struct IMGUI_API ImGuiInputTextDeactivatedState struct IMGUI_API ImGuiInputTextDeactivatedState
{ {
ImGuiID ID; // widget id owning the text state (which just got deactivated) ImGuiID ID; // widget id owning the text state (which just got deactivated)
@@ -1468,6 +1477,7 @@ struct ImGuiPtrOrIndex
}; };
// Data used by IsItemDeactivated()/IsItemDeactivatedAfterEdit() functions // Data used by IsItemDeactivated()/IsItemDeactivatedAfterEdit() functions
// Also see ImGuiInputTextDeactivatedState which is an extension for this for InputText()
struct ImGuiDeactivatedItemData struct ImGuiDeactivatedItemData
{ {
ImGuiID ID; ImGuiID ID;
@@ -1715,6 +1725,7 @@ enum ImGuiActivateFlags_
}; };
// Early work-in-progress API for ScrollToItem() // Early work-in-progress API for ScrollToItem()
// FIXME: Missing flags to request making both edges visible when possible.
enum ImGuiScrollFlags_ enum ImGuiScrollFlags_
{ {
ImGuiScrollFlags_None = 0, ImGuiScrollFlags_None = 0,
@@ -1912,6 +1923,7 @@ struct ImGuiBoxSelectState
// Temporary/Transient data // Temporary/Transient data
bool UnclipMode; // (Temp/Transient, here in hot area). Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select. bool UnclipMode; // (Temp/Transient, here in hot area). Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select.
ImRect UnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. ImRect UnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets.
ImRect UnclipRects[2]; // Per-axis versions.
ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos)
ImRect BoxSelectRectCurr; ImRect BoxSelectRectCurr;
@@ -1934,7 +1946,8 @@ struct IMGUI_API ImGuiMultiSelectTempData
ImGuiMultiSelectFlags Flags; ImGuiMultiSelectFlags Flags;
ImVec2 ScopeRectMin; ImVec2 ScopeRectMin;
ImVec2 BackupCursorMaxPos; ImVec2 BackupCursorMaxPos;
ImGuiSelectionUserData LastSubmittedItem; // Copy of last submitted item data, used to merge output ranges. //ImGuiSelectionUserData CurrSubmittedItem; // Copy of last submitted item data, used to merge output ranges.
//ImGuiSelectionUserData PrevSubmittedItem; // Copy of previous submitted item data, used to merge output ranges.
ImGuiID BoxSelectId; ImGuiID BoxSelectId;
ImGuiKeyChord KeyMods; ImGuiKeyChord KeyMods;
ImS8 LoopRequestSetAll; // -1: no operation, 0: clear all, 1: select all. ImS8 LoopRequestSetAll; // -1: no operation, 0: clear all, 1: select all.
@@ -2022,7 +2035,7 @@ enum ImGuiDockNodeState
ImGuiDockNodeState_HostWindowVisible, ImGuiDockNodeState_HostWindowVisible,
}; };
// sizeof() 156~192 // sizeof() 176~216
struct IMGUI_API ImGuiDockNode struct IMGUI_API ImGuiDockNode
{ {
ImGuiID ID; ImGuiID ID;
@@ -2039,8 +2052,8 @@ struct IMGUI_API ImGuiDockNode
ImVec2 Size; // Current size ImVec2 Size; // Current size
ImVec2 SizeRef; // [Split node only] Last explicitly written-to size (overridden when using a splitter affecting the node), used to calculate Size. ImVec2 SizeRef; // [Split node only] Last explicitly written-to size (overridden when using a splitter affecting the node), used to calculate Size.
ImGuiAxis SplitAxis; // [Split node only] Split axis (X or Y) ImGuiAxis SplitAxis; // [Split node only] Split axis (X or Y)
ImGuiWindowClass WindowClass; // [Root node only]
ImU32 LastBgColor; ImU32 LastBgColor;
ImGuiWindowClass WindowClass; // [Root node only]
ImGuiWindow* HostWindow; ImGuiWindow* HostWindow;
ImGuiWindow* VisibleWindow; // Generally point to window which is ID is == SelectedTabID, but when CTRL+Tabbing this can be a different window. ImGuiWindow* VisibleWindow; // Generally point to window which is ID is == SelectedTabID, but when CTRL+Tabbing this can be a different window.
@@ -2410,6 +2423,7 @@ struct ImGuiContext
float CurrentDpiScale; // Current window/viewport DpiScale == CurrentViewport->DpiScale float CurrentDpiScale; // Current window/viewport DpiScale == CurrentViewport->DpiScale
ImDrawListSharedData DrawListSharedData; ImDrawListSharedData DrawListSharedData;
ImGuiID WithinEndChildID; // Set within EndChild() ImGuiID WithinEndChildID; // Set within EndChild()
ImGuiID WithinEndPopupID; // Set within EndPopup()
void* TestEngine; // Test engine user data void* TestEngine; // Test engine user data
// Inputs // Inputs
@@ -3439,7 +3453,7 @@ namespace ImGui
IMGUI_API void SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags); IMGUI_API void SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags);
// Fonts, drawing // Fonts, drawing
IMGUI_API void RegisterUserTexture(ImTextureData* tex); // Register external texture. EXPERIMENTAL: DO NOT USE YET. IMGUI_API void RegisterUserTexture(ImTextureData* tex); // Register external texture. EXPERIMENTAL.
IMGUI_API void UnregisterUserTexture(ImTextureData* tex); IMGUI_API void UnregisterUserTexture(ImTextureData* tex);
IMGUI_API void RegisterFontAtlas(ImFontAtlas* atlas); IMGUI_API void RegisterFontAtlas(ImFontAtlas* atlas);
IMGUI_API void UnregisterFontAtlas(ImFontAtlas* atlas); IMGUI_API void UnregisterFontAtlas(ImFontAtlas* atlas);
@@ -3557,6 +3571,7 @@ namespace ImGui
// Childs // Childs
IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags); IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags);
IMGUI_API ImGuiWindow* FindFrontMostVisibleChildWindow(ImGuiWindow* window);
// Popups, Modals // Popups, Modals
IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags); IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags);
@@ -3662,7 +3677,7 @@ namespace ImGui
IMGUI_API ImGuiID GetKeyOwner(ImGuiKey key); IMGUI_API ImGuiID GetKeyOwner(ImGuiKey key);
IMGUI_API void SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags = 0); IMGUI_API void SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags = 0);
IMGUI_API void SetKeyOwnersForKeyChord(ImGuiKeyChord key, ImGuiID owner_id, ImGuiInputFlags flags = 0); IMGUI_API void SetKeyOwnersForKeyChord(ImGuiKeyChord key, ImGuiID owner_id, ImGuiInputFlags flags = 0);
IMGUI_API void SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags); // Set key owner to last item if it is hovered or active. Equivalent to 'if (IsItemHovered() || IsItemActive()) { SetKeyOwner(key, GetItemID());'. IMGUI_API bool SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags);
IMGUI_API bool TestKeyOwner(ImGuiKey key, ImGuiID owner_id); // Test that key is either not owned, either owned by 'owner_id' IMGUI_API bool TestKeyOwner(ImGuiKey key, ImGuiID owner_id); // Test that key is either not owned, either owned by 'owner_id'
inline ImGuiKeyOwnerData* GetKeyOwnerData(ImGuiContext* ctx, ImGuiKey key) { if (key & ImGuiMod_Mask_) key = ConvertSingleModFlagToKey(key); IM_ASSERT(IsNamedKey(key)); return &ctx->KeysOwnerData[key - ImGuiKey_NamedKey_BEGIN]; } inline ImGuiKeyOwnerData* GetKeyOwnerData(ImGuiContext* ctx, ImGuiKey key) { if (key & ImGuiMod_Mask_) key = ConvertSingleModFlagToKey(key); IM_ASSERT(IsNamedKey(key)); return &ctx->KeysOwnerData[key - ImGuiKey_NamedKey_BEGIN]; }
@@ -3765,6 +3780,7 @@ namespace ImGui
// We don't use the ID Stack for this as it is common to want them separate. // We don't use the ID Stack for this as it is common to want them separate.
IMGUI_API void PushFocusScope(ImGuiID id); IMGUI_API void PushFocusScope(ImGuiID id);
IMGUI_API void PopFocusScope(); IMGUI_API void PopFocusScope();
IMGUI_API bool IsInNavFocusRoute(ImGuiID focus_scope_id);
inline ImGuiID GetCurrentFocusScope() { ImGuiContext& g = *GImGui; return g.CurrentFocusScopeId; } // Focus scope we are outputting into, set by PushFocusScope() inline ImGuiID GetCurrentFocusScope() { ImGuiContext& g = *GImGui; return g.CurrentFocusScopeId; } // Focus scope we are outputting into, set by PushFocusScope()
// Drag and Drop // Drag and Drop
@@ -3774,7 +3790,7 @@ namespace ImGui
IMGUI_API void ClearDragDrop(); IMGUI_API void ClearDragDrop();
IMGUI_API bool IsDragDropPayloadBeingAccepted(); IMGUI_API bool IsDragDropPayloadBeingAccepted();
IMGUI_API void RenderDragDropTargetRectForItem(const ImRect& bb); IMGUI_API void RenderDragDropTargetRectForItem(const ImRect& bb);
IMGUI_API void RenderDragDropTargetRectEx(ImDrawList* draw_list, const ImRect& bb); IMGUI_API void RenderDragDropTargetRectEx(ImDrawList* draw_list, const ImRect& bb, float rounding);
// Typing-Select API // Typing-Select API
// (provide Windows Explorer style "select items by typing partial name" + "cycle through items by typing same letter" feature) // (provide Windows Explorer style "select items by typing partial name" + "cycle through items by typing same letter" feature)
@@ -3831,6 +3847,7 @@ namespace ImGui
IMGUI_API void TableUpdateLayout(ImGuiTable* table); IMGUI_API void TableUpdateLayout(ImGuiTable* table);
IMGUI_API void TableUpdateBorders(ImGuiTable* table); IMGUI_API void TableUpdateBorders(ImGuiTable* table);
IMGUI_API void TableUpdateColumnsWeightFromWidth(ImGuiTable* table); IMGUI_API void TableUpdateColumnsWeightFromWidth(ImGuiTable* table);
IMGUI_API void TableApplyExternalUnclipRect(ImGuiTable* table, ImRect& rect);
IMGUI_API void TableDrawBorders(ImGuiTable* table); IMGUI_API void TableDrawBorders(ImGuiTable* table);
IMGUI_API void TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display); IMGUI_API void TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display);
IMGUI_API bool TableBeginContextMenuPopup(ImGuiTable* table); IMGUI_API bool TableBeginContextMenuPopup(ImGuiTable* table);
@@ -4261,6 +4278,8 @@ IMGUI_API void ImFontAtlasTextureBlockFill(ImTextureData* dst_tex,
IMGUI_API void ImFontAtlasTextureBlockCopy(ImTextureData* src_tex, int src_x, int src_y, ImTextureData* dst_tex, int dst_x, int dst_y, int w, int h); IMGUI_API void ImFontAtlasTextureBlockCopy(ImTextureData* src_tex, int src_x, int src_y, ImTextureData* dst_tex, int dst_x, int dst_y, int w, int h);
IMGUI_API void ImFontAtlasTextureBlockQueueUpload(ImFontAtlas* atlas, ImTextureData* tex, int x, int y, int w, int h); IMGUI_API void ImFontAtlasTextureBlockQueueUpload(ImFontAtlas* atlas, ImTextureData* tex, int x, int y, int w, int h);
IMGUI_API bool ImTextureDataUpdateNewFrame(ImTextureData* tex);
IMGUI_API void ImTextureDataQueueUpload(ImTextureData* tex, int x, int y, int w, int h);
IMGUI_API int ImTextureDataGetFormatBytesPerPixel(ImTextureFormat format); IMGUI_API int ImTextureDataGetFormatBytesPerPixel(ImTextureFormat format);
IMGUI_API const char* ImTextureDataGetStatusName(ImTextureStatus status); IMGUI_API const char* ImTextureDataGetStatusName(ImTextureStatus status);
IMGUI_API const char* ImTextureDataGetFormatName(ImTextureFormat format); IMGUI_API const char* ImTextureDataGetFormatName(ImTextureFormat format);
+50 -27
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7 // dear imgui, v1.92.9 WIP
// (tables and columns code) // (tables and columns code)
/* /*
@@ -240,6 +240,7 @@ Index of this file:
#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*' #pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*'
#pragma GCC diagnostic ignored "-Wstrict-overflow" #pragma GCC diagnostic ignored "-Wstrict-overflow"
#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead #pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead
#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may change value
#pragma GCC diagnostic ignored "-Wsign-conversion" // warning: conversion to 'xxxx' from 'xxxx' may change the sign of the result #pragma GCC diagnostic ignored "-Wsign-conversion" // warning: conversion to 'xxxx' from 'xxxx' may change the sign of the result
#endif #endif
@@ -1315,14 +1316,32 @@ void ImGui::TableUpdateLayout(ImGuiTable* table)
table->InnerWindow->DecoInnerSizeY1 = table_instance->LastFrozenHeight; table->InnerWindow->DecoInnerSizeY1 = table_instance->LastFrozenHeight;
table_instance->LastFrozenHeight = 0.0f; table_instance->LastFrozenHeight = 0.0f;
// Initial state
ImGuiWindow* inner_window = table->InnerWindow; ImGuiWindow* inner_window = table->InnerWindow;
ImGuiBoxSelectState* bs = &g.BoxSelectState;
if (bs->Window == inner_window && bs->UnclipMode)
TableApplyExternalUnclipRect(table, bs->UnclipRect);
// Initial state
if (table->Flags & ImGuiTableFlags_NoClip) if (table->Flags & ImGuiTableFlags_NoClip)
table->DrawSplitter->SetCurrentChannel(inner_window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP); table->DrawSplitter->SetCurrentChannel(inner_window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP);
else else
inner_window->DrawList->PushClipRect(inner_window->InnerClipRect.Min, inner_window->InnerClipRect.Max, false); // FIXME: use table->InnerClipRect? inner_window->DrawList->PushClipRect(inner_window->InnerClipRect.Min, inner_window->InnerClipRect.Max, false); // FIXME: use table->InnerClipRect?
} }
// When starting a BeginMultiSelect() after table has been layout we update IsRequestOutput fields.
void ImGui::TableApplyExternalUnclipRect(ImGuiTable* table, ImRect& rect)
{
if (rect.IsInverted())
return;
for (int column_n = 0; column_n < table->ColumnsCount; column_n++)
{
ImGuiTableColumn* column = &table->Columns[column_n];
if (!column->IsRequestOutput)
if (rect.Overlaps(ImRect(column->MinX, table->WorkRect.Min.y, column->MaxX, FLT_MAX)))
column->IsRequestOutput = true;
}
}
// Process hit-testing on resizing borders. Actual size change will be applied in EndTable() // Process hit-testing on resizing borders. Actual size change will be applied in EndTable()
// - Set table->HoveredColumnBorder with a short delay/timer to reduce visual feedback noise. // - Set table->HoveredColumnBorder with a short delay/timer to reduce visual feedback noise.
void ImGui::TableUpdateBorders(ImGuiTable* table) void ImGui::TableUpdateBorders(ImGuiTable* table)
@@ -1436,12 +1455,12 @@ void ImGui::EndTable()
if (table->Flags & ImGuiTableFlags_ScrollX) if (table->Flags & ImGuiTableFlags_ScrollX)
{ {
const float outer_padding_for_border = (table->Flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f; const float outer_padding_for_border = (table->Flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f;
float max_pos_x = table->InnerWindow->DC.CursorMaxPos.x; float max_pos_x = inner_window->DC.CursorMaxPos.x;
if (table->RightMostEnabledColumn != -1) if (table->RightMostEnabledColumn != -1)
max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - outer_padding_for_border); max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - outer_padding_for_border);
if (table->ResizedColumn != -1) if (table->ResizedColumn != -1)
max_pos_x = ImMax(max_pos_x, table->ResizeLockMinContentsX2); max_pos_x = ImMax(max_pos_x, table->ResizeLockMinContentsX2);
table->InnerWindow->DC.CursorMaxPos.x = max_pos_x + table->TempData->AngledHeadersExtraWidth; inner_window->DC.CursorMaxPos.x = max_pos_x + table->TempData->AngledHeadersExtraWidth;
} }
// Pop clipping rect // Pop clipping rect
@@ -1550,7 +1569,7 @@ void ImGui::EndTable()
} }
else else
{ {
table->InnerWindow->DC.TreeDepth--; inner_window->DC.TreeDepth--;
ItemSize(table->OuterRect.GetSize()); ItemSize(table->OuterRect.GetSize());
ItemAdd(table->OuterRect, 0); ItemAdd(table->OuterRect, 0);
} }
@@ -1565,13 +1584,12 @@ void ImGui::EndTable()
} }
else if (temp_data->UserOuterSize.x <= 0.0f) else if (temp_data->UserOuterSize.x <= 0.0f)
{ {
// Some references for this: #7651 + tests "table_reported_size", "table_reported_size_outer" equivalent Y block // Some references for this: #7651 + tests "table_reported_size", "table_reported_size_outer" equivalent Y block, #9352
// - Checking for ImGuiTableFlags_ScrollX/ScrollY flag makes us a frame ahead when disabling those flags. // - FIXME-TABLE: Would make sense to pre-compute expected scrollbar visibility/sizes to generally save a frame of feedback? See broken test in 'table_reported_size_outer'
// - FIXME-TABLE: Would make sense to pre-compute expected scrollbar visibility/sizes to generally save a frame of feedback. const float outer_content_max_x = table->OuterRect.Min.x + table->ColumnsAutoFitWidth;
const float inner_content_max_x = table->OuterRect.Min.x + table->ColumnsAutoFitWidth; // Slightly misleading name but used for code symmetry with inner_content_max_y const float decoration_size = table->TempData->AngledHeadersExtraWidth + ((inner_window != outer_window) ? inner_window->ScrollbarSizes.x : 0.0f);
const float decoration_size = table->TempData->AngledHeadersExtraWidth + ((table->Flags & ImGuiTableFlags_ScrollY) ? inner_window->ScrollbarSizes.x : 0.0f); outer_window->DC.IdealMaxPos.x = ImMax(outer_window->DC.IdealMaxPos.x, outer_content_max_x + decoration_size - temp_data->UserOuterSize.x);
outer_window->DC.IdealMaxPos.x = ImMax(outer_window->DC.IdealMaxPos.x, inner_content_max_x + decoration_size - temp_data->UserOuterSize.x); outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, ImMin(table->OuterRect.Max.x, outer_content_max_x + decoration_size));
outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, ImMin(table->OuterRect.Max.x, inner_content_max_x + decoration_size));
} }
else else
{ {
@@ -1579,9 +1597,12 @@ void ImGui::EndTable()
} }
if (temp_data->UserOuterSize.y <= 0.0f) if (temp_data->UserOuterSize.y <= 0.0f)
{ {
const float decoration_size = (table->Flags & ImGuiTableFlags_ScrollX) ? inner_window->ScrollbarSizes.y : 0.0f; // (same comment as above)
outer_window->DC.IdealMaxPos.y = ImMax(outer_window->DC.IdealMaxPos.y, inner_content_max_y + decoration_size - temp_data->UserOuterSize.y); const float outer_content_size_y = (inner_window == outer_window) ? (inner_content_max_y - table->InnerRect.Min.y) : (inner_content_max_y - inner_window->DC.CursorStartPos.y);
outer_window->DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, ImMin(table->OuterRect.Max.y, inner_content_max_y + decoration_size)); const float outer_content_max_y = table->OuterRect.Min.y + outer_content_size_y;
const float decoration_size = (inner_window != outer_window ? inner_window->ScrollbarSizes.y : 0.0f);
outer_window->DC.IdealMaxPos.y = ImMax(outer_window->DC.IdealMaxPos.y, outer_content_max_y + decoration_size - temp_data->UserOuterSize.y);
outer_window->DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, ImMin(table->OuterRect.Max.y, outer_content_max_y + decoration_size));
} }
else else
{ {
@@ -1641,7 +1662,7 @@ void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, flo
ImGuiTable* table = g.CurrentTable; ImGuiTable* table = g.CurrentTable;
IM_ASSERT_USER_ERROR_RET(table != NULL, "Call should only be done while in BeginTable() scope!"); IM_ASSERT_USER_ERROR_RET(table != NULL, "Call should only be done while in BeginTable() scope!");
IM_ASSERT_USER_ERROR_RET(table->DeclColumnsCount < table->ColumnsCount, "TableSetupColumn(): called too many times!"); IM_ASSERT_USER_ERROR_RET(table->DeclColumnsCount < table->ColumnsCount, "TableSetupColumn(): called too many times!");
IM_ASSERT_USER_ERROR_RET(table->IsLayoutLocked == false, "TableSetupColumn(): need to call before first row!"); IM_ASSERT_USER_ERROR_RET(table->IsLayoutLocked == false, "TableSetupColumn(): need to call before first row!"); // Table layout is locked when submitting a row or when calling BeginMultiSelect() with box-select.
IM_ASSERT((flags & ImGuiTableColumnFlags_StatusMask_) == 0 && "Illegal to pass StatusMask values to TableSetupColumn()"); IM_ASSERT((flags & ImGuiTableColumnFlags_StatusMask_) == 0 && "Illegal to pass StatusMask values to TableSetupColumn()");
ImGuiTableColumn* column = &table->Columns[table->DeclColumnsCount]; ImGuiTableColumn* column = &table->Columns[table->DeclColumnsCount];
@@ -2053,11 +2074,11 @@ void ImGui::TableEndRow(ImGuiTable* table)
// Draw top border // Draw top border
if (top_border_col && bg_y1 >= table->BgClipRect.Min.y && bg_y1 < table->BgClipRect.Max.y) if (top_border_col && bg_y1 >= table->BgClipRect.Min.y && bg_y1 < table->BgClipRect.Max.y)
window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y1), ImVec2(table->BorderX2, bg_y1), top_border_col, border_size); window->DrawList->AddLineH(table->BorderX1, table->BorderX2, bg_y1, top_border_col, border_size);
// Draw bottom border at the row unfreezing mark (always strong) // Draw bottom border at the row unfreezing mark (always strong)
if (draw_strong_bottom_border && bg_y2 >= table->BgClipRect.Min.y && bg_y2 < table->BgClipRect.Max.y) if (draw_strong_bottom_border && bg_y2 >= table->BgClipRect.Min.y && bg_y2 < table->BgClipRect.Max.y)
window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y2), ImVec2(table->BorderX2, bg_y2), table->BorderColorStrong, border_size); window->DrawList->AddLineH(table->BorderX1, table->BorderX2, bg_y2, table->BorderColorStrong, border_size);
} }
// End frozen rows (when we are past the last frozen row line, teleport cursor and alter clipping rectangle) // End frozen rows (when we are past the last frozen row line, teleport cursor and alter clipping rectangle)
@@ -2834,7 +2855,7 @@ void ImGui::TableDrawBorders(ImGuiTable* table)
else if ((table->Flags & (ImGuiTableFlags_NoBordersInBodyUntilResize | ImGuiTableFlags_NoBordersInBody)) == 0) else if ((table->Flags & (ImGuiTableFlags_NoBordersInBodyUntilResize | ImGuiTableFlags_NoBordersInBody)) == 0)
draw_y2 = draw_y2_body; draw_y2 = draw_y2_body;
if (draw_y2 > draw_y1) if (draw_y2 > draw_y1)
inner_drawlist->AddLine(ImVec2(column->MaxX, draw_y1), ImVec2(column->MaxX, draw_y2), TableGetColumnBorderCol(table, order_n, column_n), border_size); inner_drawlist->AddLineV(column->MaxX, draw_y1, draw_y2, TableGetColumnBorderCol(table, order_n, column_n), border_size);
} }
} }
@@ -2851,17 +2872,17 @@ void ImGui::TableDrawBorders(ImGuiTable* table)
const ImU32 outer_col = table->BorderColorStrong; const ImU32 outer_col = table->BorderColorStrong;
if ((table->Flags & ImGuiTableFlags_BordersOuter) == ImGuiTableFlags_BordersOuter) if ((table->Flags & ImGuiTableFlags_BordersOuter) == ImGuiTableFlags_BordersOuter)
{ {
inner_drawlist->AddRect(outer_border.Min, outer_border.Max, outer_col, 0.0f, 0, border_size); inner_drawlist->AddRect(outer_border.Min, outer_border.Max, outer_col, 0.0f, border_size);
} }
else if (table->Flags & ImGuiTableFlags_BordersOuterV) else if (table->Flags & ImGuiTableFlags_BordersOuterV)
{ {
inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Min.x, outer_border.Max.y), outer_col, border_size); inner_drawlist->AddLineV(outer_border.Min.x, outer_border.Min.y, outer_border.Max.y, outer_col, border_size);
inner_drawlist->AddLine(ImVec2(outer_border.Max.x, outer_border.Min.y), outer_border.Max, outer_col, border_size); inner_drawlist->AddLineV(outer_border.Max.x, outer_border.Min.y, outer_border.Max.y, outer_col, border_size);
} }
else if (table->Flags & ImGuiTableFlags_BordersOuterH) else if (table->Flags & ImGuiTableFlags_BordersOuterH)
{ {
inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Max.x, outer_border.Min.y), outer_col, border_size); inner_drawlist->AddLineH(outer_border.Min.x, outer_border.Max.x, outer_border.Min.y, outer_col, border_size);
inner_drawlist->AddLine(ImVec2(outer_border.Min.x, outer_border.Max.y), outer_border.Max, outer_col, border_size); inner_drawlist->AddLineH(outer_border.Min.x, outer_border.Max.x, outer_border.Max.y, outer_col, border_size);
} }
} }
if ((table->Flags & ImGuiTableFlags_BordersInnerH) && table->RowPosY2 < table->OuterRect.Max.y) if ((table->Flags & ImGuiTableFlags_BordersInnerH) && table->RowPosY2 < table->OuterRect.Max.y)
@@ -2869,7 +2890,7 @@ void ImGui::TableDrawBorders(ImGuiTable* table)
// Draw bottom-most row border between it is above outer border. // Draw bottom-most row border between it is above outer border.
const float border_y = table->RowPosY2; const float border_y = table->RowPosY2;
if (border_y >= table->BgClipRect.Min.y && border_y < table->BgClipRect.Max.y) if (border_y >= table->BgClipRect.Min.y && border_y < table->BgClipRect.Max.y)
inner_drawlist->AddLine(ImVec2(table->BorderX1, border_y), ImVec2(table->BorderX2, border_y), table->BorderColorLight, border_size); inner_drawlist->AddLineH(table->BorderX1, table->BorderX2, border_y, table->BorderColorLight, border_size);
} }
inner_drawlist->PopClipRect(); inner_drawlist->PopClipRect();
@@ -3174,7 +3195,7 @@ void ImGui::TableHeader(const char* label)
if (label == NULL) if (label == NULL)
label = ""; label = "";
const char* label_end = FindRenderedTextEnd(label); const char* label_end = FindRenderedTextEnd(label);
ImVec2 label_size = CalcTextSize(label, label_end, true); ImVec2 label_size = CalcTextSize(label, label_end, false);
ImVec2 label_pos = window->DC.CursorPos; ImVec2 label_pos = window->DC.CursorPos;
// If we already got a row height, there's use that. // If we already got a row height, there's use that.
@@ -3293,6 +3314,8 @@ void ImGui::TableHeader(const char* label)
// We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden // We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden
if (IsPopupOpenRequestForItem(ImGuiPopupFlags_None, id)) if (IsPopupOpenRequestForItem(ImGuiPopupFlags_None, id))
TableOpenContextMenu(column_n); TableOpenContextMenu(column_n);
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
} }
// Unlike TableHeadersRow() it is not expected that you can reimplement or customize this with custom widgets. // Unlike TableHeadersRow() it is not expected that you can reimplement or customize this with custom widgets.
@@ -4595,7 +4618,7 @@ void ImGui::EndColumns()
// Draw column // Draw column
const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
const float xi = IM_TRUNC(x); const float xi = IM_TRUNC(x);
window->DrawList->AddLine(ImVec2(xi, y1 + 1.0f), ImVec2(xi, y2), col); window->DrawList->AddLineV(xi, y1 + 1.0f, y2, col);
} }
// Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame. // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame.
+263 -113
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7 // dear imgui, v1.92.9 WIP
// (widgets code) // (widgets code)
/* /*
@@ -92,6 +92,7 @@ Index of this file:
#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1 #pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1
#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead #pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead
#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers #pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers
#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may change value
#pragma GCC diagnostic ignored "-Wsign-conversion" // warning: conversion to 'xxxx' from 'xxxx' may change the sign of the result #pragma GCC diagnostic ignored "-Wsign-conversion" // warning: conversion to 'xxxx' from 'xxxx' may change the sign of the result
#endif #endif
@@ -265,6 +266,8 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
} }
} }
// Note that all functions taking format strings in the API may be passed ("%s", text) or ("%.*s", text_len, text),
// which will automatically bypass the formatter.
void ImGui::TextUnformatted(const char* text, const char* text_end) void ImGui::TextUnformatted(const char* text, const char* text_end)
{ {
TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
@@ -401,7 +404,8 @@ void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
const char* value_text_begin, *value_text_end; const char* value_text_begin, *value_text_end;
ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args); ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false); const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false);
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
const ImVec2 pos = window->DC.CursorPos; const ImVec2 pos = window->DC.CursorPos;
const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2)); const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
@@ -413,7 +417,7 @@ void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
// Render // Render
RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f)); RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f));
if (label_size.x > 0.0f) if (label_size.x > 0.0f)
RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label, label_end, false);
} }
void ImGui::BulletText(const char* fmt, ...) void ImGui::BulletText(const char* fmt, ...)
@@ -788,7 +792,8 @@ bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style; const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label); const ImGuiID id = window->GetID(label);
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
ImVec2 pos = window->DC.CursorPos; ImVec2 pos = window->DC.CursorPos;
if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag) if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag)
@@ -810,7 +815,7 @@ bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags
if (g.LogEnabled) if (g.LogEnabled)
LogSetNextTextDecoration("[", "]"); LogSetNextTextDecoration("[", "]");
RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, label_end, &label_size, style.ButtonTextAlign, &bb);
// Automatically close popups // Automatically close popups
//if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
@@ -981,7 +986,7 @@ ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar) const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
IM_ASSERT(scrollbar_size >= 0.0f); IM_ASSERT(scrollbar_size >= 0.0f);
const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f); const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f);
const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : 0.0f; const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : (window->Flags & ImGuiWindowFlags_NoTitleBar) ? border_size : 0;
if (axis == ImGuiAxis_X) if (axis == ImGuiAxis_X)
return ImRect(inner_rect.Min.x + border_size, ImMax(outer_rect.Min.y + border_size, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size); return ImRect(inner_rect.Min.x + border_size, ImMax(outer_rect.Min.y + border_size, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size);
else else
@@ -1155,7 +1160,7 @@ void ImGui::ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const Im
else else
window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
if (g.Style.ImageBorderSize > 0.0f) if (g.Style.ImageBorderSize > 0.0f)
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), rounding, ImDrawFlags_None, g.Style.ImageBorderSize); window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), rounding, g.Style.ImageBorderSize);
} }
void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1) void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
@@ -1250,7 +1255,8 @@ bool ImGui::Checkbox(const char* label, bool* v)
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style; const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label); const ImGuiID id = window->GetID(label);
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
const float square_sz = GetFrameHeight(); const float square_sz = GetFrameHeight();
const ImVec2 pos = window->DC.CursorPos; const ImVec2 pos = window->DC.CursorPos;
@@ -1291,8 +1297,9 @@ bool ImGui::Checkbox(const char* label, bool* v)
if (is_visible) if (is_visible)
{ {
RenderNavCursor(total_bb, id); RenderNavCursor(total_bb, id);
RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : (mixed_value || checked) ? ImGuiCol_CheckboxSelectedBg : ImGuiCol_FrameBg);
ImU32 check_col = GetColorU32(ImGuiCol_CheckMark); ImU32 check_col = GetColorU32(ImGuiCol_CheckMark);
RenderFrame(check_bb.Min, check_bb.Max, bg_col, true, style.FrameRounding);
if (mixed_value) if (mixed_value)
{ {
// Undocumented tristate/mixed/indeterminate checkbox (#2644) // Undocumented tristate/mixed/indeterminate checkbox (#2644)
@@ -1310,7 +1317,7 @@ bool ImGui::Checkbox(const char* label, bool* v)
if (g.LogEnabled) if (g.LogEnabled)
LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]"); LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
if (is_visible && label_size.x > 0.0f) if (is_visible && label_size.x > 0.0f)
RenderText(label_pos, label); RenderText(label_pos, label, label_end, false);
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
return pressed; return pressed;
@@ -1372,7 +1379,8 @@ bool ImGui::RadioButton(const char* label, bool active)
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style; const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label); const ImGuiID id = window->GetID(label);
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
const float square_sz = GetFrameHeight(); const float square_sz = GetFrameHeight();
const ImVec2 pos = window->DC.CursorPos; const ImVec2 pos = window->DC.CursorPos;
@@ -1411,7 +1419,7 @@ bool ImGui::RadioButton(const char* label, bool active)
if (g.LogEnabled) if (g.LogEnabled)
LogRenderedText(&label_pos, active ? "(x)" : "( )"); LogRenderedText(&label_pos, active ? "(x)" : "( )");
if (label_size.x > 0.0f) if (label_size.x > 0.0f)
RenderText(label_pos, label); RenderText(label_pos, label, label_end, false);
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
return pressed; return pressed;
@@ -1527,7 +1535,7 @@ bool ImGui::TextLink(const char* label)
const char* label_end = FindRenderedTextEnd(label); const char* label_end = FindRenderedTextEnd(label);
ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
ImVec2 size = CalcTextSize(label, label_end, true); ImVec2 size = CalcTextSize(label, label_end, false);
ImRect bb(pos, pos + size); ImRect bb(pos, pos + size);
ItemSize(size, 0.0f); ItemSize(size, 0.0f);
if (!ItemAdd(bb, id)) if (!ItemAdd(bb, id))
@@ -1558,10 +1566,10 @@ bool ImGui::TextLink(const char* label)
} }
float line_y = bb.Max.y + ImFloor(g.FontBaked->Descent * g.FontBakedScale * 0.20f); float line_y = bb.Max.y + ImFloor(g.FontBaked->Descent * g.FontBakedScale * 0.20f);
window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf), 1.0f * (float)(int)g.Style._MainScale); // FIXME-TEXT: Underline mode // FIXME-DPI window->DrawList->AddLineH(bb.Min.x, bb.Max.x, line_y, GetColorU32(line_colf), 1.0f * (float)(int)g.Style._MainScale); // FIXME-TEXT: Underline mode // FIXME-DPI
PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf)); PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf));
RenderText(bb.Min, label, label_end); RenderText(bb.Min, label, label_end, false);
PopStyleColor(); PopStyleColor();
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
@@ -1731,7 +1739,7 @@ void ImGui::Separator()
if (window->DC.CurrentColumns) if (window->DC.CurrentColumns)
flags |= ImGuiSeparatorFlags_SpanAllColumns; flags |= ImGuiSeparatorFlags_SpanAllColumns;
SeparatorEx(flags, g.Style.SeparatorSize); SeparatorEx(flags, ImMax(g.Style.SeparatorSize, 1.0f));
} }
void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w) void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
@@ -1747,14 +1755,14 @@ void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end
const float separator_thickness = style.SeparatorTextBorderSize; const float separator_thickness = style.SeparatorTextBorderSize;
const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness)); const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness));
const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y)); const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f)); const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f));
ItemSize(min_size, text_baseline_y); ItemSize(min_size, text_baseline_y);
if (!ItemAdd(bb, id)) if (!ItemAdd(bb, id))
return; return;
const float sep1_x1 = pos.x; const float sep1_x1 = pos.x;
const float sep2_x2 = bb.Max.x; const float sep2_x2 = bb.Max.x;
const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f); const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.999f);
const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f); const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f);
const ImVec2 label_pos(pos.x + padding.x + ImMax(0.0f, (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN const ImVec2 label_pos(pos.x + padding.x + ImMax(0.0f, (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN
@@ -1768,9 +1776,9 @@ void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end
const float sep1_x2 = label_pos.x - style.ItemSpacing.x; const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x; const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f) if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep1_x2, seps_y), separator_col, separator_thickness); window->DrawList->AddLineH(sep1_x1, sep1_x2, seps_y, separator_col, separator_thickness);
if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f) if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness); window->DrawList->AddLineH(sep2_x1, sep2_x2, seps_y, separator_col, separator_thickness);
if (g.LogEnabled) if (g.LogEnabled)
LogSetNextTextDecoration("---", NULL); LogSetNextTextDecoration("---", NULL);
RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, label, label_end, &label_size); RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, label, label_end, &label_size);
@@ -1780,7 +1788,7 @@ void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end
if (g.LogEnabled) if (g.LogEnabled)
LogText("---"); LogText("---");
if (separator_thickness > 0.0f) if (separator_thickness > 0.0f)
window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness); window->DrawList->AddLineH(sep1_x1, sep2_x2, seps_y, separator_col, separator_thickness);
} }
} }
@@ -1951,8 +1959,9 @@ bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboF
IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0); IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight(); const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f; const ImVec2 label_size = CalcTextSize(label, label_end, false);
const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, false).x : 0.0f;
const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth()); const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
@@ -2003,7 +2012,7 @@ bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboF
RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL); RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
} }
if (label_size.x > 0) if (label_size.x > 0)
RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label); RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label, label_end, false);
if (!popup_open) if (!popup_open)
return false; return false;
@@ -2376,12 +2385,17 @@ bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void
// Sanitize format // Sanitize format
// - For float/double we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in, so force them into %f and %lf // - For float/double we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in, so force them into %f and %lf
// - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
char format_sanitized[32]; char format_sanitized[32];
if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
{
format = type_info->ScanFmt; format = type_info->ScanFmt;
}
else else
{
format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_COUNTOF(format_sanitized)); format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_COUNTOF(format_sanitized));
if (format[0] == '\0')
format = type_info->ScanFmt; // Format doesn't want us to show the number currently, but we still need to parse the resulting input
}
// Small types need a 32-bit buffer to receive the result from scanf() // Small types need a 32-bit buffer to receive the result from scanf()
int v32 = 0; int v32 = 0;
@@ -2716,7 +2730,8 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data,
const float w = CalcItemWidth(); const float w = CalcItemWidth();
const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0; const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0;
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
@@ -2792,7 +2807,7 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data,
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
if (label_size.x > 0.0f) if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
return value_changed; return value_changed;
@@ -3317,7 +3332,8 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat
const float w = CalcItemWidth(); const float w = CalcItemWidth();
const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0; const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0;
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
@@ -3389,7 +3405,7 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
if (label_size.x > 0.0f) if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
return value_changed; return value_changed;
@@ -3494,7 +3510,8 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
const ImGuiStyle& style = g.Style; const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label); const ImGuiID id = window->GetID(label);
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
@@ -3539,8 +3556,9 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format); const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f)); RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f));
if (label_size.x > 0.0f) if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
return value_changed; return value_changed;
} }
@@ -3563,7 +3581,8 @@ bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min,
// - ImParseFormatSanitizeForPrinting() [Internal] // - ImParseFormatSanitizeForPrinting() [Internal]
// - ImParseFormatSanitizeForScanning() [Internal] // - ImParseFormatSanitizeForScanning() [Internal]
// - ImParseFormatPrecision() [Internal] // - ImParseFormatPrecision() [Internal]
// - TempInputTextScalar() [Internal] // - TempInputText() [Internal]
// - TempInputScalar() [Internal]
// - InputScalar() // - InputScalar()
// - InputScalarN() // - InputScalarN()
// - InputFloat() // - InputFloat()
@@ -3792,7 +3811,7 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
ImGuiStyle& style = g.Style; ImGuiStyle& style = g.Style;
IM_ASSERT((flags & ImGuiInputTextFlags_EnterReturnsTrue) == 0); // Not supported by InputScalar(). Please open an issue if you this would be useful to you. Otherwise use IsItemDeactivatedAfterEdit()! //IM_ASSERT((flags & ImGuiInputTextFlags_EnterReturnsTrue) == 0); // Not supported by InputScalar(). Please open an issue if you this would be useful to you. Otherwise use IsItemDeactivatedAfterEdit()!
if (format == NULL) if (format == NULL)
format = DataTypeGetInfo(data_type)->PrintFmt; format = DataTypeGetInfo(data_type)->PrintFmt;
@@ -3829,7 +3848,8 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
} }
// Apply // Apply
bool value_changed = ret ? DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL) : false; bool input_edited = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_EditedInternal) != 0; // We would be using 'ret' if ImGuiInputTextFlags_EnterReturnsTrue was not involved.
bool value_changed = input_edited ? DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL) : false;
// Step buttons // Step buttons
if (has_step_buttons) if (has_step_buttons)
@@ -3843,13 +3863,13 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
if (ButtonEx("-", ImVec2(button_size, button_size))) if (ButtonEx("-", ImVec2(button_size, button_size)))
{ {
DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
value_changed = true; value_changed = ret = true;
} }
SameLine(0, style.ItemInnerSpacing.x); SameLine(0, style.ItemInnerSpacing.x);
if (ButtonEx("+", ImVec2(button_size, button_size))) if (ButtonEx("+", ImVec2(button_size, button_size)))
{ {
DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
value_changed = true; value_changed = ret = true;
} }
PopItemFlag(); PopItemFlag();
if (flags & ImGuiInputTextFlags_ReadOnly) if (flags & ImGuiInputTextFlags_ReadOnly)
@@ -3871,6 +3891,8 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
if (value_changed) if (value_changed)
MarkItemEdited(g.LastItemData.ID); MarkItemEdited(g.LastItemData.ID);
if (flags & ImGuiInputTextFlags_EnterReturnsTrue)
return ret;
return value_changed; return value_changed;
} }
@@ -4520,6 +4542,9 @@ static bool InputTextFilterCharacter(ImGuiContext* ctx, ImGuiInputTextState* sta
callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter; callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
callback_data.EventChar = (ImWchar)c; callback_data.EventChar = (ImWchar)c;
callback_data.EventActivated = (g.ActiveId == state->ID && g.ActiveIdIsJustActivated); callback_data.EventActivated = (g.ActiveId == state->ID && g.ActiveIdIsJustActivated);
callback_data.CursorPos = state->Stb->cursor;
callback_data.SelectionStart = state->Stb->select_start;
callback_data.SelectionEnd = state->Stb->select_end;
callback_data.UserData = user_data; callback_data.UserData = user_data;
if (callback(&callback_data) != 0) if (callback(&callback_data) != 0)
return false; return false;
@@ -4568,6 +4593,7 @@ void ImGui::InputTextDeactivateHook(ImGuiID id)
ImGuiInputTextState* state = &g.InputTextState; ImGuiInputTextState* state = &g.InputTextState;
if (id == 0 || state->ID != id) if (id == 0 || state->ID != id)
return; return;
//IMGUI_DEBUG_LOG_ACTIVEID("InputTextDeactivateHook() id = 0x%08X\n", id);
g.InputTextDeactivatedState.ID = state->ID; g.InputTextDeactivatedState.ID = state->ID;
if (state->Flags & ImGuiInputTextFlags_ReadOnly) if (state->Flags & ImGuiInputTextFlags_ReadOnly)
{ {
@@ -4702,7 +4728,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar) if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
BeginGroup(); BeginGroup();
const ImGuiID id = window->GetID(label); const ImGuiID id = window->GetID(label);
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line
const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y); const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
@@ -4716,7 +4743,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
{ {
ImVec2 backup_pos = window->DC.CursorPos; ImVec2 backup_pos = window->DC.CursorPos;
ItemSize(total_bb, style.FramePadding.y); ItemSize(total_bb, style.FramePadding.y);
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) bool no_clip = (g.InputTextDeactivatedState.ID == id) || (g.ActiveId == id) || (id == g.NavActivateId); // Mimic some of ItemAdd() logic + add InputTextDeactivatedState.ID check.
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable) && !no_clip)
{ {
EndGroup(); EndGroup();
return false; return false;
@@ -4742,7 +4770,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
g.NavActivateId = backup_activate_id; g.NavActivateId = backup_activate_id;
PopStyleVar(3); PopStyleVar(3);
PopStyleColor(); PopStyleColor();
if (!child_visible) if (!child_visible && !no_clip)
{ {
EndChild(); EndChild();
EndGroup(); EndGroup();
@@ -4806,7 +4834,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX; float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf); const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf);
const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state. const bool init_changed_specs_multiline = (state != NULL && (state->Stb->single_line != !is_multiline)); // state != NULL means its our state.
const bool init_changed_specs_readonly = (state != NULL && ((state->Flags ^ flags) & ImGuiInputTextFlags_ReadOnly)); // state != NULL means its our state.
const bool init_make_active = (input_requested_by_user || input_requested_by_nav || input_requested_by_reactivate || user_scroll_finish); const bool init_make_active = (input_requested_by_user || input_requested_by_nav || input_requested_by_reactivate || user_scroll_finish);
if (init_reload_from_user_buf) if (init_reload_from_user_buf)
{ {
@@ -4820,7 +4849,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
state->Stb->select_start = state->ReloadSelectionStart; state->Stb->select_start = state->ReloadSelectionStart;
state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd; // will be clamped to bounds below state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd; // will be clamped to bounds below
} }
else if ((init_make_active && g.ActiveId != id) || init_changed_specs) else if ((init_make_active && g.ActiveId != id) || init_changed_specs_multiline || init_changed_specs_readonly)
{ {
// Access state even if we don't own it yet. // Access state even if we don't own it yet.
state = &g.InputTextState; state = &g.InputTextState;
@@ -4841,8 +4870,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
// Preserve cursor position and undo/redo stack if we come back to same widget // Preserve cursor position and undo/redo stack if we come back to same widget
// FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate? // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
bool recycle_state = (state->ID == id && !init_changed_specs); bool recycle_state = (state->ID == id && !init_changed_specs_multiline);
if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0))) if (recycle_state && !init_changed_specs_readonly && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0)))
recycle_state = false; recycle_state = false;
// Start edition // Start edition
@@ -4880,7 +4909,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
} }
const bool is_osx = io.ConfigMacOSXBehaviors; const bool is_osx = io.ConfigMacOSXBehaviors;
if (g.ActiveId != id && init_make_active) if (init_make_active && g.ActiveId != id)
{ {
IM_ASSERT(state && state->ID == id); IM_ASSERT(state && state->ID == id);
SetActiveID(id, window); SetActiveID(id, window);
@@ -4951,13 +4980,17 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
if (is_password && !is_displaying_hint) if (is_password && !is_displaying_hint)
PushPasswordFont(); PushPasswordFont();
// Word-wrapping: attempt to keep cursor in view while resizing frame/parent if (state != NULL && state->ID == id)
// FIXME-WORDWRAP: It would be better to preserve same relative offset.
if (is_wordwrap && state != NULL && state->ID == id && state->WrapWidth != wrap_width)
{ {
state->CursorCenterY = true; state->Flags = flags;
state->WrapWidth = wrap_width;
render_cursor = true; // Word-wrapping: attempt to keep cursor in view while resizing frame/parent (FIXME-WORDWRAP: would be better to preserve same relative offset)
if (is_wordwrap && state->WrapWidth != wrap_width)
{
state->CursorCenterY = true;
state->WrapWidth = wrap_width;
render_cursor = true;
}
} }
// Process mouse inputs and character inputs // Process mouse inputs and character inputs
@@ -4966,7 +4999,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
IM_ASSERT(state != NULL); IM_ASSERT(state != NULL);
state->EditedThisFrame = false; state->EditedThisFrame = false;
state->BufCapacity = buf_size; state->BufCapacity = buf_size;
state->Flags = flags;
state->WrapWidth = wrap_width; state->WrapWidth = wrap_width;
// Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
@@ -5618,7 +5650,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll); ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f);
if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_InputTextCursor), 1.0f * (float)(int)style._MainScale); // FIXME-DPI: Cursor thickness (#7031) draw_window->DrawList->AddLineV(cursor_screen_rect.Min.x, cursor_screen_rect.Min.y, cursor_screen_rect.Max.y, GetColorU32(ImGuiCol_InputTextCursor), style.InputTextCursorSize);
// Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.)
// This is required for some backends (SDL3) to start emitting character/text inputs. // This is required for some backends (SDL3) to start emitting character/text inputs.
@@ -5666,7 +5698,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
} }
if (label_size.x > 0) if (label_size.x > 0)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
if (value_changed) if (value_changed)
MarkItemEdited(id); MarkItemEdited(id);
@@ -6332,7 +6364,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl
const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps; const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
const int vert_start_idx = draw_list->VtxBuffer.Size; const int vert_start_idx = draw_list->VtxBuffer.Size;
draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc); draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc);
draw_list->PathStroke(col_white, 0, wheel_thickness); draw_list->PathStroke(col_white, wheel_thickness);
const int vert_end_idx = draw_list->VtxBuffer.Size; const int vert_end_idx = draw_list->VtxBuffer.Size;
// Paint colors over existing vertices // Paint colors over existing vertices
@@ -6476,7 +6508,7 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl
if (g.Style.FrameBorderSize > 0.0f) if (g.Style.FrameBorderSize > 0.0f)
RenderFrameBorder(bb.Min, bb.Max, rounding); RenderFrameBorder(bb.Min, bb.Max, rounding);
else else
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding, 0, 1.0f * (float)(int)g.Style._MainScale); // Color buttons are often in need of some sort of border // FIXME-DPI window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding, 1.0f * (float)(int)g.Style._MainScale); // Color buttons are often in need of some sort of border // FIXME-DPI
} }
// Drag and Drop Source // Drag and Drop Source
@@ -7149,11 +7181,11 @@ void ImGui::TreeNodeDrawLineToChildNode(const ImVec2& target_pos)
window->DrawList->PathArcToFast(ImVec2(x1, y - rounding), rounding, 6, 3); window->DrawList->PathArcToFast(ImVec2(x1, y - rounding), rounding, 6, 3);
if (x1 < x2) if (x1 < x2)
window->DrawList->PathLineTo(ImVec2(x2, y)); window->DrawList->PathLineTo(ImVec2(x2, y));
window->DrawList->PathStroke(GetColorU32(ImGuiCol_TreeLines), ImDrawFlags_None, g.Style.TreeLinesSize); window->DrawList->PathStroke(GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
} }
else else
{ {
window->DrawList->AddLine(ImVec2(x1, y), ImVec2(x2, y), GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize); window->DrawList->AddLineH(x1, x2, y, GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
} }
} }
@@ -7179,7 +7211,7 @@ void ImGui::TreeNodeDrawLineToTreePop(const ImGuiTreeNodeStackData* data)
float x = ImTrunc(data->DrawLinesX1); float x = ImTrunc(data->DrawLinesX1);
if (data->DrawLinesTableColumn != -1) if (data->DrawLinesTableColumn != -1)
TablePushColumnChannel(data->DrawLinesTableColumn); TablePushColumnChannel(data->DrawLinesTableColumn);
window->DrawList->AddLine(ImVec2(x, y1), ImVec2(x, y2), GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize); window->DrawList->AddLineV(x, y1, y2, GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
if (data->DrawLinesTableColumn != -1) if (data->DrawLinesTableColumn != -1)
TablePopColumnChannel(); TablePopColumnChannel();
} }
@@ -7338,7 +7370,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl
// Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle. // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
ImGuiID id = window->GetID(label); ImGuiID id = window->GetID(label);
ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
ImVec2 label_size = CalcTextSize(label, label_end, false);
ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
ImVec2 pos = window->DC.CursorPos; ImVec2 pos = window->DC.CursorPos;
pos.y += window->DC.CurrLineTextBaseOffset; pos.y += window->DC.CurrLineTextBaseOffset;
@@ -7493,7 +7526,11 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl
// Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns. // Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns.
if (is_visible) if (is_visible)
RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, NULL, &label_size, style.SelectableTextAlign, &bb); RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, label_end, &label_size, style.SelectableTextAlign, &bb);
#ifdef IMGUI_DEBUG_BOXSELECT
if (g.BoxSelectState.UnclipMode) { GetForegroundDrawList()->AddText(pos, IM_COL32(255,255,0,200), label, label_end); }
#endif
// Automatically close popups // Automatically close popups
if (pressed && !auto_selected && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups)) if (pressed && !auto_selected && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
@@ -7810,7 +7847,7 @@ bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiI
return false; return false;
// Current frame absolute prev/current rectangles are used to toggle selection. // Current frame absolute prev/current rectangles are used to toggle selection.
// They are derived from positions relative to scrolling space. // They are derived from positions relative to scrolling space, so "previous" rectangle is reprojected for current frame coordinates.
ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel); ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel);
ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already
ImVec2 curr_end_pos_abs = g.IO.MousePos; ImVec2 curr_end_pos_abs = g.IO.MousePos;
@@ -7820,20 +7857,69 @@ bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiI
bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs);
bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs);
bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs);
//IMGUI_DEBUG_LOG("StartPosRel (%.2f,%.2f) EndPosRel (%.2f,%.2f) -> (%.2f,%.2f)\n", bs->StartPosRel.x, bs->StartPosRel.y, bs->EndPosRel.x, bs->EndPosRel.y, WindowPosAbsToRel(window, g.IO.MousePos).x, WindowPosAbsToRel(window, g.IO.MousePos).y);
// Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) // Box-select 2D mode detects change of the rectangle.
// Storing an extra rect used by widgets supporting box-select. // Storing unclip rects which will be tested by widgets supporting box-select. Always update rectangles when active (even if we don't use them).
if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) // To facilitate understanding this: enable IMGUI_DEBUG_BOXSELECT and visualize all geometry.
if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
{
// For both sides, compute the area differing between Prev and Curr rectangles.
bs->UnclipRects[0] = bs->UnclipRects[1] = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX);
for (int side = 0; side < 2; side++)
{ {
bs->UnclipMode = true; ImVec2 d_min = (side == 0) ? ImMin(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMin(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max);
bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis. ImVec2 d_max = (side == 0) ? ImMax(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMax(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max);
bs->UnclipRect.Add(bs->BoxSelectRectCurr); if (d_min.x != d_max.x)
{
bs->UnclipRects[0].AddX(d_min.x);
bs->UnclipRects[0].AddX(d_max.x);
}
if (d_min.y != d_max.y)
{
bs->UnclipRects[1].AddY(d_min.y);
bs->UnclipRects[1].AddY(d_max.y);
}
} }
//GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); ImRect box_select_intersection = bs->BoxSelectRectPrev;
box_select_intersection.Add(bs->BoxSelectRectCurr);
if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
{
bs->UnclipRects[0].AddY(box_select_intersection.Min.y);
bs->UnclipRects[0].AddY(box_select_intersection.Max.y);
}
if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
if (bs->BoxSelectRectPrev.Min.y != bs->BoxSelectRectCurr.Min.y || bs->BoxSelectRectPrev.Max.y != bs->BoxSelectRectCurr.Max.y)
{
bs->UnclipRects[1].AddX(box_select_intersection.Min.x);
bs->UnclipRects[1].AddX(box_select_intersection.Max.x);
}
// Merge both rectangles into one.
// FIXME-OPT: When UnclipRect.Area() is much larger than the sum of UnclipRects[0]/[1] Areas, widgets should
// ideally first use UnclipRect as a first coarse cull layer + the individual ones as a second validation.
bs->UnclipRect = bs->UnclipRects[0];
bs->UnclipRect.Add(bs->UnclipRects[1]);
if (!bs->UnclipRect.IsInverted() && (!window->ClipRect.Contains(bs->UnclipRect.Min) || !window->ClipRect.Contains(bs->UnclipRect.Max))) // !! Don't use Contains(ImRect)
bs->UnclipMode = true;
if (bs->UnclipMode && g.CurrentTable != NULL)
TableApplyExternalUnclipRect(g.CurrentTable, bs->UnclipRect); // No need submitting both
}
#ifdef IMGUI_DEBUG_BOXSELECT
//GetForegroundDrawList()->AddRect(scope_rect.Min, scope_rect.Max, IM_COL32(0, 255, 0, 200), 0.0f, 0, 4.0f);
//GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
//GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
{
for (ImRect& unclip_r : bs->UnclipRects)
if (!unclip_r.IsInverted())
GetForegroundDrawList()->AddRect(unclip_r.Min, unclip_r.Max, bs->UnclipMode ? IM_COL32(255, 255, 0, 200) : IM_COL32(255, 0, 0, 200), 0.0f, 0, 4.0f);
GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, bs->UnclipMode ? IM_COL32(255, 255, 0, 200) : IM_COL32(255, 0, 0, 200), 0.0f, 0, 2.0f);
}
#endif
return true; return true;
} }
@@ -7849,8 +7935,9 @@ void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flag
bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view
ImRect box_select_r = bs->BoxSelectRectCurr; ImRect box_select_r = bs->BoxSelectRectCurr;
box_select_r.ClipWith(scope_rect); box_select_r.ClipWith(scope_rect);
window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling ImGuiWindow* draw_window = FindFrontMostVisibleChildWindow(window);
window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling draw_window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling
draw_window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling
// Scroll // Scroll
const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
@@ -7890,18 +7977,18 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe
static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window) static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
{ {
ImGuiContext& g = *GImGui;
if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
{ {
// Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
// This probably doesn't work inside a table as there are ample ambiguities related to exact time of calling BeginMultiSelect()/EndMultiSelect().
return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)); return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin));
} }
else else
{ {
// When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970) //// When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
ImRect scope_rect = window->InnerClipRect; ImRect scope_rect = window->InnerClipRect;
if (g.CurrentTable != NULL) //if (g.CurrentTable != NULL)
scope_rect = g.CurrentTable->HostClipRect; // scope_rect = g.CurrentTable->HostClipRect;
// Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect? // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max); scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max);
@@ -7937,18 +8024,24 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel
// FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250) // FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250)
// They should perhaps be stacked properly? // They should perhaps be stacked properly?
if (ImGuiTable* table = g.CurrentTable) if (ImGuiTable* table = g.CurrentTable)
if (table->CurrentColumn != -1) {
if (!table->IsLayoutLocked)
TableUpdateLayout(table);
else if (table->CurrentColumn != -1)
TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it. TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it.
}
// FIXME: BeginFocusScope() // FIXME: BeginFocusScope()
const ImGuiID id = window->IDStack.back(); const ImGuiID id = window->IDStack.back();
ms->Clear(); ms->Clear();
ms->FocusScopeId = id; ms->FocusScopeId = id;
ms->Flags = flags; ms->Flags = flags;
ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
ms->BackupCursorMaxPos = window->DC.CursorMaxPos; ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; ms->ScopeRectMin = window->DC.CursorPos;
if (flags & ImGuiMultiSelectFlags_ScopeRect)
window->DC.CursorMaxPos = ms->ScopeRectMin; // CalcScopeRect() for ImGuiMultiSelectFlags_ScopeRect will measure in EndMultiSelect().
PushFocusScope(ms->FocusScopeId); PushFocusScope(ms->FocusScopeId);
ms->IsFocused = IsInNavFocusRoute(g.CurrentFocusScopeId);
if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items. if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main; window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
@@ -8030,7 +8123,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel
storage->LastSelectionSize = 0; storage->LastSelectionSize = 0;
} }
ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid; //ms->PrevSubmittedItem = ImGuiSelectionUserData_Invalid;
if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO); DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO);
@@ -8076,7 +8169,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect()
// Clear selection when clicking void? // Clear selection when clicking void?
// We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
// The InnerRect test is necessary for non-child/decorated windows. // The InnerRect test is necessary for non-child/decorated windows.
bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos); bool scope_hovered = window->InnerRect.Contains(g.IO.MousePos) && IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)) if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
scope_hovered &= scope_rect.Contains(g.IO.MousePos); scope_hovered &= scope_rect.Contains(g.IO.MousePos);
if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
@@ -8102,10 +8195,13 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect()
if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX) if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
{ {
IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); NavMoveRequestTryWrapping(GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
} }
// Unwind // Unwind
if (ImGuiTable* table = g.CurrentTable)
if (table->IsInsideRow)
TableEndRow(table);
window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos); window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos);
PopFocusScope(); PopFocusScope();
@@ -8133,6 +8229,8 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect; g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
if (ms->IO.RangeSrcItem == selection_user_data) if (ms->IO.RangeSrcItem == selection_user_data)
ms->RangeSrcPassedBy = true; ms->RangeSrcPassedBy = true;
//ms->PrevSubmittedItem = ms->CurrSubmittedItem; // Can't rely on previous g.NextItemData.SelectionUserData because NextItemData is not restored on nested multi-select.
//ms->CurrSubmittedItem = selection_user_data;
} }
else else
{ {
@@ -8281,8 +8379,31 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
if (ms->BoxSelectId != 0) if (ms->BoxSelectId != 0)
if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId))
{ {
const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); ImRect item_rect = g.LastItemData.Rect;
const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); if (!window->DC.NavIsScrollPushableX) // FIXME: Rename to be more generic.
if (ImGuiTable* table = g.CurrentTable)
if (table->CurrentColumn != -1)
{
// FIXME: We cannot solely use current ClipRect as it includes HostClipRect.
// However we account for ClipRect being larger than current column (e.g. when using SpanAllColumns)
// A more generic version would be nice, but window->WorkRect.Min/Max exclude CellPadding. (#7994, #9383)
ImGuiTableColumn* column = &table->Columns[table->CurrentColumn];
float clip_min_x = (g.LastItemData.ItemFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect.Min.x : window->ClipRect.Min.x;
float clip_max_x = (g.LastItemData.ItemFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect.Max.x : window->ClipRect.Max.x;
if (clip_min_x != clip_max_x) // When zero sized we expect that bounds have been clamped and thus are unreliable
{
item_rect.Min.x = ImMax(item_rect.Min.x, ImMin(column->MinX, clip_min_x));
item_rect.Max.x = ImMin(item_rect.Max.x, ImMax(column->MaxX, clip_max_x));
}
else
{
item_rect.Min.x = ImMax(item_rect.Min.x, column->MinX);
item_rect.Max.x = ImMin(item_rect.Max.x, column->MaxX);
}
//GetForegroundDrawList()->AddRect(item_rect.Min, item_rect.Max, IM_COL32(255, 0, 255, 255));
}
const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(item_rect);
const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(item_rect);
if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
{ {
if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce) if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
@@ -8294,6 +8415,9 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
{ {
selected = !selected; selected = !selected;
MultiSelectAddSetRange(ms, selected, +1, item_data, item_data); MultiSelectAddSetRange(ms, selected, +1, item_data, item_data);
#ifdef IMGUI_DEBUG_BOXSELECT
GetForegroundDrawList()->AddRectFilled(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, selected ? IM_COL32(0, 255, 0, 200) : IM_COL32(255, 0, 0, 200));
#endif
} }
storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1); storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1);
} }
@@ -8407,7 +8531,6 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
} }
if (storage->NavIdItem == item_data) if (storage->NavIdItem == item_data)
ms->NavIdPassedBy = true; ms->NavIdPassedBy = true;
ms->LastSubmittedItem = item_data;
*p_selected = selected; *p_selected = selected;
*p_pressed = pressed; *p_pressed = pressed;
@@ -8423,15 +8546,20 @@ void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item) void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
{ {
// Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges) // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
// FIXME-OPT: Disabled on 2026/04/09 as this would break with any form of coarse clipping that we don't know about (e.g. TableNextColumn() return value).
// The low-hanging fruit would be to know that ImGuiSelectionUserData are sequential indices, in which case we can trivially compare PrevSubmittedItem + RangeDir == FirstItem.
// User can always perform this merge if required.
#if 0
if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0) if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
{ {
ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1]; ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected) if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->PrevSubmittedItem && prev->Selected == selected)
{ {
prev->RangeLastItem = last_item; prev->RangeLastItem = last_item;
return; return;
} }
} }
#endif
ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item }; ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item };
ms->IO.Requests.push_back(req); // Add new request ms->IO.Requests.push_back(req); // Add new request
@@ -8669,7 +8797,8 @@ bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
const ImGuiStyle& style = g.Style; const ImGuiStyle& style = g.Style;
const ImGuiID id = GetID(label); const ImGuiID id = GetID(label);
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
// Size default to hold ~7.25 items. // Size default to hold ~7.25 items.
// Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
@@ -8692,7 +8821,7 @@ bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
if (label_size.x > 0.0f) if (label_size.x > 0.0f)
{ {
ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y); ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
RenderText(label_pos, label); RenderText(label_pos, label, label_end, false);
window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size); window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size);
AlignTextToFramePadding(); AlignTextToFramePadding();
} }
@@ -8773,9 +8902,7 @@ bool ImGui::ListBox(const char* label, int* current_item, const char* (*getter)(
// - PlotHistogram() // - PlotHistogram()
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
// Plot/Graph widgets are not very good. // Plot/Graph widgets are not very good.
// Consider writing your own, or using a third-party one, see: // Consider using ImPlot (https://github.com/epezent/implot) which is much better!
// - ImPlot https://github.com/epezent/implot
// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg) int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg)
@@ -8788,7 +8915,8 @@ int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_get
const ImGuiStyle& style = g.Style; const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label); const ImGuiID id = window->GetID(label);
const ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
const ImVec2 label_size = CalcTextSize(label, label_end, false);
const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f); const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f);
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
@@ -8831,7 +8959,7 @@ int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_get
// Tooltip on hover // Tooltip on hover
if (hovered && inner_bb.Contains(g.IO.MousePos)) if (hovered && inner_bb.Contains(g.IO.MousePos))
{ {
const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.999f);
const int v_idx = (int)(t * item_count); const int v_idx = (int)(t * item_count);
IM_ASSERT(v_idx >= 0 && v_idx < values_count); IM_ASSERT(v_idx >= 0 && v_idx < values_count);
@@ -8887,10 +9015,11 @@ int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_get
RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f)); RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f));
if (label_size.x > 0.0f) if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label, label_end, false);
// Return hovered index or -1 if none are hovered. // Return hovered index or -1 if none are hovered.
// This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx(). // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx().
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
return idx_hovered; return idx_hovered;
} }
@@ -8920,6 +9049,7 @@ void ImGui::PlotLines(const char* label, float (*values_getter)(void* data, int
PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
} }
// Plot Histogram (the data provided _is_ histogram data. it doesn't compute the histogram of your data)
void ImGui::PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) void ImGui::PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride)
{ {
ImGuiPlotArrayGetterData data(values, stride); ImGuiPlotArrayGetterData data(values, stride);
@@ -9257,7 +9387,8 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
// Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
g.MenusIdSubmittedThisFrame.push_back(id); g.MenusIdSubmittedThisFrame.push_back(id);
ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
ImVec2 label_size = CalcTextSize(label, label_end, false);
// Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent without always being a Child window) // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent without always being a Child window)
// This is only done for items for the menu set and not the full parent window. // This is only done for items for the menu set and not the full parent window.
@@ -9276,8 +9407,7 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
bool pressed; bool pressed;
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoAutoClosePopups | (ImGuiSelectableFlags)ImGuiSelectableFlags_SelectOnClick;
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
ImGuiMenuColumns* offsets = &window->DC.MenuColumns; ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
{ {
@@ -9289,7 +9419,7 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, pos.y + window->DC.CurrLineTextBaseOffset); ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, pos.y + window->DC.CurrLineTextBaseOffset);
pressed = Selectable("", menu_is_open, selectable_flags, label_size); pressed = Selectable("", menu_is_open, selectable_flags, label_size);
LogSetNextTextDecoration("[", "]"); LogSetNextTextDecoration("[", "]");
RenderText(text_pos, label); RenderText(text_pos, label, label_end, false);
PopStyleVar(); PopStyleVar();
window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar().
popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), text_pos.y - style.FramePadding.y + window->MenuBarHeight); popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), text_pos.y - style.FramePadding.y + window->MenuBarHeight);
@@ -9306,7 +9436,7 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
ImVec2 text_pos(window->DC.CursorPos.x, pos.y + window->DC.CurrLineTextBaseOffset); ImVec2 text_pos(window->DC.CursorPos.x, pos.y + window->DC.CurrLineTextBaseOffset);
pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
LogSetNextTextDecoration("", ">"); LogSetNextTextDecoration("", ">");
RenderText(ImVec2(text_pos.x + offsets->OffsetLabel, text_pos.y), label); RenderText(ImVec2(text_pos.x + offsets->OffsetLabel, text_pos.y), label, label_end, false);
if (icon_w > 0.0f) if (icon_w > 0.0f)
RenderText(ImVec2(text_pos.x + offsets->OffsetIcon, text_pos.y), icon); RenderText(ImVec2(text_pos.x + offsets->OffsetIcon, text_pos.y), icon);
RenderArrow(window->DrawList, ImVec2(text_pos.x + offsets->OffsetMark + extra_w + g.FontSize * 0.30f, text_pos.y), GetColorU32(ImGuiCol_Text), ImGuiDir_Right); RenderArrow(window->DrawList, ImVec2(text_pos.x + offsets->OffsetMark + extra_w + g.FontSize * 0.30f, text_pos.y), GetColorU32(ImGuiCol_Text), ImGuiDir_Right);
@@ -9315,6 +9445,14 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
if (!enabled) if (!enabled)
EndDisabled(); EndDisabled();
// Once dragged, release ActiveId + key ownership. This is to allow the idiom of mouse down a menu, dragging elsewhere, up on some other MenuItem(). (#8233, #9394)
// Could move logic into lower-level ImGuiButtonFlags_AutoReleaseActiveId + ImGuiButtonFlags_AutoReleaseKeyOwner? Easier once we get rid of the Selectable() middle-man here.
if (g.ActiveId == id && g.HoveredId != id && g.ActiveIdSource == ImGuiInputSource_Mouse && IsMouseDragging(0))
{
ClearActiveID();
SetKeyOwner(ImGuiKey_MouseLeft, ImGuiKeyOwner_NoOwner);
}
const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav; const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
if (menuset_is_open) if (menuset_is_open)
PopItemFlag(); PopItemFlag();
@@ -9395,6 +9533,9 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0)); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
PopID(); PopID();
if (g.ActiveId == id && want_open)
g.ActiveIdNoClearOnFocusLoss = true;
if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size) if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
{ {
// Don't reopen/recycle same menu level in the same frame if it is a different menu ID, first close the other menu and yield for a frame. // Don't reopen/recycle same menu level in the same frame if it is a different menu ID, first close the other menu and yield for a frame.
@@ -9471,7 +9612,8 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
ImGuiStyle& style = g.Style; ImGuiStyle& style = g.Style;
ImVec2 pos = window->DC.CursorPos; ImVec2 pos = window->DC.CursorPos;
ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
ImVec2 label_size = CalcTextSize(label, label_end, false);
// See BeginMenuEx() for comments about this. // See BeginMenuEx() for comments about this.
const bool menuset_is_open = IsRootOfOpenMenuSet(); const bool menuset_is_open = IsRootOfOpenMenuSet();
@@ -9486,7 +9628,7 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut
BeginDisabled(); BeginDisabled();
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover; const ImGuiSelectableFlags selectable_flags = (ImGuiSelectableFlags)ImGuiSelectableFlags_SelectOnRelease | (ImGuiSelectableFlags)ImGuiSelectableFlags_SetNavIdOnHover;
ImGuiMenuColumns* offsets = &window->DC.MenuColumns; ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
{ {
@@ -9498,7 +9640,7 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut
pressed = Selectable("", selected, selectable_flags, ImVec2(label_size.x, 0.0f)); pressed = Selectable("", selected, selectable_flags, ImVec2(label_size.x, 0.0f));
PopStyleVar(); PopStyleVar();
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
RenderText(text_pos, label); RenderText(text_pos, label, label_end, false);
window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar().
} }
else else
@@ -9515,7 +9657,7 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut
pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
{ {
RenderText(text_pos + ImVec2(offsets->OffsetLabel, 0.0f), label); RenderText(text_pos + ImVec2(offsets->OffsetLabel, 0.0f), label, label_end, false);
if (icon_w > 0.0f) if (icon_w > 0.0f)
RenderText(text_pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); RenderText(text_pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
if (shortcut_w > 0.0f) if (shortcut_w > 0.0f)
@@ -9529,6 +9671,17 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut
RenderCheckMark(window->DrawList, text_pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f); RenderCheckMark(window->DrawList, text_pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f);
} }
} }
// Once dragged, release ActiveId + key ownership. This is to allow the idiom of mouse down a menu, dragging elsewhere, up on some other MenuItem(). (#8233, #9394)
// Could move logic into lower-level ImGuiButtonFlags_AutoReleaseActiveId + ImGuiButtonFlags_AutoReleaseKeyOwner? Easier once we get rid of the Selectable() middle-man here.
const ImGuiID id = g.LastItemData.ID;
if (g.ActiveId == id && g.HoveredId != id && g.ActiveIdSource == ImGuiInputSource_Mouse && IsMouseDragging(0))
{
ClearActiveID();
SetKeyOwner(ImGuiKey_MouseLeft, ImGuiKeyOwner_NoOwner);
}
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0)); IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
if (!enabled) if (!enabled)
EndDisabled(); EndDisabled();
@@ -10616,7 +10769,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
// We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation) // We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation)
const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX); const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
if (want_clip_rect) if (want_clip_rect)
PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true); PushClipRect(ImVec2(ImClamp(bb.Min.x, tab_bar->ScrollingRectMinX, tab_bar->ScrollingRectMaxX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true);
ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos; ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
ItemSize(bb.GetSize(), style.FramePadding.y); ItemSize(bb.GetSize(), style.FramePadding.y);
@@ -10740,7 +10893,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
float rounding = style.TabRounding; float rounding = style.TabRounding;
display_draw_list->PathArcToFast(tl + ImVec2(+rounding, +rounding), rounding, 7, 9); display_draw_list->PathArcToFast(tl + ImVec2(+rounding, +rounding), rounding, 7, 9);
display_draw_list->PathArcToFast(tr + ImVec2(-rounding, +rounding), rounding, 9, 11); display_draw_list->PathArcToFast(tr + ImVec2(-rounding, +rounding), rounding, 9, 11);
display_draw_list->PathStroke(overline_col, 0, style.TabBarOverlineSize); display_draw_list->PathStroke(overline_col, style.TabBarOverlineSize);
} }
else else
{ {
@@ -10845,20 +10998,16 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI
IM_UNUSED(flags); IM_UNUSED(flags);
IM_ASSERT(width > 0.0f); IM_ASSERT(width > 0.0f);
const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f)); const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f));
const float y1 = bb.Min.y + 1.0f; const float y1 = bb.Min.y + 1.0f; // Leave a bit of room in title bars.
const float y2 = bb.Max.y - g.Style.TabBarBorderSize; const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
draw_list->PathLineTo(ImVec2(bb.Min.x, y2)); draw_list->AddRectFilled(bb.Min, ImVec2(bb.Max.x, y2), col, rounding, ImDrawFlags_RoundCornersTop);
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9);
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12);
draw_list->PathLineTo(ImVec2(bb.Max.x, y2));
draw_list->PathFillConvex(col);
if (g.Style.TabBorderSize > 0.0f) if (g.Style.TabBorderSize > 0.0f)
{ {
draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2)); draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2));
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9); draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9);
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12); draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2)); draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2));
draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize); draw_list->PathStroke(GetColorU32(ImGuiCol_Border), g.Style.TabBorderSize);
} }
} }
@@ -10867,7 +11016,8 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI
void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped) void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped)
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
ImVec2 label_size = CalcTextSize(label, NULL, true); const char* label_end = FindRenderedTextEnd(label);
ImVec2 label_size = CalcTextSize(label, label_end, false);
if (out_just_closed) if (out_just_closed)
*out_just_closed = false; *out_just_closed = false;
@@ -10952,7 +11102,7 @@ void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb,
} }
} }
LogSetNextTextDecoration("/", "\\"); LogSetNextTextDecoration("/", "\\");
RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, ellipsis_max_x, label, NULL, &label_size); RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, ellipsis_max_x, label, label_end, &label_size);
#if 0 #if 0
if (!is_contents_visible) if (!is_contents_visible)
+162 -101
View File
@@ -33,13 +33,14 @@ namespace games::ccj {
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices, static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
UINT cbSize) { UINT cbSize) {
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize); auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
if (result == 0xFFFFFFFF) if (result == 0xFFFFFFFF) {
return result; return result;
}
if (pRawInputDeviceList == NULL) { if (pRawInputDeviceList == NULL) {
(*puiNumDevices)++; (*puiNumDevices)++;
} else if (result < *puiNumDevices) { } else if (result < *puiNumDevices) {
pRawInputDeviceList[result] = {fakeHandle, 0}; pRawInputDeviceList[result] = { fakeHandle, 0 };
result++; result++;
} }
@@ -47,8 +48,9 @@ namespace games::ccj {
} }
static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) { static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) {
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME) if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME) {
return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize); return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize);
}
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t); const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
@@ -68,23 +70,157 @@ namespace games::ccj {
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) { static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
wchar_t buffer[256]; wchar_t buffer[256];
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName)) if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName)) {
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong); return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
}
hWnd = _hWnd; hWnd = _hWnd;
wndProc = (WNDPROC)dwNewLong; wndProc = (WNDPROC)dwNewLong;
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong); return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
} }
// compute the cursor wrap region in client coordinates. The cursor is confined to the
// monitor, so if the window's client area extends past a screen edge (window larger than /
// offset off the monitor) the cursor can never reach that far client edge. Clamp the region
// to the on-screen portion of the client area so the right/bottom edges wrap as reliably as
// the left/top edges.
static RECT trackball_wrap_bounds(HWND wnd, const RECT &client) {
RECT bounds = client;
POINT origin = { 0, 0 };
ClientToScreen(wnd, &origin);
RECT clientScreen = {
origin.x,
origin.y,
origin.x + client.right,
origin.y + client.bottom
};
MONITORINFO mi = {};
mi.cbSize = sizeof(mi);
RECT usable;
if (GetMonitorInfo(MonitorFromWindow(wnd, MONITOR_DEFAULTTONEAREST), &mi)
&& IntersectRect(&usable, &clientScreen, &mi.rcMonitor)) {
bounds.left = usable.left - origin.x;
bounds.top = usable.top - origin.y;
bounds.right = usable.right - origin.x;
bounds.bottom = usable.bottom - origin.y;
}
return bounds;
}
// drive the trackball from the physical mouse cursor, wrapping it at the window edges so it
// can spin indefinitely. gated by the secondary-mouse button (hold or debounced toggle).
static void trackball_mouse_input(RAWMOUSE &rawMouse) {
static bool active = false;
static bool lastState = false;
static std::chrono::steady_clock::time_point lastModified = std::chrono::steady_clock::now();
static const std::chrono::milliseconds debounceDuration(100);
const auto currentTime = std::chrono::steady_clock::now();
const bool pressed = get_async_secondary_mouse();
const bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
active = !active;
lastModified = currentTime;
}
const bool engaged = focused
&& ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed));
if (!engaged) {
if (lastState && !active) {
lastState = false;
}
return;
}
POINT cursor;
RECT client;
GetClientRect(hWnd, &client);
GetCursorPos(&cursor);
ScreenToClient(hWnd, &cursor);
const RECT bounds = trackball_wrap_bounds(hWnd, client);
static int lastX = cursor.x;
static int lastY = cursor.y;
if (!lastState) {
lastX = cursor.x;
lastY = cursor.y;
lastState = true;
}
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
// wrap the cursor to the opposite edge once it reaches a boundary, so the trackball
// can keep spinning past the screen edge.
bool updateCursor = false;
auto wrap = [&updateCursor](LONG value, LONG lo, LONG hi) -> LONG {
if (value <= lo) {
updateCursor = true;
return hi - 5;
}
if (value >= hi - 1) {
updateCursor = true;
return lo + 5;
}
return value;
};
cursor.x = wrap(cursor.x, bounds.left, bounds.right);
cursor.y = wrap(cursor.y, bounds.top, bounds.bottom);
lastX = cursor.x;
lastY = cursor.y;
if (updateCursor) {
ClientToScreen(hWnd, &cursor);
SetCursorPos(cursor.x, cursor.y);
}
}
// drive the trackball from the configured analog axes / direction buttons.
static void trackball_mapped_input(RAWMOUSE &rawMouse) {
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
auto &analogs = get_analogs();
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
}
auto &buttons = get_buttons();
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up])) {
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down])) {
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left])) {
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right])) {
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
}
}
static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) { static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) {
if (hRawInput != fakeHandle) if (hRawInput != fakeHandle) {
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader); return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
}
if (pData == NULL) { if (pData == NULL) {
if (uiCommand == RID_HEADER) if (uiCommand == RID_HEADER) {
*pcbSize = sizeof(RAWINPUTHEADER); *pcbSize = sizeof(RAWINPUTHEADER);
else } else {
*pcbSize = sizeof(RAWINPUT); *pcbSize = sizeof(RAWINPUT);
}
return 0; return 0;
} }
@@ -107,92 +243,9 @@ namespace games::ccj {
RAWMOUSE rawMouse {}; RAWMOUSE rawMouse {};
if (MOUSE_TRACKBALL) { if (MOUSE_TRACKBALL) {
static bool active = false; trackball_mouse_input(rawMouse);
static bool lastState = false;
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
static std::chrono::milliseconds debounceDuration(100);
auto currentTime = std::chrono::steady_clock::now();
bool pressed = get_async_secondary_mouse();
bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
active = !active;
lastModified = currentTime;
}
if (focused && ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed))) {
POINT cursor;
RECT client;
GetClientRect(hWnd, &client);
int sx = client.right - client.left;
int sy = client.bottom - client.top;
GetCursorPos(&cursor);
ScreenToClient(hWnd, &cursor);
static int lastX = cursor.x;
static int lastY = cursor.y;
if (!lastState) {
lastX = cursor.x;
lastY = cursor.y;
lastState = true;
}
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
bool updateCursor = false;
if (cursor.x <= 0) {
updateCursor = true;
cursor.x = sx - 5;
} else if (cursor.x >= sx - 1) {
updateCursor = true;
cursor.x = 5;
}
if (cursor.y <= 0) {
updateCursor = true;
cursor.y = sy - 5;
} else if (cursor.y >= sy - 1) {
updateCursor = true;
cursor.y = 5;
}
lastX = cursor.x;
lastY = cursor.y;
if (updateCursor) {
ClientToScreen(hWnd, &cursor);
SetCursorPos(cursor.x, cursor.y);
}
} else if (lastState && !active) {
lastState = false;
}
} else { } else {
rawMouse.usFlags = MOUSE_MOVE_RELATIVE; trackball_mapped_input(rawMouse);
auto &analogs = get_analogs();
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
}
auto &buttons = get_buttons();
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up]))
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down]))
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left]))
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right]))
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
} }
*((RAWINPUT*)pData) = { header, { rawMouse } }; *((RAWINPUT*)pData) = { header, { rawMouse } };
@@ -203,10 +256,19 @@ namespace games::ccj {
} }
static BOOL WINAPI RegisterRawInputDevices_hook(PCRAWINPUTDEVICE pRawInputDevices, UINT uiNumDevices, UINT cbSize) { static BOOL WINAPI RegisterRawInputDevices_hook(PCRAWINPUTDEVICE pRawInputDevices, UINT uiNumDevices, UINT cbSize) {
if (uiNumDevices == 2 && pRawInputDevices[1].usUsage == HID_USAGE_GENERIC_GAMEPAD)
uiNumDevices = 1;
return RegisterRawInputDevices_orig(pRawInputDevices, uiNumDevices, cbSize); // if the caller is spice itself, then pass through.
if (pRawInputDevices &&
(uiNumDevices > 0) &&
(pRawInputDevices[0].hwndTarget == RI_MGR->input_hwnd)) {
return RegisterRawInputDevices_orig(pRawInputDevices, uiNumDevices, cbSize);
}
// otherwise, it must be the game; prevent the game from registering for raw input
// and hijacking WM_INPUT messages; we need that for rawinput to work.
// even if we drop this, trackball emulation and mouse-as-touch input still work
return TRUE;
} }
@@ -215,9 +277,8 @@ namespace games::ccj {
static bool initialized = false; static bool initialized = false;
if (initialized) { if (initialized) {
return; return;
} else {
initialized = true;
} }
initialized = true;
// announce // announce
log_info("trackball", "init"); log_info("trackball", "init");
@@ -237,8 +298,6 @@ namespace games::ccj {
} }
void trackball_thread_start() { void trackball_thread_start() {
using namespace std::chrono_literals;
tbThreadRunning = true; tbThreadRunning = true;
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE); log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
@@ -250,8 +309,9 @@ namespace games::ccj {
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle); wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
} }
if (!tbThreadRunning) if (!tbThreadRunning) {
break; break;
}
timer.sleep(10); timer.sleep(10);
} }
@@ -260,8 +320,9 @@ namespace games::ccj {
void trackball_thread_stop() { void trackball_thread_stop() {
tbThreadRunning = false; tbThreadRunning = false;
if (tbThread) if (tbThread) {
tbThread->join(); tbThread->join();
}
log_info("trackball", "thread stop"); log_info("trackball", "thread stop");
+171
View File
@@ -0,0 +1,171 @@
#include "asio.h"
#include <windows.h>
#include <cstring>
#include "avs/game.h"
#include "gitadora.h"
#include "util/detour.h"
#include "util/logging.h"
namespace games::gitadora {
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
// driver name in ASIO_DRIVER by intercepting registry calls to
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
// so we can recognise them on subsequent reg* calls.
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
PHKEY phkResult)
{
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == PARENT_ASIO_REG_HANDLE &&
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
*phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
const auto result = RegOpenKeyExA_orig(
real_asio_reg_handle,
ASIO_DRIVER.value().c_str(),
ulOptions,
samDesired,
&real_asio_device_reg_handle);
if (result != ERROR_SUCCESS) {
log_warning(
"gitadora::asio",
"failed to open registry subkey '{}', error=0x{:x}",
ASIO_DRIVER.value(), result);
log_warning(
"gitadora::asio",
"due to improper ASIO setting, audio init will fail");
}
return result;
}
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
}
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == HKEY_LOCAL_MACHINE &&
_stricmp(lpSubKey, "software\\asio") == 0)
{
*phkResult = PARENT_ASIO_REG_HANDLE;
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
}
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
}
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
if (dwIndex == 0) {
// forward to real handle just to verify the key exists; we
// overwrite the name with our fake driver string regardless
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
lpName[cchName - 1] = '\0';
}
return ret;
} else {
return ERROR_NO_MORE_ITEMS;
}
}
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
}
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData)
{
HKEY target = hKey;
if (ASIO_DRIVER.has_value() &&
lpValueName != nullptr &&
lpData != nullptr &&
lpcbData != nullptr &&
hKey == DEVICE_ASIO_REG_HANDLE) {
if (_stricmp(lpValueName, "Description") == 0) {
// engine may verify the driver name after open; ensure it still
// sees something containing "XONAR" so the substring check passes
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
if (*lpcbData < len) {
*lpcbData = static_cast<DWORD>(len);
return ERROR_MORE_DATA;
}
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
*lpcbData = static_cast<DWORD>(len);
if (lpType != nullptr) {
*lpType = REG_SZ;
}
return ERROR_SUCCESS;
}
// for everything else (CLSID etc.) defer to the real driver subkey
target = real_asio_device_reg_handle;
}
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
}
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
if (hKey == PARENT_ASIO_REG_HANDLE) {
if (real_asio_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_reg_handle);
real_asio_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
if (hKey == DEVICE_ASIO_REG_HANDLE) {
if (real_asio_device_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_device_reg_handle);
real_asio_device_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
return RegCloseKey_orig(hKey);
}
void asio_hook_init() {
if (!ASIO_DRIVER.has_value()) {
return;
}
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
RegEnumKeyA_orig = detour::iat_try(
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyA_orig = detour::iat_try(
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyExA_orig = detour::iat_try(
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
RegQueryValueExA_orig = detour::iat_try(
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
}
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
namespace games::gitadora {
// installs IAT registry hooks in gfdm.dll that redirect the game's
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
// driver name read from games::gitadora::ASIO_DRIVER.
//
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
// forward every call straight through to advapi32.
void asio_hook_init();
}
+106 -31
View File
@@ -1,8 +1,14 @@
#include "gitadora.h" #include "gitadora.h"
#include "asio.h"
#include "handle.h" #include "handle.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include <unordered_map> #include <unordered_map>
#include <ks.h>
#include <ksmedia.h>
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "hooks/audio/audio.h"
#include "hooks/audio/mme.h" #include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
@@ -27,6 +33,9 @@ namespace games::gitadora {
bool P2_LEFTY = false; bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false;
/* /*
* Prevent GitaDora from creating folders on F drive * Prevent GitaDora from creating folders on F drive
@@ -223,12 +232,6 @@ namespace games::gitadora {
} }
#endif #endif
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
// force scaling to make things usable
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() && !cfg::CONFIGURATOR_STANDALONE) {
overlay::UI_SCALE_PERCENT = 250;
}
// for guitar wail SOCD cleaning // for guitar wail SOCD cleaning
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent; socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
@@ -241,14 +244,46 @@ namespace games::gitadora {
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
if (is_arena_model() && !GRAPHICS_WINDOWED && !GRAPHICS_FORCE_SINGLE_ADAPTER) { if (is_arena_model()) {
const auto &monitors = sysutils::enumerate_monitors(); // in full screen, if single-adapter option is checked, it's functionally
const size_t active_count = monitors.size(); // the same as forcing a single monitor
log_info("gitadora", "arena model: detected {} active monitor(s)", active_count); if (!GRAPHICS_WINDOWED && GRAPHICS_FORCE_SINGLE_ADAPTER) {
if (active_count < 4) { ARENA_WINDOW_COUNT = 1;
log_info("gitadora", "arena model: enable single monitor mode due to insufficient monitors"); }
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOW = true; // figure out default settings if user didn't provide one
if (!ARENA_WINDOW_COUNT.has_value()) {
if (!GRAPHICS_WINDOWED && sysutils::enumerate_monitors().size() < 4) {
log_info("gitadora", "arena model: <4 monitors, defaulting to single window mode");
ARENA_WINDOW_COUNT = 1;
} else {
ARENA_WINDOW_COUNT = 4;
}
}
const int count = ARENA_WINDOW_COUNT.value();
switch (count) {
case 1:
log_info("gitadora", "arena model: single-window mode");
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
break;
case 2:
if (!GRAPHICS_WINDOWED) {
log_fatal(
"gitadora",
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4");
}
log_info("gitadora", "arena model: two-window mode");
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
break;
case 4:
log_info("gitadora", "arena model: four-window mode");
break;
default:
log_fatal(
"gitadora",
"arena model: unsupported window count: {}", count);
} }
} }
@@ -527,6 +562,13 @@ namespace games::gitadora {
void GitaDoraGame::attach() { void GitaDoraGame::attach() {
Game::attach(); Game::attach();
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
// force scaling to make things usable
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model()) {
log_info("gitadora", "forcing UI scale to 250% for arena model");
overlay::UI_SCALE_PERCENT = 250;
}
// modules // modules
HMODULE sharepj_module = libutils::try_module("libshare-pj.dll"); HMODULE sharepj_module = libutils::try_module("libshare-pj.dll");
HMODULE bmsd2_module = libutils::try_module("libbmsd2.dll"); HMODULE bmsd2_module = libutils::try_module("libbmsd2.dll");
@@ -553,6 +595,12 @@ namespace games::gitadora {
#ifdef SPICE64 #ifdef SPICE64
// gitadora arena model // gitadora arena model
auto aio = libutils::try_library("libaio.dll"); auto aio = libutils::try_library("libaio.dll");
// before we start patching and hooking things, detect invalid configuration
if (aio != nullptr && !is_arena_model()) {
log_fatal("gitadora", "arena model i/o (libaio.dll) detected, but <spec> is not an arena model - bad prop XML files?");
}
if (aio != nullptr) { if (aio != nullptr) {
SETUPAPI_SETTINGS settings{}; SETUPAPI_SETTINGS settings{};
settings.class_guid[0] = 0x86E0D1E0; settings.class_guid[0] = 0x86E0D1E0;
@@ -578,11 +626,32 @@ namespace games::gitadora {
detour::iat_try("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE); detour::iat_try("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE);
detour::iat_try("CreateDirectoryA", CreateDirectoryA_hook, avs::game::DLL_INSTANCE); detour::iat_try("CreateDirectoryA", CreateDirectoryA_hook, avs::game::DLL_INSTANCE);
// ASIO driver redirect (XONAR -> user-configured driver)
asio_hook_init();
// volume change prevention // volume change prevention
hooks::audio::mme::init(avs::game::DLL_INSTANCE); hooks::audio::mme::init(avs::game::DLL_INSTANCE);
// fake Realtek audio injection
// if ASIO init succeeds, game tries to look for audio device with `Realtek` in friendly name
// if ASIO init fails, game opens default audio device
// therefore, it's safe to enable this hook by default regardless of ASIO preference
// (unless the user explicitly disables it, of course)
if (ALLOW_REALTEK_AUDIO) {
log_info(
"gitadora",
"fake Realtek audio injection disabled "
"(user's real Realtek audio may be used after successful ASIO init)");
} else {
log_info(
"gitadora",
"fake Realtek audio injection enabled "
"(create a fake Realtek audio device to prevent crashes after successful ASIO init)");
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
}
// monitor/touch hooks (windowed or full screen) // monitor/touch hooks (windowed or full screen)
if (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay // enable touch hook for subscreen overlay
wintouchemu::FORCE = true; wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
@@ -620,26 +689,32 @@ namespace games::gitadora {
} }
// two channel mod // two channel mod
if (TWOCHANNEL) { if (TWOCHANNEL && !is_arena_model()) {
if (is_arena_model()) { HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
log_warning("gitadora", "two channel audio (-2ch) is not supported on Arena Model - use a patch instead"); HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
deferredlogs::defer_error_messages({
"two channel audio (-2ch) is not supported on Arena Model - use a patch instead",
});
} else { bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll"); if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
HMODULE bmsd_module = libutils::try_module("libbmsd.dll"); replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
log_warning("gitadora", "two channel mode failed");
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
log_warning("gitadora", "two channel mode failed");
}
} }
} }
#endif #endif
} }
void fix_audio_channel_mask(WAVEFORMATEX *format) {
if (!format || format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
return;
}
auto ext = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(format);
// fix the legacy 7.1 channel mask to the modern surround layout
// makes it more compatible with modern audio cards
if (ext->dwChannelMask == KSAUDIO_SPEAKER_7POINT1) {
ext->dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
}
}
} }
+8
View File
@@ -2,6 +2,9 @@
#include <optional> #include <optional>
#include <windows.h>
#include <mmreg.h>
#include "avs/game.h" #include "avs/game.h"
#include "games/game.h" #include "games/game.h"
#include "util/socd_cleaner.h" #include "util/socd_cleaner.h"
@@ -15,6 +18,9 @@ namespace games::gitadora {
extern bool P2_LEFTY; extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
extern std::optional<socd::SocdAlgorithm> PICK_ALGO; extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern std::optional<std::string> ASIO_DRIVER;
extern bool ALLOW_REALTEK_AUDIO;
class GitaDoraGame : public games::Game { class GitaDoraGame : public games::Game {
public: public:
@@ -23,6 +29,8 @@ namespace games::gitadora {
virtual void attach() override; virtual void attach() override;
}; };
void fix_audio_channel_mask(WAVEFORMATEX *format);
static inline bool is_drum() { static inline bool is_drum() {
return ( return (
avs::game::is_model({ "J32", "K32", "L32" }) || avs::game::is_model({ "J32", "K32", "L32" }) ||
+5 -37
View File
@@ -543,11 +543,7 @@ namespace games::iidx {
// if the user specified a value (other than auto), use it as the environment var // if the user specified a value (other than auto), use it as the environment var
// probably "wasapi" or "asio", but it's not explicitly checked here for forward compat // probably "wasapi" or "asio", but it's not explicitly checked here for forward compat
if (SOUND_OUTPUT_DEVICE.has_value() && SOUND_OUTPUT_DEVICE.value() != "auto") { if (SOUND_OUTPUT_DEVICE.has_value() && SOUND_OUTPUT_DEVICE.value() != "auto") {
log_info( // the environemnt variable was already set in launcher.cpp
"iidx",
"using user-supplied \"{}\" for SOUND_OUTPUT_DEVICE",
SOUND_OUTPUT_DEVICE.value());
SetEnvironmentVariable("SOUND_OUTPUT_DEVICE", SOUND_OUTPUT_DEVICE.value().c_str());
SOUND_OUTPUT_DEVICE_IN_EFFECT = SOUND_OUTPUT_DEVICE; SOUND_OUTPUT_DEVICE_IN_EFFECT = SOUND_OUTPUT_DEVICE;
return; return;
} }
@@ -893,38 +889,10 @@ namespace games::iidx {
} }
} }
// patch iidx32+ for asio compatibility // note: the iidx32+ ASIO refcount bug (a duplicate AddRef on the ASIO instance with
// only do this if NOT wasapi (as opposed to checking if it's asio) // no matching Release, which leaks the driver and can hang non-XONAR devices) is now
// the patch is only really needed for (some) non-XONAR devices but since people sometimes disguise // handled transparently by the WrappedAsio proxy (see hooks/audio/asio_proxy.cpp),
// other devices as a XONAR, don't check for the exact string (common ASIO workaround for INF) // so no game-DLL signature patch is needed here anymore
if (avs::game::is_ext(2024090100, INT_MAX) &&
!(SOUND_OUTPUT_DEVICE_IN_EFFECT.has_value() &&
SOUND_OUTPUT_DEVICE_IN_EFFECT.value() == "wasapi")) {
// in iidx32 final:
// ff 50 08 call QWORD PTR [rax+0x8] ; ASIO instance AddRef
// 48 8b 4b 08 mov rcx,QWORD PTR [rbx+0x8]
// 48 8b 01 mov rax,QWORD PTR [rcx]
// ff 50 08 call QWORD PTR [rax+0x8] ; ASIO instance AddRef
intptr_t result = replace_pattern(
avs::game::DLL_INSTANCE,
"FF50????????????????FF50??4533C94533C0418D51",
"????????????????????909090??????????????????",
0, 0);
if (result == 0) {
log_warning(
"iidx",
"Failed to apply ASIO compatibility fix for iidx32+. "
"Unless patches are applied, your ASIO device may hang or fail to work");
} else {
log_info(
"iidx",
"Successfully applied ASIO compatibility fix for iidx32+ using signature matching @ 0x{:x}.",
result);
}
}
#endif #endif
+5 -1
View File
@@ -444,6 +444,8 @@ namespace games {
// overlay button definitions // overlay button definitions
names.emplace_back("Screenshot"); names.emplace_back("Screenshot");
vkey_defaults.push_back(VK_SNAPSHOT); vkey_defaults.push_back(VK_SNAPSHOT);
names.emplace_back("Toggle All Windows");
vkey_defaults.push_back(VK_OEM_3); // backtick `
names.emplace_back("Toggle Main Menu"); names.emplace_back("Toggle Main Menu");
vkey_defaults.push_back(VK_ESCAPE); vkey_defaults.push_back(VK_ESCAPE);
names.emplace_back("Toggle Sub Screen"); names.emplace_back("Toggle Sub Screen");
@@ -468,10 +470,12 @@ namespace games {
vkey_defaults.push_back(VK_F10); vkey_defaults.push_back(VK_F10);
names.emplace_back("Toggle Screen Resize"); names.emplace_back("Toggle Screen Resize");
vkey_defaults.push_back(VK_F11); vkey_defaults.push_back(VK_F11);
names.emplace_back("Toggle Overlay"); names.emplace_back("Toggle FPS");
vkey_defaults.push_back(VK_F12); vkey_defaults.push_back(VK_F12);
names.emplace_back("Toggle Camera Control"); names.emplace_back("Toggle Camera Control");
vkey_defaults.push_back(0xFF); vkey_defaults.push_back(0xFF);
names.emplace_back("Toggle OBS Control");
vkey_defaults.push_back(0xFF);
names.emplace_back("Player 1 PIN Macro"); names.emplace_back("Player 1 PIN Macro");
vkey_defaults.push_back(0xFF); vkey_defaults.push_back(0xFF);
names.emplace_back("Player 2 PIN Macro"); names.emplace_back("Player 2 PIN Macro");
+3 -1
View File
@@ -8,6 +8,7 @@ namespace games {
namespace OverlayButtons { namespace OverlayButtons {
enum { enum {
Screenshot, Screenshot,
ToggleAllWindows,
ToggleMainMenu, ToggleMainMenu,
ToggleSubScreen, ToggleSubScreen,
InsertCoin, InsertCoin,
@@ -20,8 +21,9 @@ namespace games {
ToggleControl, ToggleControl,
TogglePatchManager, TogglePatchManager,
ToggleScreenResize, ToggleScreenResize,
ToggleOverlay, ToggleFps,
ToggleCameraControl, ToggleCameraControl,
ToggleOBSControl,
TriggerPinMacroP1, TriggerPinMacroP1,
TriggerPinMacroP2, TriggerPinMacroP2,
ScreenResize, ScreenResize,
+74
View File
@@ -4,6 +4,7 @@
#include "util/detour.h" #include "util/detour.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/sigscan.h"
#define OTOCA_DEBUG_VERBOSE 0 #define OTOCA_DEBUG_VERBOSE 0
#if OTOCA_DEBUG_VERBOSE #if OTOCA_DEBUG_VERBOSE
@@ -116,6 +117,73 @@ namespace games::otoca {
return ret; return ret;
} }
static void patch_holo_print_hang() {
// arkkep.dll's print method sets the printer busy (0x66 / Printing_Busy),
// then bails out with -1 WITHOUT printing when its holo parameter is
// nonzero (the card path passes 0 and works). The completion callback that
// clears busy never fires, so the page's poll loop spins forever -> hang.
// The image band is already built, so flip the je that skips the bail-out
// into a jmp (74 -> EB) to route holo through the print path like card.
//
// je issue_print ; <- patched to jmp
// or eax, -1 ; holo: return without printing
// ret 10h
auto arkkep = libutils::try_module("arkkep.dll");
if (arkkep == nullptr) {
log_warning("otoca", "arkkep.dll not loaded; skipping holo print hang fix");
return;
}
auto result = replace_pattern(
arkkep,
"C7465866000000895DC0C74710080700007416",
"C7465866000000895DC0C7471008070000EB16",
0, 0);
if (result) {
log_info("otoca", "patched hologram print hang in arkkep.dll");
} else {
log_warning("otoca", "could not patch hologram print hang (incompatible arkkep.dll?)");
}
}
static void patch_holo_print_search() {
// before printing, arkkep walks the page's printer list looking for one
// whose capability flag (byte at entry+120h) matches the job type
// (0 = card, 1 = holo). Our emulated printer is only card-capable, so a
// holo job finds no match and aborts without printing ("not found holo
// printer"). The emulated printer serves both media, so force the compare
// to always match by turning `cmp cl,[edi+120h]` into `cmp cl,cl` + NOPs,
// which always sets ZF. Two such compares exist, so patch both.
//
// cmp cl, [edi+120h] ; <- patched to `cmp cl,cl` (3A 8F.. -> 3A C9..)
// je printer_found
auto arkkep = libutils::try_module("arkkep.dll");
if (arkkep == nullptr) {
log_warning("otoca", "arkkep.dll not loaded; skipping holo print search fix");
return;
}
int patched = 0;
while (patched < 2) {
auto result = replace_pattern(
arkkep,
"3A8F20010000",
"3AC990909090",
0, 0);
if (!result) {
break;
}
patched++;
}
if (patched > 0) {
log_info("otoca", "patched hologram printer search in arkkep.dll ({} site(s))", patched);
} else {
log_warning("otoca", "could not patch hologram printer search (incompatible arkkep.dll?)");
}
}
void OtocaGame::attach() { void OtocaGame::attach() {
Game::attach(); Game::attach();
@@ -129,6 +197,12 @@ namespace games::otoca {
p4io_hook(); p4io_hook();
games::shared::printer_attach(); games::shared::printer_attach();
// fix hologram print hard hang in arkkep.dll
patch_holo_print_hang();
// make hologram jobs find the emulated printer (else they abort unprinted)
patch_holo_print_search();
if (BYPASS_CAMERA) { if (BYPASS_CAMERA) {
libutils::try_library("libcamera.dll"); libutils::try_library("libcamera.dll");
const auto libcamera = "libcamera.dll"; const auto libcamera = "libcamera.dll";
+72
View File
@@ -0,0 +1,72 @@
#include "sdvx_live2d.h"
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
// compiled out of 32-bit builds.
#ifdef SPICE64
#include <string>
#include "hooks/graphics/graphics.h"
#include "launcher/logger.h"
#include "util/logging.h"
namespace games::sdvx {
// Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option).
//
// the game logs scene transitions as "I:Attach: in <SCENE>" / "I:Detach: in
// <SCENE>". several scenes correspond to in-song gameplay (with the heavy
// Live2D rendering); we watch those log lines and keep the shared flag
// the d3d9 backend reads up to date. the hook never alters the log output
// (always returns false).
static bool live2d_scene_log_hook(
void *user, const std::string &data, logger::Style style, std::string &out) {
// any of these scenes counts as in-song gameplay (different play modes)
static const char *const gameplay_scenes[] = {
"in ALTERNATIVE_GAME_SCENE",
"in MEGAMIX_GAME_SCENE",
"in MEGAMIX_BATTLE",
"in BATTLE_GAME_SCENE",
"in AUTOMATION_GAME_SCENE",
"in ARENA_GAME_SCENE",
};
bool in_gameplay_scene = false;
for (const auto *scene : gameplay_scenes) {
if (data.find(scene) != std::string::npos) {
in_gameplay_scene = true;
break;
}
}
if (!in_gameplay_scene) {
return false;
}
// note: log messages here must NOT contain any matched scene token, else
// this hook would re-enter itself when the message is pushed.
if (data.find("I:Attach: in ") != std::string::npos) {
if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) {
log_info("sdvx", "Live2D skip: entering gameplay");
}
} else if (data.find("I:Detach: in ") != std::string::npos) {
if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) {
log_info("sdvx", "Live2D skip: leaving gameplay");
}
}
return false;
}
void live2d_scene_detection_init() {
static bool installed = false;
if (installed) {
return;
}
installed = true;
// the logger's hook list is a persistent static, so registering here is
// safe even though this runs before logger::start(). we intentionally do
// NOT log a confirmation now: at this point the log file isn't open yet
// and the message would be dropped. the entering/leaving-gameplay lines
// above provide runtime confirmation once the logger is running.
logger::hook_add(live2d_scene_log_hook, nullptr);
}
}
#endif // SPICE64
+14
View File
@@ -0,0 +1,14 @@
#pragma once
namespace games::sdvx {
#ifdef SPICE64
// installs the Live2D in-game scene-detection log hook used by the
// -sdvxnolive2d "ingame" option. does not require the SDVX game module to
// be attached, so it can be enabled purely from the launcher option.
// only the Live2D-capable SDVX versions are 64-bit, so this is compiled out
// of 32-bit builds.
void live2d_scene_detection_init();
#endif
}
+7
View File
@@ -7,6 +7,7 @@
#include "hooks/sleephook.h" #include "hooks/sleephook.h"
#include "hooks/libraryhook.h" #include "hooks/libraryhook.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "overlay/notifications.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/fileutils.h" #include "util/fileutils.h"
#include "util/libutils.h" #include "util/libutils.h"
@@ -397,8 +398,14 @@ namespace games::shared {
// logging // logging
if (success) { if (success) {
log_info("printer", "printer emulation has written an image to {}", image_path); log_info("printer", "printer emulation has written an image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Printer: saved {}", fileutils::basename(image_path)));
} else { } else {
log_warning("printer", "printer emulation failed to write image to {}", image_path); log_warning("printer", "printer emulation failed to write image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Error,
fmt::format("Printer: failed to write {}", fileutils::basename(image_path)));
} }
} }
} }
@@ -0,0 +1,74 @@
#include "asio_driver_scan.h"
#include <algorithm>
#include <windows.h>
#include "util/utils.h"
namespace hooks::audio {
static constexpr char ASIO_REG_PATH[] = "software\\asio";
static constexpr char ASIO_REG_DESC[] = "description";
// enumerate a single registry view, appending to entries while merging
// duplicates discovered in another view. Drivers are matched by name (not
// CLSID): the game's ASIO loader selects drivers by name, and some vendors
// register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR
// SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices.
static void scan_view(
REGSAM wow64_flag,
bool is_64bit,
std::vector<AsioDriverScanEntry> &entries) {
HKEY hkEnum = nullptr;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0,
KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) {
return;
}
char key_name[256];
for (DWORD index = 0;
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
index++) {
// read description (display name), fall back to the key name
char desc[256] = { 0 };
DWORD size = sizeof(desc);
std::string name = key_name;
if (RegGetValueA(hkEnum,
key_name,
ASIO_REG_DESC,
RRF_RT_REG_SZ | wow64_flag,
nullptr,
desc,
&size) == ERROR_SUCCESS && desc[0]) {
name = desc;
}
// merge with an existing entry from the other view (match by name)
const std::string name_lower = strtolower(name);
auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) {
return strtolower(e.name) == name_lower;
});
if (it == entries.end()) {
entries.push_back({ name });
it = entries.end() - 1;
}
it->found_32bit |= !is_64bit;
it->found_64bit |= is_64bit;
}
RegCloseKey(hkEnum);
}
std::vector<AsioDriverScanEntry> scan_asio_drivers() {
std::vector<AsioDriverScanEntry> entries;
// 64-bit view first so it wins ordering when present in both
scan_view(KEY_WOW64_64KEY, true, entries);
scan_view(KEY_WOW64_32KEY, false, entries);
return entries;
}
}
@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <vector>
namespace hooks::audio {
struct AsioDriverScanEntry {
std::string name;
bool found_32bit = false;
bool found_64bit = false;
};
std::vector<AsioDriverScanEntry> scan_asio_drivers();
}
File diff suppressed because it is too large Load Diff
+216
View File
@@ -0,0 +1,216 @@
#pragma once
#include <atomic>
#include <memory>
#include <string>
#include <vector>
#include <windows.h>
#include "external/asio/asio.h"
#include "external/asio/iasiodrv.h"
namespace hooks::audio::asio {
// returns true if a CoCreateInstance call is instantiating a registered ASIO driver.
// ASIO hosts pass the driver CLSID as both class id and interface id; we also validate
// it against the system's registered ASIO drivers to avoid false positives
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
// return a proxy that forwards every call to it
IUnknown *wrap(REFCLSID clsid, void *real);
}
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
struct WrappedAsio final : IAsio {
WrappedAsio(IAsio *real, REFCLSID clsid, std::string name)
: pReal(real), clsid(clsid), driver_name(std::move(name)) {
}
WrappedAsio(const WrappedAsio &) = delete;
WrappedAsio &operator=(const WrappedAsio &) = delete;
virtual ~WrappedAsio();
// selects which source channel pair of a multichannel ASIO output reaches the device's
// 2.0 front pair. when not None, the proxy presents the game's expected multichannel
// layout to the host so it proceeds to create_buffers, then opens only a two-channel
// stream on the real device and routes the selected pair onto it (see create_buffers).
// Front is the plain "force two channel" case (forward the device's own front pair);
// the others copy a different pair onto 0/1. assumes a standard 7.1 layout (0-indexed).
// set once at boot, before any wrapper exists, so it needs no synchronization
enum class StereoDownmix {
None, // feature disabled - full multichannel passthrough
Front, // channels 0/1 - the device front pair is forwarded as-is (no copy)
Center, // channel 2 duplicated to both 0 and 1
Rear, // channels 4/5 -> 0/1
Side, // channels 6/7 -> 0/1
};
static StereoDownmix STEREO_DOWNMIX;
// true when a stereo extraction is configured, i.e. the real device should open a 2.0
// stream and only the selected pair should reach it. the former standalone
// FORCE_TWO_CHANNELS flag is now just the Front case of this
static bool force_two_channels() {
return STEREO_DOWNMIX != StereoDownmix::None;
}
// some games hardcode a multichannel ASIO output and bail before create_buffers if
// get_channels reports fewer, so we report at least this many output channels when a
// stereo extraction is active
static constexpr long FORCED_OUTPUT_CHANNELS = 8;
// maps an option string ("front", "center", "rear", "side") to a StereoDownmix value,
// returning None for anything unrecognized
static StereoDownmix name_to_stereo_downmix(const char *name);
#pragma region IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override;
ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override;
#pragma endregion
#pragma region IAsio
AsioBool __thiscall init(void *sys_handle) override;
void __thiscall get_driver_name(char *name) override;
long __thiscall get_driver_version() override;
void __thiscall get_error_message(char *string) override;
AsioError __thiscall start() override;
AsioError __thiscall stop() override;
AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override;
AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override;
AsioError __thiscall get_buffer_size(
long *min_size,
long *max_size,
long *preferred_size,
long *granularity) override;
AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override;
AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override;
AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override;
AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override;
AsioError __thiscall set_clock_source(long reference) override;
AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override;
AsioError __thiscall get_channel_info(AsioChannelInfo *info) override;
AsioError __thiscall create_buffers(
AsioBufferInfo *buffer_infos,
long num_channels,
long buffer_size,
AsioCallbacks *callbacks) override;
AsioError __thiscall dispose_buffers() override;
AsioError __thiscall control_panel() override;
AsioError __thiscall future(long selector, void *opt) override;
AsioError __thiscall output_ready() override;
#pragma endregion
private:
// create_buffers implementation used when a stereo extraction is active: forwards only
// the channels the real device has and hands the game throwaway buffers for the rest
AsioError create_buffers_front_pair(
AsioBufferInfo *buffer_infos,
long num_channels,
long buffer_size,
AsioCallbacks *callbacks);
// if any post-processing effect (volume boost or stereo downmix) is active, saves the
// game's callbacks and returns a proxy callback set (our buffer-switch trampolines) to
// hand the real driver instead, so we can rework its output buffers after the game
// fills them. otherwise returns the game's callbacks unchanged. called at create_buffers
// time, before the stream starts
AsioCallbacks *install_proxy_callbacks(AsioCallbacks *game_callbacks);
// records a device output channel whose buffers we scale by the volume boost. queries
// the real driver for the channel's sample format. called at create_buffers time
void record_volume_output_channel(const AsioBufferInfo &info);
// the real device's output sample format, queried from its first output channel. all
// output channels of a device share one format, so this characterizes them all. returns
// ASIOSTLastEntry if the device has no output channels or the query fails
AsioSampleType device_output_sample_type();
// locates the destination pair (device channels 0/1) and the configured source channels
// in the game's buffer set so the realtime path can copy the selected pair onto 0/1.
// a no-op unless STEREO_DOWNMIX selects a non-front pair. called at create_buffers time
void record_downmix_channels(AsioBufferInfo *buffer_infos, long num_channels, long buffer_size);
// publishes the captured post-process state to the realtime thread once the buffers
// exist, making our trampolines start reworking output. called at the end of either
// create_buffers path
void publish_post_process(long buffer_size);
// detaches this instance from the realtime trampolines so they stop touching its
// buffers. called from dispose_buffers and the destructor
void detach_post_process();
// multiplies every recorded output channel's buffer for the given double-buffer index
// by the volume boost. runs on the driver's realtime thread from our buffer switch
void apply_output_volume(long double_buffer_index);
// copies the configured source channel pair onto device channels 0/1 for the given
// double-buffer index. runs on the driver's realtime thread from our buffer switch
void apply_downmix(long double_buffer_index);
// realtime-thread trampolines for the buffer-switch callbacks, handed to the real
// driver in place of the game's; ASIO callbacks carry no user data, so they reach the
// active wrapper through active_instance, call the game's original, then rework output.
// the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the
// game's own pointers, so they need no trampoline
static void __cdecl proxy_buffer_switch(long double_buffer_index, AsioBool direct_process);
static AsioTime * __cdecl proxy_buffer_switch_time_info(
AsioTime *params, long double_buffer_index, AsioBool direct_process);
// the single wrapper whose proxy callbacks are installed (ASIO is single-instance with
// one running stream); read by the static trampolines to reach the right wrapper
static std::atomic<WrappedAsio *> active_instance;
IAsio *const pReal;
const CLSID clsid;
// registry name of the driver (not get_driver_name), used in our logs as a single
// unambiguous name; constant for our lifetime
std::string driver_name;
// our own reference count; we hold one reference on pReal and release it when this
// drops to zero
std::atomic<ULONG> ref_count {1};
// throwaway double buffers handed to the channels we discard when a stereo extraction
// is active (see create_buffers). owned for the lifetime of the buffer set and freed
// in dispose_buffers; only read by the game from its own bufferSwitch, never by us
std::vector<std::unique_ptr<uint8_t[]>> dummy_buffers;
// one device output channel scaled by the volume boost in our buffer switch
struct VolumeOutputChannel {
void *buffers[2];
AsioSampleType type;
};
// the game's original callbacks (captured when we install our proxy set) and the proxy
// set we hand the real driver; the realtime trampolines reach the game's buffer_switch
// through game_callbacks regardless of which effect is active
AsioCallbacks game_callbacks {};
AsioCallbacks proxy_callbacks {};
// volume boost state, captured at create_buffers time and published to the realtime
// thread via active_instance once fully built; untouched while the stream runs.
// volume_active gates whether the realtime path scales any buffers
bool volume_active = false;
float volume_gain = 1.0f;
long volume_buffer_size = 0;
std::vector<VolumeOutputChannel> volume_channels;
// one device channel (0 or 1) fed by a source channel during stereo downmix; both
// buffer pointers are indexed by the ASIO double-buffer index, the same as the channels
struct DownmixCopy {
void *dst[2];
void *src[2];
};
// stereo downmix state, captured at create_buffers time and published alongside the
// volume state; untouched while the stream runs. downmix_active gates whether the
// realtime path copies the selected source pair onto device channels 0/1. copies[0]
// feeds device channel 0, copies[1] feeds device channel 1
bool downmix_active = false;
DownmixCopy downmix_copies[2] {};
size_t downmix_bytes = 0;
};
+11
View File
@@ -14,6 +14,7 @@
#include "audio_private.h" #include "audio_private.h"
#include "acm.h" #include "acm.h"
#include "asio_proxy.h"
#ifdef _MSC_VER #ifdef _MSC_VER
DEFINE_GUID(CLSID_MMDeviceEnumerator, DEFINE_GUID(CLSID_MMDeviceEnumerator,
@@ -39,9 +40,15 @@ namespace hooks::audio {
// public globals // public globals
bool ENABLED = true; bool ENABLED = true;
bool VOLUME_HOOK_ENABLED = true; bool VOLUME_HOOK_ENABLED = true;
std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM = std::nullopt;
float VOLUME_BOOST = 1.0f;
std::optional<uint32_t> RESAMPLE_RATE = std::nullopt;
std::optional<uint32_t> EXCLUSIVE_BUFFER_MS = std::nullopt;
bool WASAPI_COMPATIBILITY_MODE = false;
bool USE_DUMMY = false; bool USE_DUMMY = false;
WAVEFORMATEXTENSIBLE FORMAT {}; WAVEFORMATEXTENSIBLE FORMAT {};
std::optional<Backend> BACKEND = std::nullopt; std::optional<Backend> BACKEND = std::nullopt;
bool INJECT_FAKE_REALTEK_AUDIO = false;
std::optional<size_t> ASIO_DRIVER_ID = std::nullopt; std::optional<size_t> ASIO_DRIVER_ID = std::nullopt;
std::string ASIO_DRIVER_NAME = ""; std::string ASIO_DRIVER_NAME = "";
bool ASIO_FORCE_UNLOAD_ON_STOP = false; bool ASIO_FORCE_UNLOAD_ON_STOP = false;
@@ -91,6 +98,10 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
// wrap object // wrap object
auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv); auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv);
*mmde = new WrappedIMMDeviceEnumerator(*mmde); *mmde = new WrappedIMMDeviceEnumerator(*mmde);
} else if (ppv != nullptr && *ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
// wrap every ASIO driver so calls pass through to the real driver
*ppv = hooks::audio::asio::wrap(rclsid, *ppv);
} }
// return original result // return original result
+30
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <optional> #include <optional>
#include <string> #include <string>
@@ -16,11 +17,40 @@ namespace hooks::audio {
WaveOut, WaveOut,
}; };
// surround-to-stereo downmix algorithm
enum class DownmixAlgorithm {
FrontOnly, // keep only the front channels
RearOnly, // keep only the rear/back channels
SideOnly, // keep only the side channels
AC4, // AC-4 stereo downmix coefficients (ETSI TS 103 190-1)
Normalize, // all channels equally loud, LFE dropped
};
extern bool ENABLED; extern bool ENABLED;
extern bool VOLUME_HOOK_ENABLED; extern bool VOLUME_HOOK_ENABLED;
extern std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM;
extern float VOLUME_BOOST;
// target sample rate the hooked output is resampled to, if set
extern std::optional<uint32_t> RESAMPLE_RATE;
// minimum WASAPI exclusive buffer duration (milliseconds), if set. enlarges the device buffer
// to avoid underrun crackle on endpoints that cannot service a tiny buffer in time.
extern std::optional<uint32_t> EXCLUSIVE_BUFFER_MS;
// when true, WASAPI compatibility mode is active: exclusive-mode streams are redirected to
// shared mode. the Windows audio engine performs any required sample-rate / channel / bit-depth
// conversion, so the game's format is passed through unchanged and other applications can play
// audio simultaneously.
extern bool WASAPI_COMPATIBILITY_MODE;
extern bool USE_DUMMY; extern bool USE_DUMMY;
extern WAVEFORMATEXTENSIBLE FORMAT; extern WAVEFORMATEXTENSIBLE FORMAT;
extern std::optional<Backend> BACKEND; extern std::optional<Backend> BACKEND;
// when true, a synthetic "Realtek" render endpoint is injected into device enumeration that
// discards all audio. used by gitadora arena, whose device search crashes when no render
// endpoint reports a "Realtek" friendly name.
extern bool INJECT_FAKE_REALTEK_AUDIO;
extern std::optional<size_t> ASIO_DRIVER_ID; extern std::optional<size_t> ASIO_DRIVER_ID;
extern std::string ASIO_DRIVER_NAME; extern std::string ASIO_DRIVER_NAME;
extern bool ASIO_FORCE_UNLOAD_ON_STOP; extern bool ASIO_FORCE_UNLOAD_ON_STOP;
@@ -1,5 +1,6 @@
#include "device_collection.h" #include "device_collection.h"
#include "device.h" #include "device.h"
#include "null_device.h"
#include "util/utils.h" #include "util/utils.h"
#include "util/logging.h" #include "util/logging.h"
@@ -28,17 +29,48 @@ ULONG STDMETHODCALLTYPE WrappedIMMDeviceCollection::Release() {
return refs; return refs;
} }
bool WrappedIMMDeviceCollection::should_inject_fake_realtek() const {
return null_render_device_enabled()
&& (data_flow == eRender || data_flow == eAll);
}
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::GetCount(UINT *pcDevices) { HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::GetCount(UINT *pcDevices) {
// when active, hide all real devices and present only the synthetic one
if (should_inject_fake_realtek()) {
if (pcDevices == nullptr) {
return E_POINTER;
}
*pcDevices = 1;
return S_OK;
}
return pReal->GetCount(pcDevices); return pReal->GetCount(pcDevices);
} }
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::Item(UINT nDevice, IMMDevice **ppDevice) { HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::Item(UINT nDevice, IMMDevice **ppDevice) {
if (ppDevice == nullptr) {
return E_POINTER;
}
// when active, the only device in the collection is the synthetic fake Realtek
// render device; all real devices are hidden
if (should_inject_fake_realtek()) {
if (nDevice != 0) {
return E_INVALIDARG;
}
log_info("audio", "WrappedIMMDeviceCollection::Item[{}] -> synthetic fake Realtek render device", nDevice);
*ppDevice = new NullMMDevice();
return S_OK;
}
log_info("audio", "WrappedIMMDeviceCollection::Item[{}]", nDevice); log_info("audio", "WrappedIMMDeviceCollection::Item[{}]", nDevice);
// call original // call original
const auto hr = pReal->Item(nDevice, ppDevice); const auto hr = pReal->Item(nDevice, ppDevice);
// wrap interface // wrap interface
*ppDevice = new WrappedIMMDevice(*ppDevice); if (SUCCEEDED(hr) && *ppDevice != nullptr) {
*ppDevice = new WrappedIMMDevice(*ppDevice);
}
return hr; return hr;
} }
@@ -4,7 +4,8 @@
#include <mmdeviceapi.h> #include <mmdeviceapi.h>
struct WrappedIMMDeviceCollection : IMMDeviceCollection { struct WrappedIMMDeviceCollection : IMMDeviceCollection {
explicit WrappedIMMDeviceCollection(IMMDeviceCollection *orig) : pReal(orig) { WrappedIMMDeviceCollection(IMMDeviceCollection *orig, EDataFlow dataFlow)
: pReal(orig), data_flow(dataFlow) {
} }
WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete; WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete;
@@ -24,5 +25,9 @@ struct WrappedIMMDeviceCollection : IMMDeviceCollection {
#pragma endregion #pragma endregion
private: private:
// whether the synthetic fake Realtek render device should be appended to this collection
bool should_inject_fake_realtek() const;
IMMDeviceCollection *const pReal; IMMDeviceCollection *const pReal;
const EDataFlow data_flow;
}; };
@@ -45,7 +45,7 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::EnumAudioEndpoints(
{ {
const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices); const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) { if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) {
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices); *ppDevices = new WrappedIMMDeviceCollection(*ppDevices, dataFlow);
} }
return hr; return hr;
} }
@@ -0,0 +1,195 @@
#include "null_device.h"
#include <atomic>
#include <cstring>
#include <audioclient.h>
#include "hooks/audio/audio.h"
#include "hooks/audio/audio_private.h"
#include "hooks/audio/backends/wasapi/dummy_audio_client.h"
#include "util/logging.h"
#include "util/utils.h"
#include "null_discard_backend.h"
// friendly name reported by the synthetic device. must contain "Realtek" so the
// gitadora arena device search matches it.
static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio";
// arbitrary identifier reported by the synthetic device.
static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}";
// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h
static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = {
{ 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } },
14
};
bool null_render_device_enabled() {
return hooks::audio::INJECT_FAKE_REALTEK_AUDIO;
}
// duplicate a wide string into CoTaskMem so the caller can free it with
// CoTaskMemFree / PropVariantClear as the COM API contract requires.
static LPWSTR co_task_wcsdup(const wchar_t *src) {
const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t);
auto *dst = static_cast<LPWSTR>(CoTaskMemAlloc(bytes));
if (dst != nullptr) {
memcpy(dst, src, bytes);
}
return dst;
}
namespace {
// minimal IPropertyStore that only answers PKEY_Device_FriendlyName.
struct NullPropertyStore : IPropertyStore {
std::atomic<ULONG> ref_cnt = 1;
virtual ~NullPropertyStore() = default;
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override {
if (ppvObj == nullptr) {
return E_POINTER;
}
if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) {
this->AddRef();
*ppvObj = this;
return S_OK;
}
*ppvObj = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return ++this->ref_cnt;
}
ULONG STDMETHODCALLTYPE Release() override {
const ULONG refs = --this->ref_cnt;
if (refs == 0) {
delete this;
}
return refs;
}
HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override {
if (cProps == nullptr) {
return E_POINTER;
}
*cProps = 1;
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override {
if (pkey == nullptr) {
return E_POINTER;
}
if (iProp != 0) {
return E_INVALIDARG;
}
*pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL;
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override {
if (pv == nullptr) {
return E_POINTER;
}
PropVariantInit(pv);
if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid
&& key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) {
pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME);
if (pv->pwszVal == nullptr) {
return E_OUTOFMEMORY;
}
pv->vt = VT_LPWSTR;
}
// unknown keys are returned as VT_EMPTY / S_OK
return S_OK;
}
HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override {
return STG_E_ACCESSDENIED;
}
HRESULT STDMETHODCALLTYPE Commit() override {
return S_OK;
}
};
}
#pragma region IUnknown
HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) {
return E_POINTER;
}
if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) {
this->AddRef();
*ppvObj = this;
return S_OK;
}
*ppvObj = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() {
return ++this->ref_cnt;
}
ULONG STDMETHODCALLTYPE NullMMDevice::Release() {
const ULONG refs = --this->ref_cnt;
if (refs == 0) {
delete this;
}
return refs;
}
#pragma endregion
#pragma region IMMDevice
HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
REFIID iid,
DWORD,
PROPVARIANT *,
void **ppInterface)
{
if (ppInterface == nullptr) {
return E_POINTER;
}
*ppInterface = nullptr;
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
if (iid == IID_IAudioClient) {
// release any previously persisted client
if (hooks::audio::CLIENT != nullptr) {
hooks::audio::CLIENT->Release();
}
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
*ppInterface = client;
// persist the audio client
hooks::audio::CLIENT = client;
hooks::audio::CLIENT->AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) {
if (ppProperties == nullptr) {
return E_POINTER;
}
*ppProperties = new NullPropertyStore();
return S_OK;
}
HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) {
if (ppstrId == nullptr) {
return E_POINTER;
}
*ppstrId = co_task_wcsdup(NULL_DEVICE_ID);
return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY;
}
HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) {
if (pdwState == nullptr) {
return E_POINTER;
}
*pdwState = DEVICE_STATE_ACTIVE;
return S_OK;
}
#pragma endregion
@@ -0,0 +1,39 @@
#pragma once
#include <atomic>
#include <mmdeviceapi.h>
// returns true when a synthetic render endpoint should be injected into device
// enumeration. games like gitadora arena search the render endpoint list for a
// device whose friendly name contains "Realtek" and crash with a null pointer
// dereference when no match exists. presenting a fake match that routes to the
// null audio backend lets the search succeed while discarding the audio.
bool null_render_device_enabled();
// fake IMMDevice that reports a "Realtek" friendly name and activates straight
// into the null audio backend, never touching real hardware.
struct NullMMDevice : IMMDevice {
NullMMDevice() = default;
NullMMDevice(const NullMMDevice &) = delete;
NullMMDevice &operator=(const NullMMDevice &) = delete;
virtual ~NullMMDevice() = default;
#pragma region IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override;
#pragma endregion
#pragma region IMMDevice
HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override;
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override;
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override;
HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override;
#pragma endregion
private:
std::atomic<ULONG> ref_cnt = 1;
};
@@ -0,0 +1,141 @@
#include "null_discard_backend.h"
#include <algorithm>
#include <chrono>
#include "hooks/audio/util.h"
#include "util/logging.h"
#include "util/precise_timer.h"
NullDiscardBackend::~NullDiscardBackend() {
this->running = false;
if (this->pacing_thread.joinable()) {
this->pacing_thread.join();
}
}
const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept {
return this->format_;
}
HRESULT NullDiscardBackend::on_initialize(
AUDCLNT_SHAREMODE *,
DWORD *,
REFERENCE_TIME *hnsBufferDuration,
REFERENCE_TIME *,
const WAVEFORMATEX *pFormat,
LPCGUID)
{
copy_wave_format(&this->format_, pFormat);
// honor the game's requested buffer duration, falling back to 10 ms
constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units
this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0)
? *hnsBufferDuration
: DEFAULT_REFTIME;
this->buffer_frames = std::max<uint32_t>(1, static_cast<uint32_t>(
static_cast<double>(this->format_.Format.nSamplesPerSec)
* this->period_reftime / 10000000.0 + 0.5));
log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit",
this->format_.Format.nChannels,
this->format_.Format.nSamplesPerSec,
this->format_.Format.wBitsPerSample);
return S_OK;
}
HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) {
*buffer_frames = this->buffer_frames;
return S_OK;
}
HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) {
*latency = this->period_reftime;
return S_OK;
}
HRESULT NullDiscardBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) {
// discarded immediately, so the buffer always reads as fully drained
padding_frames = 0;
return S_OK;
}
HRESULT NullDiscardBackend::on_is_format_supported(
AUDCLNT_SHAREMODE *,
const WAVEFORMATEX *,
WAVEFORMATEX **ppClosestMatch)
{
if (ppClosestMatch) {
*ppClosestMatch = nullptr;
}
return S_OK;
}
HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) {
return E_NOTIMPL;
}
HRESULT NullDiscardBackend::on_get_device_period(
REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period)
{
if (default_device_period) {
*default_device_period = this->period_reftime;
}
if (minimum_device_period) {
*minimum_device_period = this->period_reftime;
}
return S_OK;
}
HRESULT NullDiscardBackend::on_start() {
if (!this->running.exchange(true)) {
this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this);
}
return S_OK;
}
HRESULT NullDiscardBackend::on_stop() {
return S_OK;
}
HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) {
// keep the game's event so pace_loop() can wake it; there is no real device behind it
this->relay_handle = *event_handle;
return S_OK;
}
HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
const size_t buffer_size =
static_cast<size_t>(this->format_.Format.nBlockAlign) * num_frames_requested;
if (this->scratch.size() < buffer_size) {
this->scratch.resize(buffer_size);
}
*ppData = this->scratch.data();
return S_OK;
}
HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
// discard the audio entirely
return S_OK;
}
void NullDiscardBackend::pace_loop() {
using namespace std::chrono;
timeutils::PreciseSleepTimer timer;
// audio is discarded, so timing precision and drift do not matter; just wake the
// game once per buffer period to keep its render thread from blocking on the event.
const auto period = duration_cast<steady_clock::duration>(
duration<double>(this->period_reftime / 10000000.0));
while (this->running.load()) {
if (this->relay_handle) {
SetEvent(this->relay_handle);
}
timer.sleep(period);
}
}
@@ -0,0 +1,54 @@
#pragma once
#include <atomic>
#include <optional>
#include <thread>
#include <vector>
#include <audioclient.h>
#include "hooks/audio/implementations/backend.h"
// discards all audio while pacing the game's event handle once per buffer period, so the game
// keeps running normally with nothing output to any real device. routed through the shared
// DummyIAudioClient, the same plumbing the asio backend uses.
struct NullDiscardBackend final : AudioBackend {
~NullDiscardBackend() final;
const WAVEFORMATEXTENSIBLE &format() const noexcept override;
HRESULT on_initialize(
AUDCLNT_SHAREMODE *,
DWORD *,
REFERENCE_TIME *hnsBufferDuration,
REFERENCE_TIME *,
const WAVEFORMATEX *pFormat,
LPCGUID) override;
HRESULT on_get_buffer_size(uint32_t *buffer_frames) override;
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override;
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) override;
HRESULT on_is_format_supported(
AUDCLNT_SHAREMODE *,
const WAVEFORMATEX *,
WAVEFORMATEX **ppClosestMatch) override;
HRESULT on_get_mix_format(WAVEFORMATEX **) override;
HRESULT on_get_device_period(
REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period) override;
HRESULT on_start() override;
HRESULT on_stop() override;
HRESULT on_set_event_handle(HANDLE *event_handle) override;
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override;
HRESULT on_release_buffer(uint32_t, DWORD) override;
private:
void pace_loop();
WAVEFORMATEXTENSIBLE format_ {};
uint32_t buffer_frames = 0;
REFERENCE_TIME period_reftime = 0;
HANDLE relay_handle = nullptr;
std::vector<BYTE> scratch;
std::thread pacing_thread;
std::atomic<bool> running = false;
};
@@ -4,6 +4,7 @@
#include <ksmedia.h> #include <ksmedia.h>
#include "avs/game.h" #include "avs/game.h"
#include "games/gitadora/gitadora.h"
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/util.h" #include "hooks/audio/util.h"
#include "hooks/audio/backends/wasapi/util.h" #include "hooks/audio/backends/wasapi/util.h"
@@ -41,6 +42,27 @@ static void fix_rec_format(WAVEFORMATEX *pFormat) {
pFormat->nAvgBytesPerSec = pFormat->nSamplesPerSec * pFormat->nBlockAlign; pFormat->nAvgBytesPerSec = pFormat->nSamplesPerSec * pFormat->nBlockAlign;
} }
// decide whether the given multi-channel format should be downmixed to stereo and which algorithm
// to use. an explicit user selection (-downmix) takes precedence; otherwise gitadora arena
// two-channel mode defaults to the AC-4 algorithm.
static std::optional<hooks::audio::DownmixAlgorithm> resolve_downmix(const WAVEFORMATEX *format) {
if (format == nullptr
|| format->nChannels <= 2
|| format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
return std::nullopt;
}
if (hooks::audio::DOWNMIX_ALGORITHM.has_value()) {
return hooks::audio::DOWNMIX_ALGORITHM;
}
if (games::gitadora::is_arena_model() && games::gitadora::TWOCHANNEL) {
return hooks::audio::DownmixAlgorithm::AC4;
}
return std::nullopt;
}
IAudioClient *wrap_audio_client(IAudioClient *audio_client) { IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
log_misc("audio::wasapi", "wrapping IAudioClient"); log_misc("audio::wasapi", "wrapping IAudioClient");
@@ -145,13 +167,51 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat)); fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
} }
// apply the -wasapishared option: redirect an exclusive request to shared mode. once redirected,
// spice's own downmix/resample paths below are skipped (gated on redirected_from_exclusive) and
// the shared engine handles any format conversion via AUTOCONVERTPCM (PCM / float only).
if (hooks::audio::SharedRedirect::wants(ShareMode, pFormat)) {
this->shared.apply(&ShareMode, &StreamFlags, &hnsPeriodicity);
} else if (hooks::audio::WASAPI_COMPATIBILITY_MODE && ShareMode == AUDCLNT_SHAREMODE_SHARED) {
log_warning(
"audio::wasapi",
"-wasapishared is enabled but the game is already opening a shared-mode stream; "
"the option has no effect");
}
WAVEFORMATEXTENSIBLE stereo_storage = {};
WAVEFORMATEXTENSIBLE resample_storage = {};
const WAVEFORMATEX *device_format = pFormat;
if (!this->shared.redirected_from_exclusive) {
// when downmixing, open the real device as stereo while the game keeps writing its native
// multi-channel format into the scratch buffer.
if (auto algorithm = resolve_downmix(pFormat)) {
this->downmix.setup(pFormat, &stereo_storage, *algorithm);
device_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
log_info("audio::wasapi", "downmix enabled: {} channels -> 2 channels ({})",
pFormat->nChannels, hooks::audio::Downmix::algorithm_name(*algorithm));
} else if (games::gitadora::is_arena_model()) {
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
}
// when resampling, open the real device at the target rate while the game keeps writing its
// native-rate audio into the scratch buffer. this runs on whatever device_format is now: the
// game's native format, or the stereo format produced above when downmix is also active, so
// the two stages chain as multi-channel -> stereo -> resampled stereo.
if (auto target_rate = hooks::audio::Resampler::resolve(device_format)) {
const uint32_t src_rate = device_format->nSamplesPerSec;
this->resample.setup(device_format, &resample_storage, *target_rate);
device_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
log_info("audio::wasapi", "resample enabled: {} Hz -> {} Hz{}",
src_rate, *target_rate, this->downmix.enabled ? " (after downmix)" : "");
}
}
// verbose output // verbose output
log_info("audio::wasapi", "IAudioClient::Initialize hook hit"); log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode)); print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, device_format);
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
print_format(pFormat);
if (this->backend) { if (this->backend) {
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize( SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
@@ -163,27 +223,67 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
AudioSessionGuid)); AudioSessionGuid));
log_info("audio::wasapi", "AudioBackend::on_initialize call finished"); log_info("audio::wasapi", "AudioBackend::on_initialize call finished");
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode)); print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
print_format(pFormat);
} }
// check for exclusive mode // check for exclusive mode
if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) { if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
this->exclusive_mode = true; this->exclusive_mode = true;
this->frame_size = pFormat->nChannels * (pFormat->wBitsPerSample / 8); this->frame_size = device_format->nChannels * (device_format->wBitsPerSample / 8);
// optionally enlarge the exclusive buffer. games request a very small buffer (e.g. 3 ms)
// which some endpoints (notably NVIDIA HDMI/DP display audio) cannot service in time,
// underrunning mid-period and crackling. a larger buffer gives the device slack. exclusive
// mode requires periodicity == buffer_duration, so raise both together; the initialize
// paths below handle any required buffer-size realignment.
if (hooks::audio::EXCLUSIVE_BUFFER_MS.has_value()) {
const REFERENCE_TIME min_duration =
(REFERENCE_TIME) hooks::audio::EXCLUSIVE_BUFFER_MS.value() * 10000;
if (hnsBufferDuration < min_duration) {
log_info("audio::wasapi",
"raising exclusive buffer from {} hns to {} hns ({} ms)",
hnsBufferDuration, min_duration, hooks::audio::EXCLUSIVE_BUFFER_MS.value());
hnsBufferDuration = min_duration;
if (hnsPeriodicity != 0) {
hnsPeriodicity = min_duration;
}
}
}
} }
// call next // call next. the resampler owns the device interaction whenever it is active (including when
HRESULT ret = pReal->Initialize( // chained after the downmix), otherwise the downmix does, otherwise the device is opened
ShareMode, // directly.
StreamFlags, HRESULT ret;
hnsBufferDuration, if (this->resample.enabled) {
hnsPeriodicity, ret = this->resample.initialize(
pFormat, pReal,
AudioSessionGuid); ShareMode,
StreamFlags,
hnsBufferDuration,
hnsPeriodicity,
device_format,
AudioSessionGuid);
} else if (this->downmix.enabled) {
ret = this->downmix.initialize(
pReal,
ShareMode,
StreamFlags,
hnsBufferDuration,
hnsPeriodicity,
device_format,
AudioSessionGuid);
} else {
ret = initialize_with_alignment_retry(
pReal,
"audio::wasapi",
ShareMode,
StreamFlags,
hnsBufferDuration,
hnsPeriodicity,
device_format,
AudioSessionGuid);
}
// check for failure // check for failure
if (FAILED(ret)) { if (FAILED(ret)) {
@@ -192,7 +292,15 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
} }
log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret)); log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret));
copy_wave_format(&hooks::audio::FORMAT, pFormat); copy_wave_format(&hooks::audio::FORMAT, device_format);
copy_wave_format(&this->device_format, device_format);
// arm the shared-mode buffer bridge so the redirected game's full-buffer writes are paced to
// the device instead of overflowing the shared buffer (AUDCLNT_E_BUFFER_TOO_LARGE).
if (this->shared.redirected_from_exclusive) {
this->shared.enable_bridge(
device_format->nChannels * (device_format->wBitsPerSample / 8));
}
return ret; return ret;
} }
@@ -214,7 +322,21 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferF
} }
} }
CHECK_RESULT(pReal->GetBufferSize(pNumBufferFrames)); HRESULT ret = pReal->GetBufferSize(pNumBufferFrames);
// report the buffer size at the game's native rate; the real device buffer is at the
// resampled rate, so translate it back so the game paces its writes correctly.
if (SUCCEEDED(ret) && this->resample.enabled && pNumBufferFrames) {
*pNumBufferFrames = this->resample.frames_device_to_game(*pNumBufferFrames);
}
// redirected to shared mode: clamp the reported buffer to one device period (see SharedRedirect).
if (SUCCEEDED(ret) && this->shared.redirected_from_exclusive && pNumBufferFrames) {
*pNumBufferFrames = this->shared.clamp_buffer_size(
pReal, this->device_format.Format.nSamplesPerSec, *pNumBufferFrames);
}
CHECK_RESULT(ret);
} }
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetStreamLatency(REFERENCE_TIME *phnsLatency) { HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetStreamLatency(REFERENCE_TIME *phnsLatency) {
static std::once_flag printed; static std::once_flag printed;
@@ -256,7 +378,21 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetCurrentPadding(UINT32 *pNumPad
} }
} }
CHECK_RESULT(pReal->GetCurrentPadding(pNumPaddingFrames)); HRESULT ret = pReal->GetCurrentPadding(pNumPaddingFrames);
// the device buffer is at the resampled rate; report padding at the game's native rate so the
// game's free-space calculation stays paced correctly.
if (SUCCEEDED(ret) && this->resample.enabled && pNumPaddingFrames) {
*pNumPaddingFrames = this->resample.padding_device_to_game(*pNumPaddingFrames);
}
// shared-mode bridge: the game writes into a FIFO, not the device buffer, so report the FIFO's
// fill level rather than the device's padding (which is in a different buffer space).
if (SUCCEEDED(ret) && this->shared.bridge_enabled() && pNumPaddingFrames) {
*pNumPaddingFrames = this->shared.virtual_padding();
}
CHECK_RESULT(ret);
} }
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported( HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
AUDCLNT_SHAREMODE ShareMode, AUDCLNT_SHAREMODE ShareMode,
@@ -274,6 +410,54 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat)); fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
} }
// log the format the game is asking about
log_info("audio::wasapi", "IAudioClient::IsFormatSupported hook hit");
print_format(ShareMode, pFormat);
// under the exclusive->shared redirect, report the exclusive format as supported so the game
// doesn't fall back before reaching Initialize.
if (hooks::audio::SharedRedirect::wants(ShareMode, pFormat)) {
log_info("audio::wasapi", "... reporting supported (will redirect to shared mode)");
if (ppClosestMatch) {
*ppClosestMatch = nullptr;
}
return S_OK;
}
// when downmixing, the real device is opened as stereo, so check whether the equivalent
// stereo format is supported instead of the multi-channel one. when resampling is also active
// it chains onto that stereo format, so check the resampled stereo format.
if (resolve_downmix(pFormat)) {
WAVEFORMATEXTENSIBLE stereo_storage = {};
hooks::audio::Downmix::make_stereo_format(pFormat, &stereo_storage);
const WAVEFORMATEX *check_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
WAVEFORMATEXTENSIBLE resample_storage = {};
if (auto target_rate = hooks::audio::Resampler::resolve(check_format)) {
hooks::audio::Resampler::make_device_format(check_format, &resample_storage, *target_rate);
check_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
}
log_info("audio::wasapi", "... checking device format instead (after downmix/resample):");
print_format(check_format);
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, check_format, ppClosestMatch));
} else if (games::gitadora::is_arena_model()) {
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
} else if (auto target_rate = hooks::audio::Resampler::resolve(pFormat)) {
// when resampling, the real device is opened at the target rate, so check whether the
// equivalent format at that rate is supported instead of the game's native rate.
WAVEFORMATEXTENSIBLE resample_storage = {};
hooks::audio::Resampler::make_device_format(pFormat, &resample_storage, *target_rate);
const auto resample_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
log_info("audio::wasapi", "... checking device format instead (after resample):");
print_format(resample_format);
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, resample_format, ppClosestMatch));
}
if (this->backend) { if (this->backend) {
HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch); HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch);
@@ -466,5 +650,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret)); log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
copy_wave_format(&hooks::audio::FORMAT, pFormat); copy_wave_format(&hooks::audio::FORMAT, pFormat);
copy_wave_format(&this->device_format, pFormat);
return ret; return ret;
} }
@@ -8,8 +8,11 @@
#include "hooks/audio/audio_private.h" #include "hooks/audio/audio_private.h"
#include "util/logging.h" #include "util/logging.h"
#include "audio_render_client.h" #include "downmix.h"
#include "resample.h"
#include "shared.h"
#include "audio_render_client.h"
// {1FBC8530-AF3E-4128-B418-115DE72F76B6} // {1FBC8530-AF3E-4128-B418-115DE72F76B6}
static const GUID IID_WrappedIAudioClient = { static const GUID IID_WrappedIAudioClient = {
0x1fbc8530, 0xaf3e, 0x4128, { 0xb4, 0x18, 0x11, 0x5d, 0xe7, 0x2f, 0x76, 0xb6 } 0x1fbc8530, 0xaf3e, 0x4128, { 0xb4, 0x18, 0x11, 0x5d, 0xe7, 0x2f, 0x76, 0xb6 }
@@ -92,5 +95,22 @@ struct WrappedIAudioClient : IAudioClient3 {
IAudioClient3 *const pReal3; IAudioClient3 *const pReal3;
AudioBackend *const backend; AudioBackend *const backend;
bool exclusive_mode = false; bool exclusive_mode = false;
// -wasapishared redirect state: when an exclusive request was redirected to shared mode, the
// engine converts the native format and the reported buffer size is clamped (see SharedRedirect).
hooks::audio::SharedRedirect shared;
int frame_size = 0; int frame_size = 0;
// the format the real device was opened with (after any downmix). used to scale the final
// output buffer for the volume boost.
WAVEFORMATEXTENSIBLE device_format = {};
// surround -> stereo downmix. the real device is opened as stereo while the game keeps
// writing multi-channel audio into a scratch buffer that we downmix in the render client.
hooks::audio::Downmix downmix;
// native-rate -> target-rate sample-rate conversion. the real device is opened at the target
// rate while the game keeps writing its native-rate audio into a scratch buffer that we
// resample in the render client.
hooks::audio::Resampler resample;
}; };
@@ -1,6 +1,13 @@
#include "audio_render_client.h" #include "audio_render_client.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include "audio_client.h" #include "audio_client.h"
#include "hooks/audio/audio.h"
#include "util.h"
#include "wasapi_private.h" #include "wasapi_private.h"
const char CLASS_NAME[] = "WrappedIAudioRenderClient"; const char CLASS_NAME[] = "WrappedIAudioRenderClient";
@@ -51,6 +58,33 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesR
return S_OK; return S_OK;
} }
// downmix + resample chained: the game writes its multi-channel native-rate audio into the
// downmix scratch, which is downmixed to stereo and then resampled on release. size the
// resampler's (stereo) input scratch now and hand the game the multi-channel downmix scratch.
if (this->client->downmix.enabled && this->client->resample.enabled) {
BYTE *resample_scratch = nullptr;
this->client->resample.get_buffer(NumFramesRequested, &resample_scratch);
CHECK_RESULT(this->client->downmix.get_scratch(NumFramesRequested, ppData));
// surround downmix: reserve the real (stereo) device buffer, but hand the game a
// multi-channel scratch buffer that we downmix on release
} else if (this->client->downmix.enabled) {
CHECK_RESULT(this->client->downmix.get_buffer(pReal, NumFramesRequested, ppData));
// resample: hand the game a native-rate scratch buffer that we convert on release. the real
// device buffer is acquired in ReleaseBuffer once the converted frame count is known.
} else if (this->client->resample.enabled) {
CHECK_RESULT(this->client->resample.get_buffer(NumFramesRequested, ppData));
// shared-mode redirect bridge: point the game at the FIFO tail it can always fill, decoupling
// its per-event writes from the shared engine's clock. the real device buffer is acquired in
// ReleaseBuffer and filled only as fast as the device frees space (see SharedRedirect::drain).
} else if (this->client->shared.bridge_enabled()) {
*ppData = this->client->shared.begin_write(NumFramesRequested);
return S_OK;
}
// call original // call original
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData); HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
@@ -75,14 +109,85 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFra
return S_OK; return S_OK;
} }
// fix for audio pop effect // downmix + resample chained: downmix the game's multi-channel scratch into the resampler's
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) { // stereo input scratch, then let the resampler convert and push it to the device. a silent
// buffer skips the downmix and feeds silence straight through.
// zero out = mute if (this->client->downmix.enabled && this->client->resample.enabled) {
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size); if ((dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
this->client->downmix.downmix_into(
this->buffers_to_mute--; this->client->resample.input_data(), NumFramesWritten);
}
return this->client->resample.flush(
pReal,
this->client->pReal,
NumFramesWritten,
dwFlags,
hooks::audio::VOLUME_BOOST);
} }
CHECK_RESULT(pReal->ReleaseBuffer(NumFramesWritten, dwFlags)); // resample: convert the game's native-rate scratch and push as many output frames as the
// device has room for, applying the volume boost to the converted output. handles acquiring
// and releasing the real device buffer itself.
if (this->client->resample.enabled) {
return this->client->resample.flush(
pReal,
this->client->pReal,
NumFramesWritten,
dwFlags,
hooks::audio::VOLUME_BOOST);
}
// shared-mode redirect bridge: queue the game's write and drain it to the device at the
// device's own pace, so a full-buffer write never overflows the shared buffer.
if (this->client->shared.bridge_enabled()) {
this->client->shared.commit_write(
NumFramesWritten, (dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) != 0);
return this->client->shared.drain(
pReal,
this->client->pReal,
this->client->device_format,
hooks::audio::VOLUME_BOOST);
}
// resolve the real device buffer for whichever path produced the audio
BYTE *device_buffer;
if (this->client->downmix.enabled) {
// downmix the game's multi-channel scratch into the real stereo buffer held since GetBuffer
this->client->downmix.write_device_buffer(NumFramesWritten, dwFlags);
device_buffer = this->client->downmix.current_buffer();
} else {
device_buffer = this->audio_buffer;
// mute the first few buffers to avoid a startup pop
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
this->buffers_to_mute--;
}
}
// boost the final output volume just before it reaches the device, layout-agnostic
if (hooks::audio::VOLUME_BOOST != 1.0f
&& device_buffer != nullptr
&& (dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
static std::once_flag boost_printed;
std::call_once(boost_printed, []() {
log_info("audio::wasapi", "volume boost active: gain={}", hooks::audio::VOLUME_BOOST);
});
apply_gain(device_buffer, NumFramesWritten, this->client->device_format,
hooks::audio::VOLUME_BOOST);
}
HRESULT ret = pReal->ReleaseBuffer(NumFramesWritten, dwFlags);
if (this->client->downmix.enabled) {
this->client->downmix.buffer_released();
}
if (FAILED(ret)) {
PRINT_FAILED_RESULT(CLASS_NAME, __func__, ret);
}
return ret;
} }
@@ -0,0 +1,264 @@
#include "downmix.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <audioclient.h>
#include <ks.h>
#include <ksmedia.h>
#include "util/logging.h"
#include "util.h"
namespace hooks::audio {
namespace {
constexpr float ATT_3DB = 0.70710678f;
// speakers routed to the left/right output; anything else (center) feeds both sides
constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT
| SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT;
constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT
| SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT;
// the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats
DWORD read_channel_mask(const WAVEFORMATEX *fmt) {
if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
&& fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask;
}
return 0;
}
// call visit(channel_index, speaker_bit) for each present speaker, in channel order
template <typename F>
void for_each_speaker(DWORD mask, int channels, F &&visit) {
int channel = 0;
for (int bit = 0; bit < 18 && channel < channels; bit++) {
const DWORD speaker = 1u << bit;
if (mask & speaker) {
visit(channel++, speaker);
}
}
}
}
void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
DownmixAlgorithm algorithm) {
this->enabled = true;
this->algorithm = algorithm;
this->bytes_per_sample = game_format->wBitsPerSample / 8;
this->game_frame_size = game_format->nChannels * this->bytes_per_sample;
this->is_float = is_ieee_float(game_format);
// supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence
const bool supported = this->is_float
? this->bytes_per_sample == 4
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
if (!supported) {
log_fatal(
"audio::downmix",
"unsupported sample format ({}-bit {}), downmix will output silence",
game_format->wBitsPerSample, this->is_float ? "float" : "int");
}
this->left_mix.clear();
this->right_mix.clear();
this->build_layout_mix(game_format);
make_stereo_format(game_format, stereo_out);
}
void Downmix::make_stereo_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *stereo_out) {
const int bytes_per_sample = game_format->wBitsPerSample / 8;
memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE));
stereo_out->Format.nChannels = 2;
stereo_out->Format.nBlockAlign = 2 * bytes_per_sample;
stereo_out->Format.nAvgBytesPerSec =
game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign;
stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
}
HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
// the smaller stereo buffer can end up unaligned for the device when the game sized the
// duration for its larger multi-channel format; the helper recovers from that.
return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags,
buffer_duration, periodicity, device_format, session_guid);
}
void Downmix::add_channel(int channel, DWORD speaker, float gain) {
if (speaker & LEFT_SPEAKERS) {
this->left_mix.push_back({ channel, gain });
} else if (speaker & RIGHT_SPEAKERS) {
this->right_mix.push_back({ channel, gain });
} else { // center: feed both sides
this->left_mix.push_back({ channel, gain });
this->right_mix.push_back({ channel, gain });
}
}
// AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped
void Downmix::build_ac4_mix(DWORD mask, int channels) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker == SPEAKER_LOW_FREQUENCY) {
return;
}
const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB);
});
}
// keep only the channels in `keep` (front/rear/side), each at unity gain
void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker & keep) {
this->add_channel(ch, speaker, 1.0f);
}
});
}
// keep every channel (LFE dropped), then average each side so its gains sum to unity
void Downmix::build_normalize_mix(DWORD mask, int channels) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker != SPEAKER_LOW_FREQUENCY) {
this->add_channel(ch, speaker, 1.0f);
}
});
for (auto *mix : { &this->left_mix, &this->right_mix }) {
if (!mix->empty()) {
const float gain = 1.0f / mix->size();
for (auto &c : *mix) {
c.gain = gain;
}
}
}
}
// fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right)
void Downmix::build_pairs_mix(int channels, float gain) {
for (int ch = 0; ch < channels; ch++) {
(((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain });
}
}
void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) {
const int channels = game_format->nChannels;
const DWORD mask = read_channel_mask(game_format);
// without a mask the layout is unknown: extract/normalize have nothing to act on, so all
// algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB)
if (mask == 0) {
this->build_pairs_mix(channels,
this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f);
return;
}
switch (this->algorithm) {
case DownmixAlgorithm::FrontOnly:
this->build_extract_mix(mask, channels,
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
break;
case DownmixAlgorithm::RearOnly:
this->build_extract_mix(mask, channels,
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER);
break;
case DownmixAlgorithm::SideOnly:
this->build_extract_mix(mask, channels,
SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT);
break;
case DownmixAlgorithm::Normalize:
this->build_normalize_mix(mask, channels);
break;
case DownmixAlgorithm::AC4:
this->build_ac4_mix(mask, channels);
break;
}
}
void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const {
const int bps = this->bytes_per_sample;
const int src_stride = this->game_frame_size;
const int dst_stride = 2 * bps;
if (dst == nullptr || src == nullptr || bps <= 0) {
return;
}
// sum each speaker's source channels into the matching stereo output
for (UINT32 i = 0; i < frames; i++) {
const BYTE *in = src + (size_t) i * src_stride;
BYTE *out = dst + (size_t) i * dst_stride;
float left = 0.0f;
float right = 0.0f;
for (const auto &c : this->left_mix) {
left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
}
for (const auto &c : this->right_mix) {
right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
}
write_sample(out, bps, this->is_float, left);
write_sample(out + bps, bps, this->is_float, right);
}
}
HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) {
this->scratch.resize(needed);
}
HRESULT ret = real->GetBuffer(frames, &this->device_buffer);
if (FAILED(ret)) {
this->device_buffer = nullptr;
return ret;
}
*ppData = this->scratch.data();
return S_OK;
}
HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) {
this->scratch.resize(needed);
}
*ppData = this->scratch.data();
return S_OK;
}
void Downmix::downmix_into(BYTE *dst, UINT32 frames) const {
this->process(dst, this->scratch.data(), frames);
}
void Downmix::write_device_buffer(UINT32 frames, DWORD flags) {
const int bps = this->bytes_per_sample;
const int dst_stride = 2 * bps;
if (this->device_buffer == nullptr || frames == 0 || bps <= 0) {
return;
}
// mute the first few buffers to avoid a pop on stream start
if (this->buffers_to_mute > 0) {
memset(this->device_buffer, 0, (size_t) frames * dst_stride);
this->buffers_to_mute--;
} else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
this->process(this->device_buffer, this->scratch.data(), frames);
}
}
}
@@ -0,0 +1,150 @@
#pragma once
#include <optional>
#include <vector>
#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
#include "hooks/audio/audio.h"
struct IAudioClient;
struct IAudioRenderClient;
namespace hooks::audio {
// Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the
// game keeps writing its native multi-channel audio into a scratch buffer; on release that
// buffer is mixed down into the two front channels.
//
// The mix is derived from the source format's speaker mask according to the selected
// DownmixAlgorithm:
// FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side
// AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right
// pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped
// Normalize - every channel folded in (center to both sides) with each output side averaged
// so its channels are equally loud, LFE dropped
struct Downmix {
// a source channel routed into one output speaker at the given gain
struct Contribution {
int channel;
float gain;
};
// map an option value (front/rear/side/ac4/normalize) to its algorithm.
static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) {
if (_stricmp(value, "front") == 0) {
return DownmixAlgorithm::FrontOnly;
} else if (_stricmp(value, "rear") == 0) {
return DownmixAlgorithm::RearOnly;
} else if (_stricmp(value, "side") == 0) {
return DownmixAlgorithm::SideOnly;
} else if (_stricmp(value, "ac4") == 0) {
return DownmixAlgorithm::AC4;
} else if (_stricmp(value, "normalize") == 0) {
return DownmixAlgorithm::Normalize;
}
return std::nullopt;
}
// human-readable name of an algorithm, for logging.
static const char *algorithm_name(DownmixAlgorithm algorithm) {
switch (algorithm) {
case DownmixAlgorithm::FrontOnly: return "front";
case DownmixAlgorithm::RearOnly: return "rear";
case DownmixAlgorithm::SideOnly: return "side";
case DownmixAlgorithm::AC4: return "ac4";
case DownmixAlgorithm::Normalize: return "normalize";
default: return "unknown";
}
}
// whether the downmix is active for the current stream
bool enabled = false;
// algorithm used to fold the multi-channel audio into stereo
DownmixAlgorithm algorithm = DownmixAlgorithm::AC4;
// size in bytes of one frame of the game's multi-channel format
int game_frame_size = 0;
// size in bytes of a single sample (per channel)
int bytes_per_sample = 0;
// whether samples are IEEE floating point rather than integer PCM
bool is_float = false;
// enable the downmix for the given game format and fill stereo_out with the equivalent
// stereo format to open the real device with.
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
DownmixAlgorithm algorithm);
// build the stereo format equivalent to game_format (same sample rate and bit depth).
static void make_stereo_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *stereo_out);
// initialize the real device with the stereo format. downmixing reduces the channel count,
// shrinking the buffer's byte size, so the duration the game sized for its multi-channel
// format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
// this performs the standard WASAPI realignment and retries.
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid);
// mix `frames` frames of multi-channel `src` down into stereo `dst`.
void process(BYTE *dst, const BYTE *src, UINT32 frames) const;
// grab the real stereo device buffer and hand the game the scratch buffer to write into.
HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData);
// size the scratch and hand it to the game without acquiring a device buffer. used when a
// later stage (the resampler) owns the device interaction.
HRESULT get_scratch(UINT32 frames, BYTE **ppData);
// downmix the scratch the game wrote into the caller's stereo buffer, without touching the
// device. used to feed the resampler when the two stages are chained.
void downmix_into(BYTE *dst, UINT32 frames) const;
// mix the scratch buffer into the stereo device buffer held since get_buffer. the caller
// owns releasing the device buffer afterwards (see current_buffer / buffer_released).
void write_device_buffer(UINT32 frames, DWORD flags);
// the real device buffer currently held, or null.
BYTE *current_buffer() const { return this->device_buffer; }
// forget the held device buffer once the caller has released it.
void buffer_released() { this->device_buffer = nullptr; }
private:
// build the mix from the source speaker layout for the selected algorithm
void build_layout_mix(const WAVEFORMATEX *game_format);
// per-algorithm builders, each filling left_mix / right_mix from the speaker mask
void build_ac4_mix(DWORD mask, int channels);
void build_extract_mix(DWORD mask, int channels, DWORD keep);
void build_normalize_mix(DWORD mask, int channels);
// fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain`
void build_pairs_mix(int channels, float gain);
// append one source channel to the output side(s) matching its speaker, at `gain`
void add_channel(int channel, DWORD speaker, float gain);
// source channels summed into each output speaker
std::vector<Contribution> left_mix;
std::vector<Contribution> right_mix;
// buffer the game writes its multi-channel audio into between get/release
std::vector<BYTE> scratch;
// the real stereo device buffer currently held, or null
BYTE *device_buffer = nullptr;
// leading buffers to silence to avoid a pop on stream start
int buffers_to_mute = 16;
};
}
@@ -73,11 +73,7 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
// verbose output // verbose output
log_info("audio::wasapi", "IAudioClient::Initialize hook hit"); log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode)); print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
print_format(pFormat);
CHECK_RESULT(this->backend->on_initialize( CHECK_RESULT(this->backend->on_initialize(
&ShareMode, &ShareMode,
@@ -0,0 +1,437 @@
#include "resample.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <audioclient.h>
#include "util/logging.h"
#include "util.h"
namespace hooks::audio {
namespace {
constexpr double PI = 3.14159265358979323846;
// normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in
inline double sinc(double x) {
if (x == 0.0) {
return 1.0;
}
const double px = PI * x;
return std::sin(px) / px;
}
// Blackman window across the kernel radius; zero at +/- radius
inline double blackman(double x, double radius) {
const double n = (x + radius) / (2.0 * radius);
if (n <= 0.0 || n >= 1.0) {
return 0.0;
}
return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n);
}
}
std::optional<uint32_t> Resampler::resolve(const WAVEFORMATEX *game_format) {
if (game_format == nullptr || !RESAMPLE_RATE.has_value()) {
return std::nullopt;
}
if (game_format->nSamplesPerSec == 0
|| game_format->nSamplesPerSec == RESAMPLE_RATE.value()) {
return std::nullopt;
}
return RESAMPLE_RATE;
}
void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
uint32_t target_rate) {
this->enabled = true;
this->channels = game_format->nChannels;
this->bytes_per_sample = game_format->wBitsPerSample / 8;
this->game_frame_size = this->channels * this->bytes_per_sample;
this->is_float = is_ieee_float(game_format);
const bool supported = this->is_float
? this->bytes_per_sample == 4
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
if (!supported) {
log_fatal(
"audio::resample",
"unsupported sample format ({}-bit {}) for -resample",
game_format->wBitsPerSample, this->is_float ? "float" : "int");
}
this->src_rate = game_format->nSamplesPerSec;
this->dst_rate = target_rate;
// anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating
this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate);
this->half_taps = 16;
// precompute the windowed-sinc kernel now that cutoff is known
this->build_kernel();
// prime the queue with half a window of silence so the first outputs have left history
this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f);
this->in_pos = this->half_taps;
this->make_device_format(game_format, device_out, target_rate);
}
void Resampler::make_device_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) {
const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize;
memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE));
memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE)));
device_out->Format.nSamplesPerSec = target_rate;
device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign;
}
HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode,
DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
// the resampler bypasses the OS mixer and talks to the device directly, so it only makes
// sense (and only works) for exclusive streams. shared streams are already resampled by
// the Windows audio engine, so refuse loudly rather than silently doing nothing.
if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) {
log_fatal("audio::resample",
"-resample requires WASAPI exclusive mode, but this stream is shared "
"(Windows already resamples shared streams)");
}
// record the pacing model. event-driven streams fill the whole device buffer each period
// (produce_exact); timer-driven streams poll padding and write variable partial chunks, so
// they drain the pending output to the device's free space each call (flush_timer).
this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0;
return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags,
buffer_duration, periodicity, device_format, session_guid);
}
UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const {
if (this->dst_rate == 0) {
return device_frames;
}
// round down so the game never believes it has more room than the device can hold
return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate);
}
UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const {
if (this->dst_rate == 0) {
return device_padding;
}
// round up so the reported free space stays conservative
return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate);
}
HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) {
this->scratch.resize(needed);
}
*ppData = this->scratch.data();
return S_OK;
}
void Resampler::enqueue_input(UINT32 frames, bool silent) {
const int bps = this->bytes_per_sample;
const int ch = this->channels;
const size_t base = this->in_queue.size();
this->in_queue.resize(base + (size_t) frames * ch);
if (silent || bps <= 0 || ch <= 0) {
std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f);
return;
}
const BYTE *src = this->scratch.data();
for (UINT32 f = 0; f < frames; f++) {
for (int c = 0; c < ch; c++) {
const size_t s = (size_t) f * ch + c;
this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float);
}
}
}
void Resampler::build_kernel() {
const int taps = 2 * this->half_taps;
const int phases = this->kernel_phases;
const double cut = this->cutoff;
const double radius = (double) this->half_taps;
// one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely
this->kernel_table.resize((size_t) (phases + 1) * taps);
for (int p = 0; p <= phases; p++) {
const double frac = (double) p / (double) phases;
for (int k = 0; k < taps; k++) {
// tap k maps to input offset t = k - (half_taps - 1), matching emit_frame
const double x = frac - (double) (k - (this->half_taps - 1));
this->kernel_table[(size_t) p * taps + k] =
(float) (cut * sinc(cut * x) * blackman(x, radius));
}
}
}
void Resampler::emit_frame() {
const int ch = this->channels;
const int radius = this->half_taps;
const int taps = 2 * radius;
const long avail = (long) (this->in_queue.size() / ch);
const long center = (long) std::floor(this->in_pos);
// pick the two kernel rows bracketing this fractional position and the blend between them
const double frac = this->in_pos - (double) center;
const double fp = frac * (double) this->kernel_phases;
const int p0 = (int) fp;
const float blend = (float) (fp - (double) p0);
const float *row0 = &this->kernel_table[(size_t) p0 * taps];
const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps];
// base input index for tap 0 (t = -(radius - 1))
const long base = center - (radius - 1);
for (int c = 0; c < ch; c++) {
double acc = 0.0;
for (int k = 0; k < taps; k++) {
const long idx = base + k;
if (idx < 0 || idx >= avail) {
continue;
}
const float w = row0[k] + blend * (row1[k] - row0[k]);
acc += (double) this->in_queue[(size_t) idx * ch + c] * w;
}
this->out_float.push_back((float) acc);
}
}
void Resampler::drop_consumed() {
const int ch = this->channels;
const long drop = (long) std::floor(this->in_pos) - this->half_taps;
if (drop > 0) {
const size_t drop_samples = (size_t) drop * ch;
if (drop_samples <= this->in_queue.size()) {
this->in_queue.erase(this->in_queue.begin(),
this->in_queue.begin() + drop_samples);
this->in_pos -= drop;
}
}
}
UINT32 Resampler::produce_exact(UINT32 out_frames) {
const int ch = this->channels;
this->out_float.clear();
if (ch <= 0 || out_frames == 0) {
return 0;
}
this->out_float.reserve((size_t) out_frames * ch);
// resample ratio. drive it from the buffer size actually advertised to the game rather
// than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game
// frames, so the game only ever delivers that many input frames per device period.
// consuming at the nominal ratio would eat slightly more input than arrives on any device
// where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly
// draining the queue until it underruns to permanent silence. using the advertised integer
// ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3%
// and inaudible, and it collapses to the exact ratio when the division is integer (160 ->
// 147 stays 147/160 = 44100/48000).
const double step = (double) this->frames_device_to_game(this->device_buffer_frames)
/ (double) this->device_buffer_frames;
// input frames the block will touch: from in_pos through the right edge of the sinc kernel
// at the final output sample. if the queue is short of this, the kernel tail reads past the
// end and distorts every buffer, so buffer one extra block of input before the first output
// (emitting silence without consuming) to build a cushion the kernel can always reach into.
const long avail = (long) (this->in_queue.size() / ch);
const long need = (long) std::ceil(this->in_pos + step * (double) out_frames)
+ this->half_taps;
if (this->priming) {
if (avail < need + (long) out_frames) {
this->out_float.assign((size_t) out_frames * ch, 0.0f);
return out_frames;
}
this->priming = false;
}
for (UINT32 o = 0; o < out_frames; o++) {
this->emit_frame();
this->in_pos += step;
}
this->drop_consumed();
return out_frames;
}
UINT32 Resampler::produce_variable() {
const int ch = this->channels;
if (ch <= 0) {
return 0;
}
// input frames consumed per output frame. timer-driven streams write variable partial
// chunks, so produce however many output frames the currently queued input can fully
// support and leave the rest for the next call; this keeps input and output balanced at
// the exact src/dst ratio over time without depending on the device buffer size.
const double step = (double) this->src_rate / (double) this->dst_rate;
const long avail = (long) (this->in_queue.size() / ch);
// emit only while the sinc kernel's right edge stays within the queued input. the kernel
// reaches from in_pos out to half_taps frames ahead, so stop once that would read past the
// end; the remaining input becomes the next block's lookahead.
UINT32 produced = 0;
while ((long) std::ceil(this->in_pos) + this->half_taps < avail) {
this->emit_frame();
this->in_pos += step;
produced++;
}
this->drop_consumed();
return produced;
}
void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const {
const int bps = this->bytes_per_sample;
const int ch = this->channels;
const size_t count = (size_t) frames * ch;
for (size_t i = 0; i < count; i++) {
write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain);
}
}
HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames,
DWORD flags, float boost) {
if (!this->enabled) {
return S_OK;
}
// cache the device buffer size once
if (this->device_buffer_frames == 0) {
client->GetBufferSize(&this->device_buffer_frames);
}
if (this->device_buffer_frames == 0) {
return S_OK;
}
const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0;
this->enqueue_input(frames, silent);
// confirm once that conversion actually started producing output
static std::once_flag active_printed;
std::call_once(active_printed, [this]() {
log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})",
this->src_rate, this->dst_rate, this->channels,
this->event_driven ? "event-driven" : "timer-driven");
});
// the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer
// path, so log it once for parity with that path's "volume boost active" line.
if (boost != 1.0f) {
static std::once_flag boost_printed;
std::call_once(boost_printed, [boost]() {
log_info("audio::resample", "volume boost active (resample): gain={}", boost);
});
}
return this->event_driven
? this->flush_event(real, boost)
: this->flush_timer(real, client, boost);
}
HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) {
// event-driven exclusive streams must hand the device a full buffer every period and may
// not push partial counts. resample the whole input block into exactly the device buffer
// size.
const UINT32 produced = this->produce_exact(this->device_buffer_frames);
if (produced == 0) {
return S_OK;
}
BYTE *dev = nullptr;
HRESULT ret = real->GetBuffer(produced, &dev);
if (FAILED(ret) || dev == nullptr) {
return ret;
}
// mute the first few buffers to avoid a pop on stream start
float gain = boost;
if (this->buffers_to_mute > 0) {
gain = 0.0f;
this->buffers_to_mute--;
}
this->write_output(dev, produced, gain);
return real->ReleaseBuffer(produced, 0);
}
HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) {
// convert everything currently queued into the pending output FIFO (out_float). timer-
// driven games write variable partial chunks, so produce only what the queued input can
// fully support and keep the remainder for the next call.
this->produce_variable();
const int ch = this->channels;
if (ch <= 0) {
return S_OK;
}
const UINT32 pending = (UINT32) (this->out_float.size() / ch);
if (pending == 0) {
return S_OK;
}
// push as many frames as the device currently has free, keeping the rest queued for the
// next call. timer-driven games poll padding and write whenever there is room, so matching
// the device's free space here avoids overflowing the ring while staying device-paced.
UINT32 padding = 0;
if (FAILED(client->GetCurrentPadding(&padding))) {
return S_OK;
}
const UINT32 device_free = this->device_buffer_frames > padding
? this->device_buffer_frames - padding
: 0;
if (device_free == 0) {
return S_OK;
}
const UINT32 to_write = std::min(pending, device_free);
BYTE *dev = nullptr;
HRESULT ret = real->GetBuffer(to_write, &dev);
if (FAILED(ret) || dev == nullptr) {
return ret;
}
// mute the first few buffers to avoid a pop on stream start
float gain = boost;
if (this->buffers_to_mute > 0) {
gain = 0.0f;
this->buffers_to_mute--;
}
this->write_output(dev, to_write, gain);
ret = real->ReleaseBuffer(to_write, 0);
// drop the frames just written from the front of the pending FIFO
this->out_float.erase(this->out_float.begin(),
this->out_float.begin() + (size_t) to_write * ch);
return ret;
}
}
@@ -0,0 +1,149 @@
#pragma once
#include <cstdint>
#include <optional>
#include <vector>
#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
#include "hooks/audio/audio.h"
struct IAudioClient;
struct IAudioRenderClient;
namespace hooks::audio {
// Streaming sample-rate converter for the WASAPI render path. The real device is opened at the
// target rate while the game keeps writing its native-rate audio into a scratch buffer; on
// release that buffer is converted with a windowed-sinc kernel and pushed to the device.
// Channel count and sample format are preserved; only the sample rate changes.
//
// Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful:
// a fractional read position and a window of input history carry across ReleaseBuffer calls,
// and the device buffer is only filled up to the space the device currently has free.
struct Resampler {
// whether the resampler is active for the current stream
bool enabled = false;
// whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven
// streams instead poll padding and write variable partial chunks, so they drain the
// pending output to the device's free space rather than pushing a full buffer per period.
bool event_driven = true;
// decide whether the stream should be resampled and to which rate. returns the target rate
// when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt.
static std::optional<uint32_t> resolve(const WAVEFORMATEX *game_format);
// enable resampling for game_format and fill device_out with the equivalent format at the
// target rate to open the real device with.
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
uint32_t target_rate);
// build the device format equivalent to game_format at target_rate (same channels/depth).
static void make_device_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate);
// initialize the real device at the target rate, performing the standard WASAPI buffer
// realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED.
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid);
// translate a device-rate frame count to the equivalent game-rate count, so the buffer-size
// and padding values reported to the game stay paced at the game's native rate.
UINT32 frames_device_to_game(UINT32 device_frames) const;
UINT32 padding_device_to_game(UINT32 device_padding) const;
// hand the game a scratch buffer sized for `frames` of its native format to write into.
HRESULT get_buffer(UINT32 frames, BYTE **ppData);
// pointer to the input scratch (sized by get_buffer). when chained after the downmix, the
// downmix writes its stereo output here for the resampler to consume on the next flush.
BYTE *input_data() { return this->scratch.data(); }
// convert the `frames` the game wrote and push output to the real render client. `boost`
// is applied to the converted output. event-driven streams fill exactly one device buffer
// per period; timer-driven streams push as many converted frames as the device has free.
HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags,
float boost);
private:
// append `frames` of the scratch buffer (native format), or silence, to the input queue
void enqueue_input(UINT32 frames, bool silent);
// event-driven path: produce exactly one full device buffer and push it.
HRESULT flush_event(IAudioRenderClient *real, float boost);
// timer-driven path: convert all queued input into the pending output FIFO, then push as
// many frames as the device currently has free, keeping the remainder for the next call.
HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost);
// produce exactly out_frames output frames using the fixed src/dst ratio. event-driven
// exclusive streams must fill the whole device buffer every period; a small input cushion
// is buffered first (see priming) so the sinc kernel always has lookahead.
UINT32 produce_exact(UINT32 out_frames);
// convert all input the kernel can fully support into the pending output FIFO (out_float),
// appending without clearing. returns the number of frames produced. used by the
// timer-driven path where output is drained to the device in device-paced chunks.
UINT32 produce_variable();
// convolve the windowed-sinc kernel at the current in_pos and append the resulting frame
// (one sample per channel) to out_float
void emit_frame();
// precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so
// emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too
// expensive to run per sample on the audio callback thread and causes underrun crackle).
void build_kernel();
// drop input frames that in_pos has advanced past, keeping a window of history for the
// next block's left context
void drop_consumed();
// convert the first `frames` of out_float to the device format, scaled by `gain`
void write_output(BYTE *dst, UINT32 frames, float gain) const;
// sample format of the stream
int channels = 0;
int bytes_per_sample = 0;
bool is_float = false;
int game_frame_size = 0;
uint32_t src_rate = 0;
uint32_t dst_rate = 0;
// sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius
double cutoff = 1.0;
int half_taps = 16;
// precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the
// fractional sample position (linearly interpolated between adjacent rows in emit_frame)
std::vector<float> kernel_table;
int kernel_phases = 1024;
// interleaved float input queue and the fractional read position within it (in frames)
std::vector<float> in_queue;
double in_pos = 0.0;
// emit silence until a full block of input lookahead has accumulated, so the sinc kernel
// never reads past the end of the queue (which would distort the tail of every buffer)
bool priming = true;
// interleaved float scratch for produced output
std::vector<float> out_float;
// buffer the game writes its native-rate audio into between get_buffer / flush
std::vector<BYTE> scratch;
// cached device buffer size (frames); a full buffer is produced every period
UINT32 device_buffer_frames = 0;
// leading buffers to silence to avoid a pop on stream start
int buffers_to_mute = 16;
};
}
@@ -0,0 +1,187 @@
#include "shared.h"
#include <algorithm>
#include <audioclient.h>
#include "hooks/audio/audio.h"
#include "util/logging.h"
#include "util.h"
#include "defs.h"
namespace hooks::audio {
// whether the engine's PCM converter can handle this format. PCM / float only; non-PCM
// bitstream (AC-3 / DTS passthrough) must be left alone.
static bool is_pcm_or_float(const WAVEFORMATEX *format) {
if (format == nullptr) {
return false;
}
switch (format->wFormatTag) {
case WAVE_FORMAT_PCM:
case WAVE_FORMAT_IEEE_FLOAT:
return true;
case WAVE_FORMAT_EXTENSIBLE: {
// SubFormat is only valid when the extra-bytes block is large enough
if (format->cbSize < sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
return false;
}
const auto *ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format);
return ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM
|| ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
}
default:
return false;
}
}
bool SharedRedirect::wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format) {
// only redirect PCM / float exclusive streams: the engine converter (AUTOCONVERTPCM) can
// handle those, but non-PCM bitstream (AC-3 / DTS passthrough) would fail in shared mode,
// so leave it in exclusive untouched.
return hooks::audio::WASAPI_COMPATIBILITY_MODE
&& share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE
&& is_pcm_or_float(format);
}
void SharedRedirect::apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags,
REFERENCE_TIME *periodicity) {
// shared mode requires periodicity == 0; AUTOCONVERTPCM lets the engine accept the game's
// native format (else shared Initialize returns AUDCLNT_E_UNSUPPORTED_FORMAT).
log_info("audio::wasapi", "redirecting exclusive WASAPI to shared mode");
*share_mode = AUDCLNT_SHAREMODE_SHARED;
*periodicity = 0;
*stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
this->redirected_from_exclusive = true;
}
UINT32 SharedRedirect::clamp_buffer_size(IAudioClient *real, uint32_t sample_rate,
UINT32 device_frames) {
if (!this->redirected_from_exclusive || real == nullptr || sample_rate == 0 || device_frames == 0) {
this->reported_frames = device_frames;
return device_frames;
}
// GetDevicePeriod returns REFERENCE_TIME units (100 ns), 10^7 per second, so
// period_frames = period * sample_rate / 10^7.
REFERENCE_TIME period = 0;
if (SUCCEEDED(real->GetDevicePeriod(&period, nullptr)) && period > 0) {
const UINT32 period_frames = (UINT32) ((period * sample_rate) / 10000000);
if (period_frames > 0 && period_frames < device_frames) {
this->reported_frames = period_frames;
return period_frames;
}
}
this->reported_frames = device_frames;
return device_frames;
}
void SharedRedirect::enable_bridge(int frame_bytes) {
if (!this->redirected_from_exclusive || frame_bytes <= 0) {
return;
}
this->frame_bytes = frame_bytes;
this->device_buffer_frames = 0;
this->fifo.clear();
log_info("audio::wasapi", "shared-mode buffer bridge enabled (frame size {} bytes)",
frame_bytes);
}
BYTE *SharedRedirect::begin_write(UINT32 frames) {
// reserve space at the FIFO tail and let the game write straight into it - no scratch copy.
this->pending_write_offset = this->fifo.size();
this->fifo.resize(this->pending_write_offset + (size_t) frames * this->frame_bytes);
return this->fifo.data() + this->pending_write_offset;
}
void SharedRedirect::commit_write(UINT32 frames, bool silent) {
// trim the tail reservation to the frames actually written; zero it in place if silent.
const size_t end = this->pending_write_offset + (size_t) frames * this->frame_bytes;
if (silent) {
std::fill(this->fifo.begin() + this->pending_write_offset,
this->fifo.begin() + end, (BYTE) 0);
}
this->fifo.resize(end);
}
UINT32 SharedRedirect::pending_frames() const {
if (this->frame_bytes <= 0) {
return 0;
}
return (UINT32) (this->fifo.size() / this->frame_bytes);
}
UINT32 SharedRedirect::virtual_padding() const {
const UINT32 pending = this->pending_frames();
return this->reported_frames > 0 ? std::min(pending, this->reported_frames) : pending;
}
HRESULT SharedRedirect::drain(IAudioRenderClient *real, IAudioClient *client,
const WAVEFORMATEXTENSIBLE &device_format, float boost) {
if (!this->bridge_enabled()) {
return S_OK;
}
// cache the real device buffer size once; it is fixed for the life of the stream.
if (this->device_buffer_frames == 0) {
if (FAILED(client->GetBufferSize(&this->device_buffer_frames))
|| this->device_buffer_frames == 0) {
return S_OK;
}
}
const UINT32 pending = this->pending_frames();
if (pending == 0) {
return S_OK;
}
// push only as many frames as the device currently has free, keeping the rest queued. this
// self-paces to the engine's real consumption so a full-buffer write never overflows.
UINT32 padding = 0;
if (FAILED(client->GetCurrentPadding(&padding))) {
return S_OK;
}
const UINT32 device_free = this->device_buffer_frames > padding
? this->device_buffer_frames - padding
: 0;
if (device_free == 0) {
return S_OK;
}
const UINT32 to_write = std::min(pending, device_free);
BYTE *dev = nullptr;
HRESULT ret = real->GetBuffer(to_write, &dev);
if (FAILED(ret) || dev == nullptr) {
return ret;
}
const size_t bytes = (size_t) to_write * this->frame_bytes;
std::copy(this->fifo.begin(), this->fifo.begin() + bytes, dev);
// mute the first few buffers to avoid a startup pop, then apply the volume boost.
if (this->buffers_to_mute > 0) {
std::fill(dev, dev + bytes, (BYTE) 0);
this->buffers_to_mute--;
} else if (boost != 1.0f) {
apply_gain(dev, to_write, device_format, boost);
}
ret = real->ReleaseBuffer(to_write, 0);
// drop the frames just handed to the device from the front of the FIFO.
this->fifo.erase(this->fifo.begin(), this->fifo.begin() + bytes);
return ret;
}
}
@@ -0,0 +1,83 @@
#pragma once
#include <cstdint>
#include <vector>
#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
struct IAudioRenderClient;
namespace hooks::audio {
// The -wasapishared option redirects an exclusive WASAPI stream to shared mode, so other apps
// can play sound and devices that can't open the exclusive format still work, at the cost of
// some latency. Only PCM / float is converted; bitstream (AC-3 / DTS) is left alone.
struct SharedRedirect {
// true once apply() has redirected an exclusive request. gates the buffer clamp; stays false
// for a natively-shared stream (it paces itself, so must not be clamped).
bool redirected_from_exclusive = false;
// whether an exclusive-mode request should be redirected, given the -wasapishared option.
// only PCM / float is eligible; bitstream (AC-3 / DTS) is left in exclusive mode.
static bool wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format);
// redirect an exclusive request to shared mode. caller must have checked wants() first.
void apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, REFERENCE_TIME *periodicity);
// clamp a reported buffer size to one device period. the FIFO bridge below is what prevents
// the overflow; this just keeps the game's per-event writes small so the bridge adds minimal
// latency. caches the chosen value for virtual_padding. a no-op unless redirected.
UINT32 clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, UINT32 device_frames);
// FIFO bridge: the redirected game writes a whole reported buffer per event paced by its own
// callback, not the shared engine clock, so a full-buffer write can intermittently exceed the
// double-buffered shared free space (AUDCLNT_E_BUFFER_TOO_LARGE). The game instead writes
// directly into a FIFO that is drained to the device only as fast as it frees space - the
// same free-space-clamped approach the timer-driven resampler uses.
// arm the bridge once the redirected stream is initialized. frame_bytes is one frame's size
// in the game's (== device, via AUTOCONVERTPCM) format.
void enable_bridge(int frame_bytes);
// whether the FIFO bridge is active (a redirect was applied and armed).
bool bridge_enabled() const { return this->frame_bytes > 0; }
// reserve `frames` at the FIFO tail and hand the game a pointer into it to write in place.
// must be paired with commit_write, which trims the reservation to the frames written.
BYTE *begin_write(UINT32 frames);
// trim the reservation from begin_write to the `frames` actually written (zeroing if silent).
void commit_write(UINT32 frames, bool silent);
// padding to report to a game that polls GetCurrentPadding while the bridge is active: the
// FIFO fill level, capped to the reported buffer size so the game's free-space calculation
// (reported_buffer - padding) reflects room in the virtual buffer rather than the device's.
UINT32 virtual_padding() const;
// push as many queued frames as the real device has free, applying `boost`, keeping the rest
// for the next call. `real` is the wrapped render client's underlying interface; `client` is
// the underlying audio client used to query the device's free space.
HRESULT drain(IAudioRenderClient *real, IAudioClient *client,
const WAVEFORMATEXTENSIBLE &device_format, float boost);
private:
// frames currently queued in the FIFO and not yet handed to the device.
UINT32 pending_frames() const;
// FIFO bridge state (see enable_bridge). fifo holds audio queued for the device in the
// game's interleaved frame format; the game writes new frames directly into its tail between
// begin_write and commit_write. frame_bytes > 0 doubles as the "bridge armed" flag (see
// bridge_enabled). pending_write_offset marks the tail reservation handed to begin_write.
int frame_bytes = 0;
UINT32 device_buffer_frames = 0;
UINT32 reported_frames = 0;
int buffers_to_mute = 4;
size_t pending_write_offset = 0;
std::vector<BYTE> fifo;
};
}
@@ -2,10 +2,64 @@
#include <audioclient.h> #include <audioclient.h>
#include "hooks/audio/util.h"
#include "util/flags_helper.h" #include "util/flags_helper.h"
#include "util/logging.h"
#include "defs.h" #include "defs.h"
void apply_gain(BYTE *buffer, UINT32 frames, const WAVEFORMATEXTENSIBLE &fmt, float gain) {
const WAVEFORMATEX &f = fmt.Format;
const size_t samples = (size_t) frames * f.nChannels;
bool is_float = is_ieee_float(&f);
if (is_float && f.wBitsPerSample == 32) {
auto p = reinterpret_cast<float *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f);
}
return;
}
switch (f.wBitsPerSample) {
case 16: {
auto p = reinterpret_cast<int16_t *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = (int16_t) std::clamp((int) std::lround(p[i] * gain), -32768, 32767);
}
break;
}
case 24: {
// packed 24-bit little-endian
for (size_t i = 0; i < samples; i++) {
BYTE *s = buffer + i * 3;
int32_t v = s[0] | (s[1] << 8) | (s[2] << 16);
if (v & 0x800000) {
v |= ~0xFFFFFF; // sign extend
}
int64_t scaled = std::clamp<int64_t>(
std::llround((double) v * gain), -8388608, 8388607);
s[0] = scaled & 0xFF;
s[1] = (scaled >> 8) & 0xFF;
s[2] = (scaled >> 16) & 0xFF;
}
break;
}
case 32: {
auto p = reinterpret_cast<int32_t *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = (int32_t) std::clamp(
std::llround((double) p[i] * gain),
(long long) INT32_MIN, (long long) INT32_MAX);
}
break;
}
default:
break;
}
}
std::string stream_flags_str(DWORD flags) { std::string stream_flags_str(DWORD flags) {
FLAGS_START(flags); FLAGS_START(flags);
FLAG(flags, AUDCLNT_STREAMFLAGS_CROSSPROCESS); FLAG(flags, AUDCLNT_STREAMFLAGS_CROSSPROCESS);
@@ -18,3 +72,46 @@ std::string stream_flags_str(DWORD flags) {
FLAG(flags, AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY); FLAG(flags, AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY);
FLAGS_END(flags); FLAGS_END(flags);
} }
void print_format(AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration,
REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format) {
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(share_mode));
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(stream_flags));
log_info("audio::wasapi", "... hnsBufferDuration : {} ({:.3f} ms)",
buffer_duration, buffer_duration / 10000.0);
log_info("audio::wasapi", "... hnsPeriodicity : {} ({:.3f} ms)",
periodicity, periodicity / 10000.0);
print_format(device_format);
}
void print_format(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *device_format) {
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(share_mode));
print_format(device_format);
}
HRESULT initialize_with_alignment_retry(IAudioClient *client, const char *log_group,
AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration,
REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format, LPCGUID session_guid) {
HRESULT ret = client->Initialize(share_mode, stream_flags, buffer_duration, periodicity,
device_format, session_guid);
// the requested buffer size can end up unaligned for the device; recover by asking for the next
// aligned buffer size and re-initializing with a matching duration.
if (ret == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
UINT32 aligned_frames = 0;
if (SUCCEEDED(client->GetBufferSize(&aligned_frames)) && aligned_frames > 0) {
REFERENCE_TIME aligned_duration = (REFERENCE_TIME)
(10000.0 * 1000 / device_format->nSamplesPerSec * aligned_frames + 0.5);
log_info(log_group, "buffer not aligned, retrying with {} frames ({} hns)",
aligned_frames, aligned_duration);
ret = client->Initialize(share_mode, stream_flags, aligned_duration,
periodicity != 0 ? aligned_duration : 0, device_format, session_guid);
}
}
return ret;
}
@@ -1,8 +1,104 @@
#pragma once #pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string> #include <string>
#include <windows.h> #include <windows.h>
#include <mmreg.h> #include <mmreg.h>
#include <audioclient.h>
std::string stream_flags_str(DWORD flags); std::string stream_flags_str(DWORD flags);
// log the stream parameters (share mode, flags, buffer duration, periodicity) followed by the wave
// format, matching the block printed at the top of IAudioClient::Initialize.
void print_format(AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration,
REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format);
// log the share mode followed by the wave format, for paths that only have a share mode (e.g.
// IAudioClient::IsFormatSupported).
void print_format(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *device_format);
// scale every sample of an interleaved device buffer by `gain`, clamped to the format's range.
// supports 16/24/32-bit PCM and 32-bit float; other formats are left untouched.
void apply_gain(BYTE *buffer, UINT32 frames, const WAVEFORMATEXTENSIBLE &fmt, float gain);
// detect IEEE float samples: WAVE_FORMAT_IEEE_FLOAT, or WAVE_FORMAT_EXTENSIBLE whose SubFormat is
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT (Data1 == 3; _PCM has Data1 == 1)
inline bool is_ieee_float(const WAVEFORMATEX *fmt) {
return fmt->wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|| (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
&& reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->SubFormat.Data1 == 0x00000003);
}
// read one sample at `p` as a normalized float in [-1, 1]
inline float read_sample(const BYTE *p, int bytes, bool is_float) {
if (is_float) {
float v;
memcpy(&v, p, sizeof(float));
return v;
}
switch (bytes) {
case 2: {
int16_t v;
memcpy(&v, p, sizeof(v));
return v * (1.0f / 32768.0f);
}
case 3: {
int32_t v = p[0] | (p[1] << 8) | (p[2] << 16);
if (v & 0x800000) {
v |= ~0xFFFFFF; // sign extend
}
return v * (1.0f / 8388608.0f);
}
case 4: {
int32_t v;
memcpy(&v, p, sizeof(v));
return (float) (v * (1.0 / 2147483648.0));
}
default:
return 0.0f;
}
}
// write the normalized float `value` to the sample at `p`, clamping to the format's range
inline void write_sample(BYTE *p, int bytes, bool is_float, float value) {
if (is_float) {
float v = std::clamp(value, -1.0f, 1.0f);
memcpy(p, &v, sizeof(v));
return;
}
switch (bytes) {
case 2: {
int16_t v = (int16_t) std::clamp(
(int) std::lround(value * 32768.0f), -32768, 32767);
memcpy(p, &v, sizeof(v));
break;
}
case 3: {
int32_t v = (int32_t) std::clamp<int64_t>(
std::llround((double) value * 8388608.0), -8388608, 8388607);
p[0] = v & 0xFF;
p[1] = (v >> 8) & 0xFF;
p[2] = (v >> 16) & 0xFF;
break;
}
case 4: {
int32_t v = (int32_t) std::clamp<int64_t>(
std::llround((double) value * 2147483648.0), INT32_MIN, INT32_MAX);
memcpy(p, &v, sizeof(v));
break;
}
default:
break;
}
}
// initialize the real audio client, recovering from AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED by asking the
// device for the next aligned buffer size and re-initializing with a matching duration. log_group
// names the subsystem in the retry log line.
HRESULT initialize_with_alignment_retry(IAudioClient *client, const char *log_group,
AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration,
REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format, LPCGUID session_guid);
+11 -11
View File
@@ -48,33 +48,33 @@ void copy_wave_format(WAVEFORMATEXTENSIBLE *destination, const WAVEFORMATEX *sou
} }
void print_format(const WAVEFORMATEX *pFormat) { void print_format(const WAVEFORMATEX *pFormat) {
log_info("audio", "Wave Format:"); log_info("audio::wasapi", "Wave Format:");
// format specific // format specific
if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat); auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat);
log_info("audio", "... SubFormat : {}", guid2s(format->SubFormat)); log_info("audio::wasapi", "... SubFormat : {}", guid2s(format->SubFormat));
} else { } else {
log_info("audio", "... wFormatTag : {}", pFormat->wFormatTag); log_info("audio::wasapi", "... wFormatTag : {}", pFormat->wFormatTag);
} }
// generic // generic
log_info("audio", "... nChannels : {}", pFormat->nChannels); log_info("audio::wasapi", "... nChannels : {}", pFormat->nChannels);
log_info("audio", "... nSamplesPerSec : {}", pFormat->nSamplesPerSec); log_info("audio::wasapi", "... nSamplesPerSec : {}", pFormat->nSamplesPerSec);
log_info("audio", "... nAvgBytesPerSec : {}", pFormat->nAvgBytesPerSec); log_info("audio::wasapi", "... nAvgBytesPerSec : {}", pFormat->nAvgBytesPerSec);
log_info("audio", "... nBlockAlign : {}", pFormat->nBlockAlign); log_info("audio::wasapi", "... nBlockAlign : {}", pFormat->nBlockAlign);
log_info("audio", "... wBitsPerSample : {}", pFormat->wBitsPerSample); log_info("audio::wasapi", "... wBitsPerSample : {}", pFormat->wBitsPerSample);
// format specific // format specific
if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat); auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat);
if (pFormat->wBitsPerSample == 0) { if (pFormat->wBitsPerSample == 0) {
log_info("audio", "... wSamplesPerBlock : {}", format->Samples.wSamplesPerBlock); log_info("audio::wasapi", "... wSamplesPerBlock : {}", format->Samples.wSamplesPerBlock);
} else { } else {
log_info("audio", "... wValidBitsPerSample : {}", format->Samples.wValidBitsPerSample); log_info("audio::wasapi", "... wValidBitsPerSample : {}", format->Samples.wValidBitsPerSample);
} }
log_info("audio", "... dwChannelMask : {}", channel_mask_str(format->dwChannelMask)); log_info("audio::wasapi", "... dwChannelMask : {}", channel_mask_str(format->dwChannelMask));
} }
} }
@@ -0,0 +1,304 @@
// dx11 / dxgi hook entrypoint. trampolines d3d11.dll / dxgi.dll exports
// the moment those DLLs appear (LDR notification + poll-thread fallback),
// then drives proactive vtable capture so we don't lose the race against
// the execexe loader. per-vtable hook implementations live in the sibling
// files (d3d11_swapchain / d3d11_factory / d3d11_vtable_capture /
// d3d11_screenshot).
//
// note: never LoadLibrary d3d11/dxgi -- execexe pre-loads them itself and
// fails (error 0xa) if they're already in the loader's module list.
//
// 64-bit only.
#include "d3d11_backend.h"
#ifndef SPICE_D3D11
void graphics_d3d11_init() {}
void graphics_d3d11_shutdown() {}
#else
#include <atomic>
#include <thread>
#include <chrono>
#include <cwchar>
#include <mutex>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "d3d11_internal.h"
#include "util/nt_loader.h"
namespace {
using D3D11CreateDeviceAndSwapChain_t = HRESULT(WINAPI *)(
IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT,
const D3D_FEATURE_LEVEL *, UINT, UINT,
const DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **,
ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **);
using CreateDXGIFactory_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **);
D3D11CreateDeviceAndSwapChain_t D3D11CreateDeviceAndSwapChain_orig = nullptr;
CreateDXGIFactory_t CreateDXGIFactory_orig = nullptr;
CreateDXGIFactory1_t CreateDXGIFactory1_orig = nullptr;
CreateDXGIFactory2_t CreateDXGIFactory2_orig = nullptr;
std::atomic<bool> g_d3d11_exports_hooked { false };
std::atomic<bool> g_dxgi_exports_hooked { false };
// ----------------------------------------------------------------------
// top-level export hooks
HRESULT WINAPI D3D11CreateDeviceAndSwapChain_hook(
IDXGIAdapter *pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, UINT Flags,
const D3D_FEATURE_LEVEL *pFeatureLevels, UINT FeatureLevels, UINT SDKVersion,
const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain,
ID3D11Device **ppDevice, D3D_FEATURE_LEVEL *pFeatureLevel,
ID3D11DeviceContext **ppImmediateContext)
{
HRESULT res = D3D11CreateDeviceAndSwapChain_orig(
pAdapter, DriverType, Software, Flags,
pFeatureLevels, FeatureLevels, SDKVersion,
pSwapChainDesc, ppSwapChain, ppDevice, pFeatureLevel, ppImmediateContext);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
if (pSwapChainDesc) {
d3d11_hooks::note_main_hwnd(pSwapChainDesc->OutputWindow);
}
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
}
return res;
}
#define DEFINE_FACTORY_HOOK(NAME, SIG_PARAMS, ORIG_ARGS) \
HRESULT WINAPI NAME##_hook SIG_PARAMS { \
HRESULT res = NAME##_orig ORIG_ARGS; \
if (SUCCEEDED(res) && ppFactory && *ppFactory) { \
d3d11_hooks::install_factory_hooks( \
reinterpret_cast<IUnknown *>(*ppFactory)); \
} \
return res; \
}
DEFINE_FACTORY_HOOK(CreateDXGIFactory,
(REFIID riid, void **ppFactory),
(riid, ppFactory))
DEFINE_FACTORY_HOOK(CreateDXGIFactory1,
(REFIID riid, void **ppFactory),
(riid, ppFactory))
DEFINE_FACTORY_HOOK(CreateDXGIFactory2,
(UINT Flags, REFIID riid, void **ppFactory),
(Flags, riid, ppFactory))
#undef DEFINE_FACTORY_HOOK
// ----------------------------------------------------------------------
// export trampoline plumbing
// serializes trampoline_export() so the LDR notification callback and the
// poll thread don't race each other into MinHook against the same target.
std::mutex g_export_mutex;
bool trampoline_export(const char *dll, const char *name, void *hook, void **orig) {
std::lock_guard<std::mutex> lock(g_export_mutex);
if (*orig) {
return true;
}
HMODULE mod = GetModuleHandleA(dll);
if (!mod) {
return false;
}
void *addr = reinterpret_cast<void *>(GetProcAddress(mod, name));
if (!addr) {
return false;
}
*orig = addr; // trampoline_try reads *orig before overwriting it.
if (!detour::trampoline_try(addr, hook, orig)) {
*orig = nullptr;
return false;
}
log_info("graphics::d3d11", "trampolined {}!{}", dll, name);
return true;
}
void try_install_d3d11_exports() {
if (g_d3d11_exports_hooked) {
return;
}
if (trampoline_export("d3d11.dll", "D3D11CreateDeviceAndSwapChain",
(void *) D3D11CreateDeviceAndSwapChain_hook,
(void **) &D3D11CreateDeviceAndSwapChain_orig)) {
g_d3d11_exports_hooked = true;
}
}
void try_install_dxgi_exports() {
if (g_dxgi_exports_hooked) {
return;
}
struct entry { const char *name; void *hook; void **orig; };
const entry entries[] = {
{ "CreateDXGIFactory", (void *) CreateDXGIFactory_hook,
(void **) &CreateDXGIFactory_orig },
{ "CreateDXGIFactory1", (void *) CreateDXGIFactory1_hook,
(void **) &CreateDXGIFactory1_orig },
{ "CreateDXGIFactory2", (void *) CreateDXGIFactory2_hook,
(void **) &CreateDXGIFactory2_orig },
};
bool any = false;
for (auto &e : entries) {
any |= trampoline_export("dxgi.dll", e.name, e.hook, e.orig);
}
if (any) {
g_dxgi_exports_hooked = true;
}
}
void try_capture_if_ready() {
if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) {
d3d11_hooks::try_capture_vtables();
}
}
// ----------------------------------------------------------------------
// LDR notification + polling fallback
bool dll_name_ends_with(PCUNICODE_STRING name, const wchar_t *suffix) {
if (!name || !name->Buffer) {
return false;
}
const size_t n = name->Length / sizeof(WCHAR);
const size_t s = wcslen(suffix);
return n >= s && _wcsnicmp(name->Buffer + n - s, suffix, s) == 0;
}
VOID CALLBACK ldr_dll_notification(
ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID /*context*/)
{
if (reason != LDR_DLL_NOTIFICATION_REASON_LOADED || !data) {
return;
}
if (dll_name_ends_with(data->Loaded.BaseDllName, L"d3d11.dll")) {
try_install_d3d11_exports();
} else if (dll_name_ends_with(data->Loaded.BaseDllName, L"dxgi.dll")) {
try_install_dxgi_exports();
}
}
// execexe maps d3d11/dxgi via a path that bypasses LdrLoadDll, so the
// notification above never fires for those DLLs and we have to poll.
std::atomic<bool> g_stop { false };
std::thread g_poll_thread;
std::mutex g_init_mutex;
PVOID g_ldr_cookie = nullptr;
void poll_thread() {
using namespace std::chrono_literals;
for (int32_t i = 0; i < 120 && !g_stop.load(); ++i) {
try_install_d3d11_exports();
try_install_dxgi_exports();
if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) {
d3d11_hooks::try_capture_vtables();
return;
}
// sliced so shutdown doesn't have to wait a full second.
for (int32_t s = 0; s < 10 && !g_stop.load(); ++s) {
std::this_thread::sleep_for(100ms);
}
}
}
// the overlay's imgui dx11 backend needs D3DCompile (d3dcompiler_XX.dll) to
// build its shaders. _43 ships with the DX June 2010 redist on stock Win7;
// _46/_47 come with newer Windows.
bool d3dcompiler_available() {
static const wchar_t *names[] = {
L"d3dcompiler_47.dll",
L"d3dcompiler_46.dll",
L"d3dcompiler_43.dll",
};
for (auto name : names) {
HMODULE mod = GetModuleHandleW(name);
if (!mod) {
mod = LoadLibraryW(name);
}
if (mod && GetProcAddress(mod, "D3DCompile")) {
return true;
}
}
return false;
}
} // namespace
void graphics_d3d11_init() {
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps
// their startup path completely untouched (no exports patched, no poll
// thread, no LDR callback).
if (!GetModuleHandleW(L"execexe.dll")) {
return;
}
// no d3dcompiler -> overlay can't build shaders; skip dx11 overlay
if (!d3dcompiler_available()) {
log_warning(
"graphics::d3d11",
"d3dcompiler not found; dx11 overlay disabled");
return;
}
std::lock_guard<std::mutex> lock(g_init_mutex);
if (g_poll_thread.joinable()) {
return; // already initialized
}
log_info("graphics::d3d11", "initializing");
// trampoline now if either DLL is already in the PEB.
try_install_d3d11_exports();
try_install_dxgi_exports();
try_capture_if_ready();
// catches standard LdrLoadDll loads.
auto reg = reinterpret_cast<decltype(&LdrRegisterDllNotification)>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification"));
if (reg) {
NTSTATUS st = reg(0, ldr_dll_notification, nullptr, &g_ldr_cookie);
if (NT_SUCCESS(st)) {
log_info("graphics::d3d11", "registered LDR DLL notification");
} else {
g_ldr_cookie = nullptr;
log_warning("graphics::d3d11",
"LdrRegisterDllNotification failed: {:#x}", (unsigned long)st);
}
}
// catches the execexe loader path that bypasses LdrLoadDll.
g_poll_thread = std::thread(poll_thread);
}
void graphics_d3d11_shutdown() {
std::lock_guard<std::mutex> lock(g_init_mutex);
// unregister first so the callback can't fire mid-teardown.
if (g_ldr_cookie) {
auto unreg = reinterpret_cast<decltype(&LdrUnregisterDllNotification)>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrUnregisterDllNotification"));
if (unreg) {
unreg(g_ldr_cookie);
}
g_ldr_cookie = nullptr;
}
g_stop.store(true);
if (g_poll_thread.joinable()) {
g_poll_thread.join();
}
}
#endif // SPICE_D3D11
@@ -0,0 +1,24 @@
#pragma once
#include "overlay/overlay.h"
void graphics_d3d11_init();
void graphics_d3d11_shutdown();
#ifdef SPICE_D3D11
struct ID3D11Device;
struct ID3D11DeviceContext;
struct ID3D11RenderTargetView;
struct IDXGISwapChain;
namespace overlay::d3d11 {
void render(ID3D11Device *device,
ID3D11DeviceContext *context,
IDXGISwapChain *swapchain,
ID3D11RenderTargetView **rtv);
}
#endif
@@ -0,0 +1,102 @@
// dx11 factory vtable hooks. patches CreateSwapChain / CreateSwapChainForHwnd
// so we can install_swapchain_hooks against every newly-created swapchain.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <mutex>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "d3d11_internal.h"
namespace {
using CreateSwapChain_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGIFactory *, IUnknown *, DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **);
using CreateSwapChainForHwnd_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGIFactory2 *, IUnknown *, HWND,
const DXGI_SWAP_CHAIN_DESC1 *,
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *,
IDXGIOutput *, IDXGISwapChain1 **);
CreateSwapChain_t CreateSwapChain_orig = nullptr;
CreateSwapChainForHwnd_t CreateSwapChainForHwnd_orig = nullptr;
bool g_factory_hooked = false;
bool g_factory2_hooked = false;
std::mutex g_hook_mutex;
HRESULT STDMETHODCALLTYPE CreateSwapChain_hook(
IDXGIFactory *factory, IUnknown *pDevice,
DXGI_SWAP_CHAIN_DESC *pDesc, IDXGISwapChain **ppSwapChain)
{
HRESULT res = CreateSwapChain_orig(factory, pDevice, pDesc, ppSwapChain);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
if (pDesc) {
d3d11_hooks::note_main_hwnd(pDesc->OutputWindow);
}
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
}
return res;
}
HRESULT STDMETHODCALLTYPE CreateSwapChainForHwnd_hook(
IDXGIFactory2 *factory, IUnknown *pDevice, HWND hWnd,
const DXGI_SWAP_CHAIN_DESC1 *pDesc,
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc,
IDXGIOutput *pRestrictToOutput, IDXGISwapChain1 **ppSwapChain)
{
HRESULT res = CreateSwapChainForHwnd_orig(
factory, pDevice, hWnd, pDesc, pFullscreenDesc, pRestrictToOutput, ppSwapChain);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
d3d11_hooks::note_main_hwnd(hWnd);
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
}
return res;
}
// QI-and-hook helper: dedupes the IDXGIFactory / IDXGIFactory2 install paths.
template<typename Iface>
void install_on(IUnknown *factory, bool &flag,
size_t vtbl_index, void *hook, void **orig, const char *name)
{
if (flag) {
return;
}
Iface *f = nullptr;
if (FAILED(factory->QueryInterface(IID_PPV_ARGS(&f))) || !f) {
return;
}
if (d3d11_hooks::hook_vtbl(f, vtbl_index, hook, orig, name)) {
flag = true;
}
f->Release();
}
} // namespace
namespace d3d11_hooks {
void install_factory_hooks(IUnknown *factory) {
if (!factory) {
return;
}
std::lock_guard<std::mutex> lock(g_hook_mutex);
install_on<IDXGIFactory>(factory, g_factory_hooked, 10,
(void *) CreateSwapChain_hook, (void **) &CreateSwapChain_orig,
"IDXGIFactory::CreateSwapChain");
install_on<IDXGIFactory2>(factory, g_factory2_hooked, 15,
(void *) CreateSwapChainForHwnd_hook, (void **) &CreateSwapChainForHwnd_orig,
"IDXGIFactory2::CreateSwapChainForHwnd");
}
}
#endif // SPICE_D3D11
@@ -0,0 +1,59 @@
#pragma once
// internal glue for the dx11 backend. all symbols gated on SPICE_D3D11.
#include "overlay/overlay.h"
#ifdef SPICE_D3D11
#include <memory>
#include "util/detour.h"
#include "util/logging.h"
struct HWND__; typedef HWND__ *HWND;
struct IUnknown;
struct IDXGISwapChain;
namespace d3d11_hooks {
void install_swapchain_hooks(IDXGISwapChain *swapchain);
void install_factory_hooks(IUnknown *factory);
void try_capture_vtables();
// first non-null swapchain HWND wins; later ones (sub-screens, IME
// helpers) are ignored. the dummy capture window is exempted via
// ignore_hwnd.
void note_main_hwnd(HWND hwnd);
HWND main_hwnd();
void ignore_hwnd(HWND hwnd);
// capture backbuffer to PNG if a screenshot was requested.
void try_screenshot(IDXGISwapChain *swapchain);
// trampoline a virtual method by vtable index. on failure *orig is null.
inline bool hook_vtbl(void *iface, size_t index,
void *hook, void **orig, const char *name)
{
void **vtbl = *reinterpret_cast<void ***>(iface);
void *target = vtbl[index];
// trampoline_try reads *orig before overwriting it.
*orig = target;
if (!detour::trampoline_try(target, hook, orig)) {
*orig = nullptr;
log_warning("graphics::d3d11", "failed to hook {}", name);
return false;
}
log_info("graphics::d3d11", "hooked {}", name);
return true;
}
// minimal COM RAII used by capture / screenshot paths.
struct com_release {
void operator()(IUnknown *p) const { if (p) p->Release(); }
};
template<typename T> using com_ptr = std::unique_ptr<T, com_release>;
}
#endif
@@ -0,0 +1,165 @@
// dx11 screenshot capture. mirrors the d3d9 backend: copy the current
// backbuffer into a staging texture, force alpha=255, write PNG via
// stb_image_write, push to clipboard and notify.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <vector>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include "d3d11_internal.h"
#include "external/stb_image_write.h"
#include "hooks/graphics/graphics.h"
#include "misc/clipboard.h"
#include "overlay/notifications.h"
#include "util/fileutils.h"
using d3d11_hooks::com_ptr;
namespace {
// copy the swapchain backbuffer into a CPU-readable staging texture and
// flatten it into an RGBA8 buffer (BGRA backbuffers are swizzled,
// alpha is forced to 255).
bool copy_backbuffer_to_rgba(IDXGISwapChain *swapchain,
ID3D11Device *device,
ID3D11DeviceContext *context,
std::vector<uint8_t> &out,
uint32_t &out_w, uint32_t &out_h)
{
ID3D11Texture2D *raw_bb = nullptr;
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&raw_bb))) || !raw_bb) {
return false;
}
com_ptr<ID3D11Texture2D> backbuffer(raw_bb);
D3D11_TEXTURE2D_DESC desc {};
backbuffer->GetDesc(&desc);
// MSAA backbuffers can't be CopyResource'd into a non-MS staging target.
com_ptr<ID3D11Texture2D> resolved;
ID3D11Texture2D *source = backbuffer.get();
if (desc.SampleDesc.Count > 1) {
D3D11_TEXTURE2D_DESC rd = desc;
rd.SampleDesc.Count = 1;
rd.SampleDesc.Quality = 0;
rd.Usage = D3D11_USAGE_DEFAULT;
rd.BindFlags = D3D11_BIND_RENDER_TARGET;
rd.CPUAccessFlags = 0;
rd.MiscFlags = 0;
ID3D11Texture2D *r = nullptr;
if (FAILED(device->CreateTexture2D(&rd, nullptr, &r)) || !r) {
return false;
}
resolved.reset(r);
context->ResolveSubresource(resolved.get(), 0, backbuffer.get(), 0, desc.Format);
source = resolved.get();
}
D3D11_TEXTURE2D_DESC sd {};
sd.Width = desc.Width;
sd.Height = desc.Height;
sd.MipLevels = 1;
sd.ArraySize = 1;
sd.Format = desc.Format;
sd.SampleDesc.Count = 1;
sd.Usage = D3D11_USAGE_STAGING;
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
ID3D11Texture2D *raw_staging = nullptr;
if (FAILED(device->CreateTexture2D(&sd, nullptr, &raw_staging)) || !raw_staging) {
return false;
}
com_ptr<ID3D11Texture2D> staging(raw_staging);
context->CopyResource(staging.get(), source);
D3D11_MAPPED_SUBRESOURCE mapped {};
if (FAILED(context->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) {
return false;
}
// backbuffers from GetDesc are always fully-typed (never _TYPELESS).
const bool is_bgra = desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM
|| desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
out.resize(static_cast<size_t>(desc.Width) * desc.Height * 4);
const uint8_t *src_base = reinterpret_cast<const uint8_t *>(mapped.pData);
for (uint32_t y = 0; y < desc.Height; ++y) {
const uint8_t *row = src_base + static_cast<size_t>(y) * mapped.RowPitch;
uint8_t *dst = out.data() + static_cast<size_t>(y) * desc.Width * 4;
for (uint32_t x = 0; x < desc.Width; ++x) {
dst[x * 4 + 0] = row[x * 4 + (is_bgra ? 2 : 0)];
dst[x * 4 + 1] = row[x * 4 + 1];
dst[x * 4 + 2] = row[x * 4 + (is_bgra ? 0 : 2)];
dst[x * 4 + 3] = 255;
}
}
context->Unmap(staging.get(), 0);
out_w = desc.Width;
out_h = desc.Height;
return true;
}
} // namespace
namespace d3d11_hooks {
void try_screenshot(IDXGISwapChain *swapchain) {
if (!swapchain || !graphics_screenshot_consume()) {
return;
}
auto file_path = graphics_screenshot_genpath();
if (file_path.empty()) {
return;
}
ID3D11Device *raw_device = nullptr;
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&raw_device))) || !raw_device) {
return;
}
com_ptr<ID3D11Device> device(raw_device);
ID3D11DeviceContext *raw_ctx = nullptr;
device->GetImmediateContext(&raw_ctx);
if (!raw_ctx) {
return;
}
com_ptr<ID3D11DeviceContext> context(raw_ctx);
std::vector<uint8_t> pixels;
uint32_t w = 0, h = 0;
if (!copy_backbuffer_to_rgba(swapchain, device.get(), context.get(), pixels, w, h)) {
log_warning("graphics::d3d11", "screenshot: failed to capture backbuffer");
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to capture");
return;
}
log_info("graphics::d3d11", "saving screenshot to {}", file_path);
if (stbi_write_png(file_path.c_str(), (int) w, (int) h, 4,
pixels.data(), (int) w * 4))
{
clipboard::copy_image(file_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
} else {
log_warning("graphics::d3d11", "screenshot: stbi_write_png failed");
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to save");
}
}
}
#endif // SPICE_D3D11
@@ -0,0 +1,294 @@
// dx11 swapchain vtable hooks + per-frame overlay pump.
//
// dxgi shares vtables across swapchain instances, so we only need to patch
// Present / Present1 / ResizeBuffers once on the first instance we see.
// each frame we lazily attach the overlay to whichever swapchain is
// presenting, then drive its imgui update / new_frame / render cycle.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <atomic>
#include <mutex>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "d3d11_internal.h"
#include "external/imgui/imgui.h"
#include "external/imgui/backends/imgui_impl_dx11.h"
#include "overlay/imgui/impl_spice.h"
#include "games/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
#include "util/utils.h"
// --------------------------------------------------------------------------
// overlay render bridge
namespace overlay::d3d11 {
// sRGB backbuffers need a UNORM view: ImGui vertex colors are already
// sRGB-encoded, so an extra linear->sRGB conversion would wash the
// overlay out white.
static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) {
switch (fmt) {
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM;
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM;
default: return fmt;
}
}
static void ensure_rtv(ID3D11Device *device,
IDXGISwapChain *swapchain,
ID3D11RenderTargetView **rtv)
{
if (*rtv || !device || !swapchain) {
return;
}
ID3D11Texture2D *backbuffer = nullptr;
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) {
return;
}
D3D11_TEXTURE2D_DESC td {};
backbuffer->GetDesc(&td);
const DXGI_FORMAT view_fmt = to_unorm_view(td.Format);
if (view_fmt != td.Format) {
D3D11_RENDER_TARGET_VIEW_DESC rtvd {};
rtvd.Format = view_fmt;
rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
device->CreateRenderTargetView(backbuffer, &rtvd, rtv);
} else {
device->CreateRenderTargetView(backbuffer, nullptr, rtv);
}
backbuffer->Release();
}
// bind the backbuffer (lazily creating the RTV) and draw the imgui
// frame on top. reset_invalidate releases *rtv on ResizeBuffers.
void render(ID3D11Device *device,
ID3D11DeviceContext *context,
IDXGISwapChain *swapchain,
ID3D11RenderTargetView **rtv)
{
ensure_rtv(device, swapchain, rtv);
if (!*rtv || !context) {
return;
}
// present happens immediately after, so no need to save the previous
// RT binding (flip-model resets it anyway).
context->OMSetRenderTargets(1, rtv, nullptr);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
}
}
// --------------------------------------------------------------------------
// file-local state + per-frame helpers
namespace {
using Present_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain *, UINT, UINT);
using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT);
using Present1_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *);
Present_t Present_orig = nullptr;
ResizeBuffers_t ResizeBuffers_orig = nullptr;
Present1_t Present1_orig = nullptr;
bool g_swapchain_hooked = false;
bool g_swapchain1_hooked = false;
void try_create_overlay(IDXGISwapChain *swapchain) {
if (!swapchain || overlay::OVERLAY) {
return;
}
DXGI_SWAP_CHAIN_DESC desc {};
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return;
}
// only attach to the main game window; ignore sub-screens / IME helpers.
HWND main = d3d11_hooks::main_hwnd();
if (main && desc.OutputWindow != main) {
return;
}
// theme the native title bar; first present is the only reliable point for
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
set_window_dark_titlebar(desc.OutputWindow);
ID3D11Device *device = nullptr;
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) {
return;
}
ID3D11DeviceContext *context = nullptr;
device->GetImmediateContext(&context);
if (context) {
overlay::create_d3d11(desc.OutputWindow, device, context, swapchain);
RECT cr {};
::GetClientRect(desc.OutputWindow, &cr);
log_info("graphics::d3d11",
"attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}",
(uintptr_t) desc.OutputWindow,
desc.BufferDesc.Width, desc.BufferDesc.Height,
cr.right - cr.left, cr.bottom - cr.top);
context->Release();
}
device->Release();
}
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour).
void poll_screenshot_hotkey() {
static bool s_down = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
const bool pressed = buttons
&& (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered())
&& GameAPI::Buttons::getState(RI_MGR,
buttons->at(games::OverlayButtons::Screenshot));
if (pressed && !s_down) {
graphics_screenshot_trigger();
}
s_down = pressed;
}
void pump_overlay(IDXGISwapChain *swapchain) {
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
return;
}
poll_screenshot_hotkey();
// size imgui to the backbuffer (not window client). dxgi may upscale
// a small backbuffer into a larger client rect; without this override
// imgui would draw past the RTV and the mouse mapping would be off.
DXGI_SWAP_CHAIN_DESC desc {};
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
ImGui_ImplSpice_SetDisplaySizeOverride(
(float) desc.BufferDesc.Width,
(float) desc.BufferDesc.Height);
}
overlay::OVERLAY->update();
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
// after overlay render so toasts/menus end up in the saved image.
d3d11_hooks::try_screenshot(swapchain);
}
// ----------------------------------------------------------------------
// swapchain method hooks
HRESULT STDMETHODCALLTYPE Present_hook(
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
{
try_create_overlay(swapchain);
pump_overlay(swapchain);
return Present_orig(swapchain, SyncInterval, Flags);
}
HRESULT STDMETHODCALLTYPE Present1_hook(
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
const DXGI_PRESENT_PARAMETERS *pParams)
{
try_create_overlay(swapchain);
pump_overlay(swapchain);
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
}
HRESULT STDMETHODCALLTYPE ResizeBuffers_hook(
IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height,
DXGI_FORMAT NewFormat, UINT SwapChainFlags)
{
const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
if (ours) {
log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}",
Width, Height, (int32_t) NewFormat);
overlay::OVERLAY->reset_invalidate();
}
HRESULT res = ResizeBuffers_orig(
swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags);
if (ours && SUCCEEDED(res)) {
overlay::OVERLAY->reset_recreate();
}
return res;
}
} // namespace
// --------------------------------------------------------------------------
// d3d11_hooks public surface: main-window tracking + vtable install.
namespace d3d11_hooks {
namespace {
std::atomic<HWND> g_main_hwnd { nullptr };
std::atomic<HWND> g_ignored_hwnd { nullptr };
}
void note_main_hwnd(HWND hwnd) {
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
return;
}
HWND expected = nullptr;
if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) {
log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}",
(uintptr_t) hwnd);
}
}
HWND main_hwnd() {
return g_main_hwnd.load();
}
void ignore_hwnd(HWND hwnd) {
g_ignored_hwnd.store(hwnd);
}
// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented)
// IDXGISwapChain1::Present1. idempotent; flag is set only after success
// so failed attempts can be retried on the next swapchain.
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
if (!swapchain) {
return;
}
static std::mutex s_hook_mutex;
std::lock_guard<std::mutex> lock(s_hook_mutex);
if (!g_swapchain_hooked) {
const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook,
(void **) &Present_orig, "IDXGISwapChain::Present");
const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook,
(void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers");
if (a && b) {
g_swapchain_hooked = true;
}
}
if (!g_swapchain1_hooked) {
IDXGISwapChain1 *sc1 = nullptr;
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
if (hook_vtbl(sc1, 22, (void *) Present1_hook,
(void **) &Present1_orig, "IDXGISwapChain1::Present1")) {
g_swapchain1_hooked = true;
}
sc1->Release();
}
}
}
}
#endif // SPICE_D3D11
@@ -0,0 +1,175 @@
// proactive vtable capture for the dx11 backend.
//
// titles under the execexe loader routinely race past our export-level
// trampolines, so the game's first real swapchain never goes through us.
// we sidestep that by creating a throwaway device + swapchain ourselves
// the moment d3d11.dll + dxgi.dll appear, which patches the shared
// IDXGISwapChain[1] / IDXGIFactory[2] vtables ahead of the game.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <atomic>
#include <memory>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "d3d11_internal.h"
using d3d11_hooks::com_ptr;
namespace {
using D3D11CreateDevice_t = HRESULT(WINAPI *)(
IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT,
const D3D_FEATURE_LEVEL *, UINT, UINT,
ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **);
using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **);
std::atomic<bool> g_vtables_captured { false };
template<typename Fn>
Fn resolve(HMODULE mod, const char *name) {
return reinterpret_cast<Fn>(GetProcAddress(mod, name));
}
com_ptr<IDXGIFactory2> create_factory2(CreateDXGIFactory2_t f2,
CreateDXGIFactory1_t f1)
{
IDXGIFactory2 *raw = nullptr;
if (f2 && SUCCEEDED(f2(0, IID_PPV_ARGS(&raw))) && raw) {
return com_ptr<IDXGIFactory2>(raw);
}
IDXGIFactory1 *factory1 = nullptr;
if (f1 && SUCCEEDED(f1(IID_PPV_ARGS(&factory1))) && factory1) {
factory1->QueryInterface(IID_PPV_ARGS(&raw));
factory1->Release();
}
return com_ptr<IDXGIFactory2>(raw);
}
bool create_dummy_device(D3D11CreateDevice_t create,
com_ptr<ID3D11Device> &device,
com_ptr<ID3D11DeviceContext> &context)
{
static constexpr D3D_FEATURE_LEVEL levels[] = {
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0,
};
// hardware first, then WARP so headless / unusual configs still work.
for (auto type : { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }) {
ID3D11Device *d = nullptr;
ID3D11DeviceContext *c = nullptr;
D3D_FEATURE_LEVEL got;
if (SUCCEEDED(create(nullptr, type, nullptr, 0,
levels, ARRAYSIZE(levels), D3D11_SDK_VERSION,
&d, &got, &c)) && d) {
device.reset(d);
context.reset(c);
return true;
}
}
return false;
}
} // namespace
namespace d3d11_hooks {
// create a throwaway device + swapchain to patch the shared vtables before
// the game's loader races past our export trampolines. safe to call
// repeatedly; runs at most once.
void try_capture_vtables() {
if (g_vtables_captured.load()) {
return;
}
HMODULE d3d11 = GetModuleHandleW(L"d3d11.dll");
HMODULE dxgi = GetModuleHandleW(L"dxgi.dll");
if (!d3d11 || !dxgi) {
return;
}
auto create_device = resolve<D3D11CreateDevice_t>(d3d11, "D3D11CreateDevice");
auto f2 = resolve<CreateDXGIFactory2_t>(dxgi, "CreateDXGIFactory2");
auto f1 = resolve<CreateDXGIFactory1_t>(dxgi, "CreateDXGIFactory1");
if (!create_device || (!f1 && !f2)) {
return;
}
// serialize concurrent calls (poll thread + LDR notification). only
// flip g_vtables_captured after success so failed attempts remain
// retriable on the next tick.
static std::atomic<bool> in_progress { false };
if (in_progress.exchange(true)) {
return;
}
struct scope_clear {
std::atomic<bool> &flag;
~scope_clear() { flag.store(false); }
} clear { in_progress };
// hidden message-only window; STATIC is always registered by user32.
HWND dummy_hwnd = CreateWindowExW(
0, L"STATIC", L"", 0, 0, 0, 1, 1,
HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr);
if (!dummy_hwnd) {
log_warning("graphics::d3d11",
"vtable capture: CreateWindowExW failed (gle={})", (unsigned long)GetLastError());
return;
}
auto destroy_hwnd = std::unique_ptr<HWND__, decltype(&DestroyWindow)>(
dummy_hwnd, &DestroyWindow);
// if the game's CreateDXGIFactory_hook already raced us, our
// CreateSwapChainForHwnd call below would trip the hook and try to
// record dummy_hwnd as the main window. block that.
ignore_hwnd(dummy_hwnd);
auto factory2 = create_factory2(f2, f1);
if (!factory2) {
log_warning("graphics::d3d11", "vtable capture: CreateDXGIFactory* failed");
return;
}
com_ptr<ID3D11Device> device;
com_ptr<ID3D11DeviceContext> context;
if (!create_dummy_device(create_device, device, context)) {
log_warning("graphics::d3d11", "vtable capture: D3D11CreateDevice failed");
return;
}
DXGI_SWAP_CHAIN_DESC1 desc {};
desc.Width = 1;
desc.Height = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = 2;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
IDXGISwapChain1 *raw_sc = nullptr;
HRESULT hr = factory2->CreateSwapChainForHwnd(
device.get(), dummy_hwnd, &desc, nullptr, nullptr, &raw_sc);
if (FAILED(hr) || !raw_sc) {
log_warning("graphics::d3d11",
"vtable capture: CreateSwapChainForHwnd failed (hr={:#x})", (unsigned long)hr);
return;
}
com_ptr<IDXGISwapChain1> swapchain(raw_sc);
install_swapchain_hooks(swapchain.get());
install_factory_hooks(factory2.get());
g_vtables_captured.store(true);
log_info("graphics::d3d11", "vtable capture complete (via dummy swapchain)");
}
}
#endif // SPICE_D3D11
@@ -28,8 +28,10 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/notifications.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/fileutils.h"
#include "util/flags_helper.h" #include "util/flags_helper.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
@@ -716,7 +718,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
} }
// in windowed mode, LDJ will always launch two windows, no special handling needed here // in windowed mode, LDJ will always launch two windows, no special handling needed here
} else if (avs::game::is_model("KFC")) { } else if (avs::game::is_model("KFC")) {
if (GRAPHICS_WINDOWED && GRAPHICS_PREVENT_SECONDARY_WINDOW) { if (GRAPHICS_WINDOWED && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// user wants windowed mode but does not want subscreen at all // user wants windowed mode but does not want subscreen at all
pCaps->NumberOfAdaptersInGroup = 1; pCaps->NumberOfAdaptersInGroup = 1;
} else { } else {
@@ -1271,9 +1273,11 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
wintouchemu::update(); wintouchemu::update();
// newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW // newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW
// (when enabled on older versions of EG, you end up with graphical glitches on the subscreen // (when enabled on older versions of EG, you end up with graphical glitches on the subscreen)
// early versions of popn HC needs this as well, otherwise the subscreen doesn't update at all //
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW || games::popn::is_pikapika_model()) { // early versions of popn HC needs this as well, but not on by default as it can cause
// graphical glitches on some GPUs
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0); SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
} }
} }
@@ -1434,11 +1438,18 @@ static void save_screenshot(const std::string &file_path, UINT height, IDirect3D
if (FAILED(hr)) { if (FAILED(hr)) {
log_warning("graphics::d3d9", "Failed to save screenshot"); log_warning("graphics::d3d9", "Failed to save screenshot");
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to save");
return; return;
} }
// save to clipboard // save to clipboard
clipboard::copy_image(file_path); clipboard::copy_image(file_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
} else { } else {
log_warning("graphics::d3d9", "Direct3D save helper function not available"); log_warning("graphics::d3d9", "Direct3D save helper function not available");
} }
@@ -1449,6 +1460,15 @@ void graphics_d3d9_on_present(
IDirect3DDevice9 *device, IDirect3DDevice9 *device,
IDirect3DDevice9 *wrapped_device) { IDirect3DDevice9 *wrapped_device) {
// image resize / orientation swap. run here (the present path) rather than from `EndScene`,
// which may fire several times per frame on multi-pass / render-to-texture games. this is the
// single point guaranteed to be after the game's last `EndScene` and before the real `Present`,
// so the back buffer is fully drawn and the expensive StretchRect work happens exactly once per
// frame. it must run before the overlay is rendered so the overlay isn't scaled with the image.
if (cfg::SCREENRESIZE->enable_screen_resize || GRAPHICS_FS_ORIENTATION_SWAP) {
SurfaceHook(device);
}
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never // Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with // call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
// `dx9osd.dll` for pfreepanic. // `dx9osd.dll` for pfreepanic.
@@ -1471,9 +1491,7 @@ void graphics_d3d9_on_present(
// for IIDX TDJ / SDVX UFC, handle subscreen // for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm = games::sdvx::is_valkyrie_model(); const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE; const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
const bool is_gfdm_arena = games::gitadora::is_arena_model() && const bool is_gfdm_arena = games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS;
(GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW);
const bool is_pika = games::popn::is_pikapika_model(); const bool is_pika = games::popn::is_pikapika_model();
if (is_vm || is_tdj || is_gfdm_arena || is_pika) { if (is_vm || is_tdj || is_gfdm_arena || is_pika) {
graphics_d3d9_ldj_on_present(wrapped_device); graphics_d3d9_ldj_on_present(wrapped_device);
@@ -1519,6 +1537,9 @@ void graphics_d3d9_on_present(
log_warning("graphics::d3d9", log_warning("graphics::d3d9",
"failed to get back buffer, hr={}", "failed to get back buffer, hr={}",
FMT_HRESULT(hr)); FMT_HRESULT(hr));
if (capture) {
graphics_capture_skip(capture_screen);
}
return; return;
} }
@@ -1529,6 +1550,9 @@ void graphics_d3d9_on_present(
"failed to acquire back buffer descriptor, hr={}", "failed to acquire back buffer descriptor, hr={}",
FMT_HRESULT(hr)); FMT_HRESULT(hr));
buffer->Release(); buffer->Release();
if (capture) {
graphics_capture_skip(capture_screen);
}
return; return;
} }
@@ -1542,6 +1566,9 @@ void graphics_d3d9_on_present(
"failed to acquire temporary surface, hr={}", "failed to acquire temporary surface, hr={}",
FMT_HRESULT(hr)); FMT_HRESULT(hr));
buffer->Release(); buffer->Release();
if (capture) {
graphics_capture_skip(capture_screen);
}
return; return;
} }
@@ -1552,6 +1579,9 @@ void graphics_d3d9_on_present(
FMT_HRESULT(hr)); FMT_HRESULT(hr));
temp_surface->Release(); temp_surface->Release();
buffer->Release(); buffer->Release();
if (capture) {
graphics_capture_skip(capture_screen);
}
return; return;
} }
@@ -1,7 +1,11 @@
#include "d3d9_device.h" #include "d3d9_device.h"
#include <algorithm>
#include <cassert> #include <cassert>
#include <climits>
#include <mutex> #include <mutex>
#include <unordered_map>
#include <vector>
#include "avs/game.h" #include "avs/game.h"
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
@@ -12,6 +16,7 @@
#include "cfg/screen_resize.h" #include "cfg/screen_resize.h"
#include "d3d9_backend.h" #include "d3d9_backend.h"
#include "d3d9_live2d.h"
#include "d3d9_texture.h" #include "d3d9_texture.h"
#ifndef SPICE64 #ifndef SPICE64
@@ -253,15 +258,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
{ {
WRAP_VERBOSE; WRAP_VERBOSE;
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
int index = 0; int index = 0;
bool create_swap_chain = false; bool create_swap_chain = false;
bool create_fake_swap_chain = false;
if (avs::game::is_model({"LDJ", "KFC", "M39"})) { if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
create_swap_chain = true; create_swap_chain = true;
} else if (games::gitadora::is_arena_model() && } else if (games::gitadora::is_arena_model() &&
(GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW)) { (GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
if (pPresentationParameters->BackBufferWidth == 800) { if (pPresentationParameters->BackBufferWidth == 800) {
// SMALL (subscreen) // SMALL (subscreen)
create_swap_chain = true; create_swap_chain = true;
@@ -273,11 +278,32 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
if (sub_swapchain[index] || fake_sub_swapchain[index]) { if (sub_swapchain[index] || fake_sub_swapchain[index]) {
index = 2; index = 2;
} }
create_fake_swap_chain = GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS &&
!GRAPHICS_PREVENT_SECONDARY_WINDOWS;
} else { } else {
log_warning("graphics::d3d9", "unknown swap chain detected in CreateAdditionalSwapChain"); log_warning("graphics::d3d9", "unknown swap chain detected in CreateAdditionalSwapChain");
} }
} }
if (create_fake_swap_chain) {
if (!fake_sub_swapchain[index]) {
log_info(
"graphics::d3d9",
"CreateAdditionalSwapChain called for hidden GITADORA side swap chain {}, "
"using fake swap chain",
index);
fake_sub_swapchain[index] =
new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
}
fake_sub_swapchain[index]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
return D3D_OK;
}
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
if (create_swap_chain) { if (create_swap_chain) {
log_misc( log_misc(
"graphics::d3d9", "graphics::d3d9",
@@ -739,18 +765,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::BeginScene() {
static IDirect3DSurface9 *topSurface = nullptr; static IDirect3DSurface9 *topSurface = nullptr;
static IDirect3DSurface9 *backbuffer = nullptr; static IDirect3DSurface9 *backbuffer = nullptr;
static LPDIRECT3DSWAPCHAIN9 mSwapChain = nullptr; static IDirect3DTexture9 *tex = nullptr;
static IDirect3DTexture9* tex;
static UINT topSurface_width = 0; static UINT topSurface_width = 0;
static UINT topSurface_height = 0; static UINT topSurface_height = 0;
static float topSurface_aspect_ratio = 1.f; static float topSurface_aspect_ratio = 1.f;
static UINT backbuffer_width = 0;
static UINT backbuffer_height = 0;
void SurfaceHook(IDirect3DDevice9 *pReal) { void SurfaceHook(IDirect3DDevice9 *pReal) {
D3DPRESENT_PARAMETERS param {};
pReal->GetSwapChain(0, &mSwapChain);
mSwapChain->GetPresentParameters(&param);
// phase 0 - create a surface that has the same aspect ratio as the original back buffer // phase 0 - create a surface that has the same aspect ratio as the original back buffer
// but larger in size. this is needed to ensure that when the user zooms out, we have // but larger in size. this is needed to ensure that when the user zooms out, we have
@@ -758,8 +781,20 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
// //
// (only done once) // (only done once)
if (!topSurface) { if (!topSurface) {
topSurface_width = param.BackBufferWidth * 3; // GetSwapChain/GetBackBuffer AddRef their out parameters; the swap chain is only
topSurface_height = param.BackBufferHeight * 3; // needed here (during one-time init), so release it after use to avoid a per-frame
// ref leak. The backbuffer ref is intentionally retained for the lifetime of the
// cached resources.
LPDIRECT3DSWAPCHAIN9 swapChain = nullptr;
D3DPRESENT_PARAMETERS param {};
pReal->GetSwapChain(0, &swapChain);
swapChain->GetPresentParameters(&param);
backbuffer_width = param.BackBufferWidth;
backbuffer_height = param.BackBufferHeight;
topSurface_width = backbuffer_width * 3;
topSurface_height = backbuffer_height * 3;
topSurface_aspect_ratio = (float)topSurface_width / (float)topSurface_height; topSurface_aspect_ratio = (float)topSurface_width / (float)topSurface_height;
if (pReal->CreateTexture(topSurface_width, topSurface_height, 1, if (pReal->CreateTexture(topSurface_width, topSurface_height, 1,
D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,
@@ -772,16 +807,18 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
param.BackBufferWidth, param.BackBufferHeight, param.BackBufferCount); param.BackBufferWidth, param.BackBufferHeight, param.BackBufferCount);
tex->GetSurfaceLevel(0, &topSurface); tex->GetSurfaceLevel(0, &topSurface);
if (mSwapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backbuffer) != D3D_OK) { if (swapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backbuffer) != D3D_OK) {
log_warning("graphics::d3d9", "SurfaceHook - GetBackBuffer failed"); log_warning("graphics::d3d9", "SurfaceHook - GetBackBuffer failed");
} }
swapChain->Release();
} }
// pre-calculate dimensions used for phase 1 // pre-calculate dimensions used for phase 1
const int rectLeft = param.BackBufferWidth; const int rectLeft = backbuffer_width;
const int rectTop = param.BackBufferHeight; const int rectTop = backbuffer_height;
const int w = param.BackBufferWidth; const int w = backbuffer_width;
const int h = param.BackBufferHeight; const int h = backbuffer_height;
// this code used to clear the surface using ColorFill on every call, but // this code used to clear the surface using ColorFill on every call, but
// this turned out to be very expensive, leading to major frame drops in // this turned out to be very expensive, leading to major frame drops in
@@ -791,9 +828,7 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
cfg::SCREENRESIZE->need_surface_clean = false; cfg::SCREENRESIZE->need_surface_clean = false;
} }
D3DLOCKED_RECT rect;
HRESULT hr; HRESULT hr;
topSurface->LockRect(&rect, NULL, D3DLOCK_DONOTWAIT);
// phase 1 - copy the original back buffer onto the new surface. // phase 1 - copy the original back buffer onto the new surface.
// we draw it 1:1 in the center of the surface. // we draw it 1:1 in the center of the surface.
@@ -856,7 +891,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
originRect2.left, originRect2.top, originRect2.right, originRect2.bottom); originRect2.left, originRect2.top, originRect2.right, originRect2.bottom);
} }
} }
topSurface->UnlockRect();
// phase 2 - viewport calculation - do the actual zoom / offset math // phase 2 - viewport calculation - do the actual zoom / offset math
// figure out what region of the surface to copy back to the back buffer. // figure out what region of the surface to copy back to the back buffer.
@@ -931,7 +965,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
// targetRect.left, targetRect.top, targetRect.right, targetRect.bottom); // targetRect.left, targetRect.top, targetRect.right, targetRect.bottom);
// phase 3 - draw the surface to back buffer // phase 3 - draw the surface to back buffer
backbuffer->LockRect(&rect, NULL, D3DLOCK_DONOTWAIT);
bool use_linear_filter = true; bool use_linear_filter = true;
if (cfg::SCREENRESIZE->enable_screen_resize) { if (cfg::SCREENRESIZE->enable_screen_resize) {
use_linear_filter = cfg::SCREENRESIZE->enable_linear_filter; use_linear_filter = cfg::SCREENRESIZE->enable_linear_filter;
@@ -940,7 +973,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
topSurface, &targetRect, topSurface, &targetRect,
backbuffer, nullptr, backbuffer, nullptr,
use_linear_filter ? D3DTEXF_LINEAR : D3DTEXF_NONE); use_linear_filter ? D3DTEXF_LINEAR : D3DTEXF_NONE);
backbuffer->UnlockRect();
if (hr != D3D_OK) { if (hr != D3D_OK) {
log_warning( log_warning(
"graphics::d3d9", "graphics::d3d9",
@@ -952,10 +984,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::EndScene() { HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::EndScene() {
WRAP_DEBUG; WRAP_DEBUG;
if (cfg::SCREENRESIZE->enable_screen_resize || GRAPHICS_FS_ORIENTATION_SWAP) {
SurfaceHook(pReal);
}
static std::once_flag printed; static std::once_flag printed;
std::call_once(printed, []() { std::call_once(printed, []() {
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::EndScene"); log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::EndScene");
@@ -1278,6 +1306,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitive(
UINT PrimitiveCount) UINT PrimitiveCount)
{ {
WRAP_DEBUG; WRAP_DEBUG;
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
return D3D_OK;
}
CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount)); CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount));
} }
@@ -1290,6 +1321,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitive(
UINT PrimitiveCount) UINT PrimitiveCount)
{ {
WRAP_DEBUG; WRAP_DEBUG;
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
return D3D_OK;
}
CHECK_RESULT(pReal->DrawIndexedPrimitive( CHECK_RESULT(pReal->DrawIndexedPrimitive(
PrimitiveType, BaseVertexIndex, PrimitiveType, BaseVertexIndex,
MinVertexIndex, NumVertices, MinVertexIndex, NumVertices,
@@ -1305,6 +1339,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitiveUP(
WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})", WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})",
PrimitiveType, PrimitiveCount, PrimitiveType, PrimitiveCount,
fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride); fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride);
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
return D3D_OK;
}
CHECK_RESULT(pReal->DrawPrimitiveUP( CHECK_RESULT(pReal->DrawPrimitiveUP(
PrimitiveType, PrimitiveCount, PrimitiveType, PrimitiveCount,
pVertexStreamZeroData, VertexStreamZeroStride)); pVertexStreamZeroData, VertexStreamZeroStride));
@@ -1321,6 +1358,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitiveUP(
UINT VertexStreamZeroStride) UINT VertexStreamZeroStride)
{ {
WRAP_DEBUG; WRAP_DEBUG;
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
return D3D_OK;
}
CHECK_RESULT(pReal->DrawIndexedPrimitiveUP( CHECK_RESULT(pReal->DrawIndexedPrimitiveUP(
PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData, PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData,
IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride)); IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride));
@@ -1380,7 +1420,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateVertexShader(
IDirect3DVertexShader9 **ppShader) IDirect3DVertexShader9 **ppShader)
{ {
WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction)); WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction));
CHECK_RESULT(pReal->CreateVertexShader(pFunction, ppShader)); HRESULT ret = pReal->CreateVertexShader(pFunction, ppShader);
if (SUCCEEDED(ret) && ppShader != nullptr) {
d3d9_live2d::on_create_vertex_shader(*ppShader, pFunction);
}
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
}
return ret;
} }
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader( HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
@@ -1388,6 +1435,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
{ {
WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader)); WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader));
d3d9_live2d::on_set_vertex_shader(pShader);
#ifndef SPICE64 #ifndef SPICE64
// diagonal line fix // diagonal line fix
@@ -1534,13 +1583,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreatePixelShader(
IDirect3DPixelShader9 **ppShader) IDirect3DPixelShader9 **ppShader)
{ {
WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction)); WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction));
CHECK_RESULT(pReal->CreatePixelShader(pFunction, ppShader)); HRESULT ret = pReal->CreatePixelShader(pFunction, ppShader);
if (SUCCEEDED(ret) && ppShader != nullptr) {
d3d9_live2d::on_create_pixel_shader(*ppShader, pFunction);
}
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
}
return ret;
} }
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader( HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader(
IDirect3DPixelShader9 *pShader) IDirect3DPixelShader9 *pShader)
{ {
WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader)); WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader));
d3d9_live2d::on_set_pixel_shader(pShader);
CHECK_RESULT(pReal->SetPixelShader(pShader)); CHECK_RESULT(pReal->SetPixelShader(pShader));
} }
@@ -35,6 +35,10 @@ static const GUID IID_WrappedIDirect3DDevice9 = {
0x6dec0d40, 0x1339, 0x4bda, { 0xa5, 0xf2, 0x22, 0x31, 0xd4, 0x1, 0xf, 0xd1 } 0x6dec0d40, 0x1339, 0x4bda, { 0xa5, 0xf2, 0x22, 0x31, 0xd4, 0x1, 0xf, 0xd1 }
}; };
// applies the image resize / orientation swap by copying the back buffer through an
// oversized intermediate surface and back. expects the real (unwrapped) device.
void SurfaceHook(IDirect3DDevice9 *pReal);
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig) explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig)
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) { : hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) {
@@ -0,0 +1,144 @@
#include "d3d9_live2d.h"
// only the Live2D-capable SDVX versions are 64-bit, so the entire implementation
// is compiled out of 32-bit builds (the header supplies inline no-op stubs there).
#ifdef SPICE64
#include <cstdint>
#include <unordered_set>
#include "hooks/graphics/graphics.h"
// how the Live2D draw filtering works
// ------------------------------------
// SDVX draws its Live2D characters with a small, fixed set of pixel and
// vertex shaders. to skip those draws (and save GPU) we have to recognise them at
// the exact moment the game issues a draw call. the d3d9 device hooks feed three
// kinds of events into this module:
//
// 1. shader creation (on_create_pixel_shader / on_create_vertex_shader)
// the game compiles its shaders once at load. we can't trust the shader
// *object pointer* to identify a shader (it's just a heap address that
// varies per run and can be recycled), so instead we hash the shader's
// D3D9 *bytecode* - that fingerprint is stable across runs because the
// game ships the same shaders. if the hash matches a known Live2D shader
// we remember that object pointer in g_live2d_shaders.
//
// 2. shader binding (on_set_pixel_shader / on_set_vertex_shader)
// whenever the game binds a shader we look it up in that set once and cache
// the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen
// far less often than draws, so this is where the lookup cost lives.
//
// 3. draw call (should_skip_draw, called from every Draw* hook)
// the per-draw question "is this a Live2D draw?" is then just reading those
// two cached bools - no hashing, no map lookups. if the skip is currently
// active (see graphics_sdvx_live2d_should_skip) and either bound shader is
// Live2D, the Draw* hook drops the call instead of forwarding it.
//
// everything is gated on the feature being enabled (mode != Off); when it's Off
// every entry point is a single predicted-not-taken branch. d3d9 rendering for a
// device is single-threaded, so none of this state needs locking.
namespace {
// shader state is tracked whenever the feature might act (mode != Off) so the
// known-shader set is populated before a song starts. when Off, every entry
// point is a single cheap branch.
bool tracking_enabled() {
return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off;
}
// the set of shader objects (pixel or vertex) whose bytecode matched a known
// Live2D fingerprint. only matching shaders are stored, so this stays tiny.
std::unordered_set<void *> g_live2d_shaders;
// whether the currently-bound shaders are known Live2D shaders. cached at set
// time so the per-draw check is just two bool reads.
bool g_cur_ps_is_live2d = false;
bool g_cur_vs_is_live2d = false;
// FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF)
uint64_t bytecode_hash(const DWORD *func) {
if (func == nullptr) {
return 0;
}
const DWORD *p = func;
const DWORD *cap = func + 65536; // safety bound
while (p < cap && *p != 0x0000FFFF) {
p++;
}
const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD);
uint64_t h = 1469598103934665603ULL;
const auto *bytes = reinterpret_cast<const uint8_t *>(func);
for (size_t i = 0; i < n_bytes; i++) {
h ^= bytes[i];
h *= 1099511628211ULL;
}
return h;
}
// known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable
// across runs because the game ships fixed shaders. the two sets are disjoint so
// a single shader can be classified by its own hash alone.
bool hash_is_live2d(uint64_t hash) {
switch (hash) {
case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song)
case 0x2d7ce428c6b4775dULL: // pixel: masked model draw
case 0x3ce00cc6111c10e7ULL: // pixel: mask generation
case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant)
case 0xe9cf898c331e2a51ULL: // vertex
case 0x94dc84e7b7c0f437ULL: // vertex
case 0xc872937c5cc04309ULL: // vertex
return true;
}
return false;
}
// classify a shader at creation time and record it if it is Live2D. erasing on a
// miss keeps the set correct if the runtime reuses a freed shader pointer.
void classify_shader(void *shader, const DWORD *func) {
if (hash_is_live2d(bytecode_hash(func))) {
g_live2d_shaders.insert(shader);
} else {
g_live2d_shaders.erase(shader);
}
}
} // namespace
namespace d3d9_live2d {
// stage 1: fingerprint each shader as the game creates it
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) {
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
classify_shader(shader, func);
}
}
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) {
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
classify_shader(shader, func);
}
}
// stage 2: remember whether the just-bound shader is a Live2D one
void on_set_vertex_shader(IDirect3DVertexShader9 *shader) {
if (tracking_enabled()) [[unlikely]] {
g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0;
}
}
void on_set_pixel_shader(IDirect3DPixelShader9 *shader) {
if (tracking_enabled()) [[unlikely]] {
g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0;
}
}
// stage 3: drop the draw if the skip is active and a Live2D shader is bound
bool should_skip_draw() {
return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d);
}
} // namespace d3d9_live2d
#endif // SPICE64
@@ -0,0 +1,44 @@
#pragma once
#include <windows.h>
#include <d3d9.h>
// SDVX Live2D draw-skip support for the D3D9 backend.
//
// SDVX renders its Live2D navigator / in-song character through a fixed set of
// shaders. when the skip is active (see graphics_sdvx_live2d_should_skip)
// the matching draw calls are dropped to save GPU. shaders are identified by a
// stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode
// does not). the hashes were captured with the draw-call fingerprinting tool.
//
// every entry point is a no-op unless the feature is enabled (mode != Off), and
// d3d9 rendering for a device is single-threaded, so none of this needs locking.
namespace d3d9_live2d {
#ifdef SPICE64
// record a shader's bytecode fingerprint at creation time
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func);
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func);
// remember the currently-bound shaders
void on_set_vertex_shader(IDirect3DVertexShader9 *shader);
void on_set_pixel_shader(IDirect3DPixelShader9 *shader);
// true if the current draw call should be dropped (skip active AND the bound
// shaders identify it as SDVX Live2D)
bool should_skip_draw();
#else // !SPICE64
// only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point
// compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their
// call sites.
inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {}
inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {}
inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {}
inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {}
inline bool should_skip_draw() { return false; }
#endif // SPICE64
}
+290 -23
View File
@@ -5,6 +5,7 @@
#include "graphics.h" #include "graphics.h"
#include <chrono>
#include <set> #include <set>
#include <vector> #include <vector>
#include <mutex> #include <mutex>
@@ -18,6 +19,7 @@
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/popn/popn.h" #include "games/popn/popn.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h" #include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "touch/touch.h" #include "touch/touch.h"
@@ -39,6 +41,8 @@ struct CaptureData {
HWND TDJ_SUBSCREEN_WINDOW = nullptr; HWND TDJ_SUBSCREEN_WINDOW = nullptr;
HWND SDVX_SUBSCREEN_WINDOW = nullptr; HWND SDVX_SUBSCREEN_WINDOW = nullptr;
HWND GFDM_SUBSCREEN_WINDOW = nullptr; HWND GFDM_SUBSCREEN_WINDOW = nullptr;
static HWND GFDM_LEFT_WINDOW = nullptr;
static HWND GFDM_RIGHT_WINDOW = nullptr;
HWND POPN_SUBSCREEN_WINDOW = nullptr; HWND POPN_SUBSCREEN_WINDOW = nullptr;
bool FAKE_SUBSCREEN_ADAPTER = false; bool FAKE_SUBSCREEN_ADAPTER = false;
@@ -58,6 +62,19 @@ static std::mutex GRAPHICS_CAPTURE_SCREENS_M {};
static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {}; static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {};
static bool GRAPHICS_CAPTURE_SKIP_SIGNAL[GRAPHICS_CAPTURE_SCREEN_NO] {};
static constexpr std::chrono::milliseconds GRAPHICS_CAPTURE_RECEIVE_TIMEOUT {2000};
static void graphics_capture_cancel_pending(int screen) {
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
for (auto it = GRAPHICS_CAPTURE_SCREENS.rbegin(); it != GRAPHICS_CAPTURE_SCREENS.rend(); ++it) {
if (*it == screen) {
GRAPHICS_CAPTURE_SCREENS.erase(std::next(it).base());
return;
}
}
}
static std::optional<graphics_orientation> target_orientation_on_boot; static std::optional<graphics_orientation> target_orientation_on_boot;
static UINT target_refresh_rate_on_boot = 0; static UINT target_refresh_rate_on_boot = 0;
@@ -69,6 +86,10 @@ static bool monitor_layout_needs_reset = false;
bool GRAPHICS_CAPTURE_CURSOR = false; bool GRAPHICS_CAPTURE_CURSOR = false;
bool GRAPHICS_LOG_HRESULT = false; bool GRAPHICS_LOG_HRESULT = false;
bool GRAPHICS_SDVX_FORCE_720 = false; bool GRAPHICS_SDVX_FORCE_720 = false;
#ifdef SPICE64
SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Off;
std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY = false;
#endif // SPICE64
bool GRAPHICS_SHOW_CURSOR = false; bool GRAPHICS_SHOW_CURSOR = false;
bool GRAPHICS_WINDOWED = false; bool GRAPHICS_WINDOWED = false;
std::vector<HWND> GRAPHICS_WINDOWS; std::vector<HWND> GRAPHICS_WINDOWS;
@@ -76,7 +97,8 @@ UINT GRAPHICS_FORCE_REFRESH = 0;
std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB; std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER; std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
bool GRAPHICS_FORCE_SINGLE_ADAPTER = false; bool GRAPHICS_FORCE_SINGLE_ADAPTER = false;
bool GRAPHICS_PREVENT_SECONDARY_WINDOW = false; bool GRAPHICS_PREVENT_SECONDARY_WINDOWS = false;
bool GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = false;
graphics_dx9on12_state GRAPHICS_9_ON_12_STATE = DX9ON12_AUTO; graphics_dx9on12_state GRAPHICS_9_ON_12_STATE = DX9ON12_AUTO;
bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false; bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false;
bool SUBSCREEN_FORCE_REDRAW = false; bool SUBSCREEN_FORCE_REDRAW = false;
@@ -121,6 +143,128 @@ static void reset_window_hook(HWND hWnd) {
} }
} }
static std::string gitadora_canonical_window_name(const std::string &window_name) {
if (window_name == "GITADORA") {
return "GITADORA";
}
if (window_name.ends_with("LEFT")) {
return "LEFT";
}
if (window_name.ends_with("RIGHT")) {
return "RIGHT";
}
if (window_name.ends_with("SMALL")) {
return "SMALL";
}
return "";
}
static const char *gitadora_window_name_for_hwnd(HWND hWnd) {
if (hWnd == nullptr) {
return nullptr;
}
if (GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value()) {
return "GITADORA";
}
if (hWnd == GFDM_LEFT_WINDOW) {
return "LEFT";
}
if (hWnd == GFDM_RIGHT_WINDOW) {
return "RIGHT";
}
if (hWnd == GFDM_SUBSCREEN_WINDOW) {
return "SMALL";
}
return nullptr;
}
static bool is_gfdm_known_window(HWND hWnd) {
return gitadora_window_name_for_hwnd(hWnd) != nullptr;
}
static bool gitadora_should_block_game_window_placement(HWND hWnd) {
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
return false;
}
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
return window_name != nullptr && graphics_gitadora_has_window_monitor(window_name);
}
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
if (window_name == "LEFT") {
GFDM_LEFT_WINDOW = hWnd;
} else if (window_name == "RIGHT") {
GFDM_RIGHT_WINDOW = hWnd;
} else if (window_name == "SMALL") {
GFDM_SUBSCREEN_WINDOW = hWnd;
}
}
static bool gitadora_should_allow_small_resize() {
return GRAPHICS_WINDOWED &&
games::gitadora::is_arena_model() &&
!graphics_gitadora_is_borderless_windowed();
}
static void gitadora_apply_small_resize_style(DWORD &style) {
if (!gitadora_should_allow_small_resize()) {
return;
}
style |= WS_SIZEBOX;
style |= WS_MAXIMIZEBOX;
style |= WS_SYSMENU;
}
static void gitadora_force_window_style(HWND hWnd) {
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model() || hWnd == nullptr) {
return;
}
DWORD style = GetWindowLongA(hWnd, GWL_STYLE);
DWORD style_ex = GetWindowLongA(hWnd, GWL_EXSTYLE);
const DWORD style_orig = style;
const DWORD style_ex_orig = style_ex;
graphics_gitadora_apply_window_style(style, style_ex);
if (hWnd == GFDM_SUBSCREEN_WINDOW) {
gitadora_apply_small_resize_style(style);
}
if (style == style_orig && style_ex == style_ex_orig) {
return;
}
if (SetWindowLongA_orig != nullptr) {
SetWindowLongA_orig(hWnd, GWL_STYLE, static_cast<LONG>(style));
SetWindowLongA_orig(hWnd, GWL_EXSTYLE, static_cast<LONG>(style_ex));
} else {
SetWindowLongA(hWnd, GWL_STYLE, static_cast<LONG>(style));
SetWindowLongA(hWnd, GWL_EXSTYLE, static_cast<LONG>(style_ex));
}
const UINT flags =
SWP_NOMOVE |
SWP_NOSIZE |
SWP_NOZORDER |
SWP_NOACTIVATE |
SWP_FRAMECHANGED;
if (SetWindowPos_orig != nullptr) {
SetWindowPos_orig(hWnd, nullptr, 0, 0, 0, 0, flags);
} else {
SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, flags);
}
log_misc(
"graphics",
"GITADORA window style override: hwnd={}, style 0x{:x}->0x{:x}, ex 0x{:x}->0x{:x}",
fmt::ptr(hWnd),
style_orig,
style,
style_ex_orig,
style_ex);
}
// window procedure // window procedure
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
@@ -282,6 +426,18 @@ static LRESULT CALLBACK WsubWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPAR
log_misc("graphics", "ignore WM_CLOSE for subscreen window"); log_misc("graphics", "ignore WM_CLOSE for subscreen window");
return false; return false;
} }
if (hWnd == GFDM_SUBSCREEN_WINDOW && (uMsg == WM_MOVE || uMsg == WM_SIZE)) {
update_spicetouch_window_dimensions(hWnd);
if (SPICETOUCH_TOUCH_HWND) {
SetWindowPos(
SPICETOUCH_TOUCH_HWND, HWND_TOP,
SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y,
SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT,
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
}
}
return CallWindowProcA(WSUB_WNDPROC_ORIG, hWnd, uMsg, wParam, lParam); return CallWindowProcA(WSUB_WNDPROC_ORIG, hWnd, uMsg, wParam, lParam);
} }
@@ -345,25 +501,37 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
fmt::ptr(lpParam)); fmt::ptr(lpParam));
// gfdm // gfdm
std::string effective_window_name = window_name;
if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) { if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) {
// set window name // set window name
if (!lpWindowName) { if (!lpWindowName) {
lpWindowName = "GITADORA"; lpWindowName = "GITADORA";
effective_window_name = "GITADORA";
} }
} }
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub"); bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen"); bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen");
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen"); bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
bool is_gfdm_sub_window = games::gitadora::is_arena_model() && window_name.ends_with("SMALL"); const std::string gfdm_window_name = games::gitadora::is_arena_model()
? gitadora_canonical_window_name(effective_window_name)
: "";
const bool is_gfdm_window = !gfdm_window_name.empty();
const bool is_gfdm_sub_window = gfdm_window_name == "SMALL";
const bool allow_gfdm_small_resize =
is_gfdm_sub_window && gitadora_should_allow_small_resize();
// update style / ex-style // update style / ex-style
if (is_tdj_sub_window || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) { if (is_tdj_sub_window || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) {
// hide maximize button (prevent misaligned touches) // hide maximize button (prevent misaligned touches)
dwStyle &= ~(WS_MAXIMIZEBOX); if (!allow_gfdm_small_resize) {
dwStyle &= ~(WS_MAXIMIZEBOX);
}
// mouse clicks become misaligned when resized // mouse clicks become misaligned when resized
dwStyle &= ~(WS_SIZEBOX); if (!allow_gfdm_small_resize) {
dwStyle &= ~(WS_SIZEBOX);
}
// borderless // borderless
if (GRAPHICS_WINDOWED && GRAPHICS_WSUB_BORDERLESS) { if (GRAPHICS_WINDOWED && GRAPHICS_WSUB_BORDERLESS) {
@@ -377,6 +545,13 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
} }
if (allow_gfdm_small_resize) {
gitadora_apply_small_resize_style(dwStyle);
}
if (is_gfdm_window) {
graphics_gitadora_apply_window_style(dwStyle, dwExStyle);
}
if (is_sdvx_sub_window) { if (is_sdvx_sub_window) {
graphics_load_windowed_subscreen_parameters(); graphics_load_windowed_subscreen_parameters();
if (GRAPHICS_WSUB_SIZE.has_value()) { if (GRAPHICS_WSUB_SIZE.has_value()) {
@@ -395,6 +570,16 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
} }
if (is_gfdm_window) {
graphics_gitadora_apply_window_monitor(
gfdm_window_name,
x,
y,
nWidth,
nHeight,
true);
}
if (GRAPHICS_WINDOWED) { if (GRAPHICS_WINDOWED) {
graphics_window_check_bounds_before_creation(x, y, nWidth, nHeight); graphics_window_check_bounds_before_creation(x, y, nWidth, nHeight);
} }
@@ -404,6 +589,9 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
hWndParent, hMenu, hInstance, lpParam); hWndParent, hMenu, hInstance, lpParam);
GRAPHICS_WINDOWS.push_back(result); GRAPHICS_WINDOWS.push_back(result);
// theme the native title bar (dark/light)
set_window_dark_titlebar(result);
if (is_tdj_sub_window) { if (is_tdj_sub_window) {
// TDJ windowed mode: remember the subscreen window handle for later // TDJ windowed mode: remember the subscreen window handle for later
TDJ_SUBSCREEN_WINDOW = result; TDJ_SUBSCREEN_WINDOW = result;
@@ -421,14 +609,20 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
// only hook touch window if multiple windows are allowed // only hook touch window if multiple windows are allowed
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOW) { if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
GFDM_SUBSCREEN_WINDOW = result; gitadora_remember_window(result, gfdm_window_name);
}
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_remember_window(result, gfdm_window_name);
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
} }
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_force_window_style(result);
}
if (is_popn_sub_window) { if (is_popn_sub_window) {
POPN_SUBSCREEN_WINDOW = result; POPN_SUBSCREEN_WINDOW = result;
if (!GRAPHICS_PREVENT_SECONDARY_WINDOW) { if (!GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
graphics_hook_subscreen_window(POPN_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(POPN_SUBSCREEN_WINDOW);
} }
} }
@@ -519,6 +713,9 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
hWndParent, hMenu, hInstance, lpParam); hWndParent, hMenu, hInstance, lpParam);
GRAPHICS_WINDOWS.push_back(result); GRAPHICS_WINDOWS.push_back(result);
// theme the native title bar (dark/light)
set_window_dark_titlebar(result);
log_misc( log_misc(
"graphics", "graphics",
"CreateWindowExW returned {}, {}", "CreateWindowExW returned {}, {}",
@@ -613,6 +810,12 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
} }
} }
// Monitor overrides are applied at creation time. Suppress later game
// placement calls instead of resizing again during scene transitions.
if (gitadora_should_block_game_window_placement(hWnd)) {
return TRUE;
}
// call original // call original
return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint); return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint);
} }
@@ -666,8 +869,17 @@ static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
dwNewLong |= WS_OVERLAPPEDWINDOW; dwNewLong |= WS_OVERLAPPEDWINDOW;
} }
// call original const bool force_gfdm_style =
return SetWindowLongA_orig(hWnd, nIndex, dwNewLong); GRAPHICS_WINDOWED &&
games::gitadora::is_arena_model() &&
is_gfdm_known_window(hWnd) &&
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE);
const auto result = SetWindowLongA_orig(hWnd, nIndex, dwNewLong);
if (force_gfdm_style) {
gitadora_force_window_style(hWnd);
}
return result;
} }
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) { static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
@@ -677,8 +889,17 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
dwNewLong |= WS_OVERLAPPEDWINDOW; dwNewLong |= WS_OVERLAPPEDWINDOW;
} }
// call original const bool force_gfdm_style =
return SetWindowLongW_orig(hWnd, nIndex, dwNewLong); GRAPHICS_WINDOWED &&
games::gitadora::is_arena_model() &&
is_gfdm_known_window(hWnd) &&
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE);
const auto result = SetWindowLongW_orig(hWnd, nIndex, dwNewLong);
if (force_gfdm_style) {
gitadora_force_window_style(hWnd);
}
return result;
} }
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter, static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
@@ -689,6 +910,13 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
return TRUE; return TRUE;
} }
// Monitor overrides are applied at creation time. Suppress later game
// placement calls instead of resizing again during scene transitions.
if (gitadora_should_block_game_window_placement(hWnd) &&
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE)) {
return TRUE;
}
// prevent gitadora arena model from shifting windows around if the user has preferences // prevent gitadora arena model from shifting windows around if the user has preferences
if (GRAPHICS_WINDOWED && games::gitadora::is_arena_model() && if (GRAPHICS_WINDOWED && games::gitadora::is_arena_model() &&
GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value() && GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value() &&
@@ -702,14 +930,21 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) { static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
if (games::gitadora::is_arena_model() && if (games::gitadora::is_arena_model() &&
GRAPHICS_PREVENT_SECONDARY_WINDOW && GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
hWnd != GRAPHICS_HOOKED_WINDOW) { hWnd != GRAPHICS_HOOKED_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd)); log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true; return true;
} }
if (games::gitadora::is_arena_model() &&
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS &&
(hWnd == GFDM_LEFT_WINDOW || hWnd == GFDM_RIGHT_WINDOW)) {
log_info("graphics", "ShowWindow_hook - hiding GITADORA side window {}", fmt::ptr(hWnd));
return true;
}
if (games::popn::is_pikapika_model() && if (games::popn::is_pikapika_model() &&
GRAPHICS_PREVENT_SECONDARY_WINDOW && GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
hWnd == POPN_SUBSCREEN_WINDOW) { hWnd == POPN_SUBSCREEN_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd)); log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true; return true;
@@ -841,6 +1076,7 @@ void graphics_init() {
// init backends // init backends
graphics_d3d9_init(); graphics_d3d9_init();
graphics_d3d11_init();
// general hooks // general hooks
ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook); ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook);
@@ -997,18 +1233,20 @@ void graphics_capture_trigger(int screen) {
} }
bool graphics_capture_consume(int *screen) { bool graphics_capture_consume(int *screen) {
auto flag = !GRAPHICS_CAPTURE_SCREENS.empty(); std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
if (flag) {
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
*screen = GRAPHICS_CAPTURE_SCREENS.back(); if (GRAPHICS_CAPTURE_SCREENS.empty()) {
GRAPHICS_CAPTURE_SCREENS.pop_back(); return false;
} }
return flag;
*screen = GRAPHICS_CAPTURE_SCREENS.back();
GRAPHICS_CAPTURE_SCREENS.pop_back();
return true;
} }
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height) { void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height) {
GRAPHICS_CAPTURE_BUFFER_M[screen].lock(); GRAPHICS_CAPTURE_BUFFER_M[screen].lock();
GRAPHICS_CAPTURE_SKIP_SIGNAL[screen] = false;
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen]; auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
capture.data.reset(data); capture.data.reset(data);
capture.width = width; capture.width = width;
@@ -1019,6 +1257,14 @@ void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t he
} }
void graphics_capture_skip(int screen) { void graphics_capture_skip(int screen) {
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return;
}
{
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
GRAPHICS_CAPTURE_SKIP_SIGNAL[screen] = true;
}
GRAPHICS_CAPTURE_CV[screen].notify_one(); GRAPHICS_CAPTURE_CV[screen].notify_one();
} }
@@ -1026,11 +1272,32 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp, bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp,
int *width, int *height) { int *width, int *height) {
// wait for capture event if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return false;
}
// wait for capture event (with timeout)
std::unique_lock<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]); std::unique_lock<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
GRAPHICS_CAPTURE_CV[screen].wait(lock, [screen] { const bool ready = GRAPHICS_CAPTURE_CV[screen].wait_for(
return GRAPHICS_CAPTURE_BUFFER[screen].data != nullptr; lock,
}); GRAPHICS_CAPTURE_RECEIVE_TIMEOUT,
[screen] {
return GRAPHICS_CAPTURE_BUFFER[screen].data != nullptr
|| GRAPHICS_CAPTURE_SKIP_SIGNAL[screen];
});
if (!ready) {
lock.unlock();
graphics_capture_cancel_pending(screen);
return false;
}
if (GRAPHICS_CAPTURE_SKIP_SIGNAL[screen]) {
GRAPHICS_CAPTURE_SKIP_SIGNAL[screen] = false;
lock.unlock();
return false;
}
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen]; auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
auto capture_data = capture.data; auto capture_data = capture.data;
auto capture_width = capture.width; auto capture_width = capture.width;
+45 -2
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <atomic>
#include <string> #include <string>
#include <vector> #include <vector>
#include <optional> #include <optional>
@@ -27,6 +28,33 @@ enum graphics_dx9on12_state {
DX9ON12_FORCE_ON, DX9ON12_FORCE_ON,
}; };
// SDVX Live2D suppression policy. the mode comes from the launcher
// option; the in-gameplay flag is maintained by the SDVX scene-detection hook in
// games/sdvx (which does not require the SDVX game module to be enabled). the
// d3d9 backend reads graphics_sdvx_live2d_should_skip() on every draw.
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
// compiled out of 32-bit builds.
#ifdef SPICE64
enum class SdvxLive2dMode {
Off, // leave Live2D untouched (default)
Always, // always skip Live2D draws (also removes the menu navigator)
InGame, // skip Live2D draws only during a song
};
extern SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE;
extern std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY;
inline bool graphics_sdvx_live2d_should_skip() {
switch (GRAPHICS_SDVX_LIVE2D_MODE) {
case SdvxLive2dMode::Always:
return true;
case SdvxLive2dMode::InGame:
return GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.load(std::memory_order_relaxed);
default:
return false;
}
}
#endif // SPICE64
// flag settings // flag settings
extern bool GRAPHICS_CAPTURE_CURSOR; extern bool GRAPHICS_CAPTURE_CURSOR;
extern bool GRAPHICS_LOG_HRESULT; extern bool GRAPHICS_LOG_HRESULT;
@@ -38,7 +66,8 @@ extern UINT GRAPHICS_FORCE_REFRESH;
extern std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB; extern std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
extern std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER; extern std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
extern bool GRAPHICS_FORCE_SINGLE_ADAPTER; extern bool GRAPHICS_FORCE_SINGLE_ADAPTER;
extern bool GRAPHICS_PREVENT_SECONDARY_WINDOW; extern bool GRAPHICS_PREVENT_SECONDARY_WINDOWS;
extern bool GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS;
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION; extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION;
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION_SUB; extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION_SUB;
extern bool GRAPHICS_FS_ORIENTATION_SWAP; extern bool GRAPHICS_FS_ORIENTATION_SWAP;
@@ -55,6 +84,10 @@ extern bool GRAPHICS_WINDOW_ALWAYS_ON_TOP;
extern bool GRAPHICS_WINDOW_BACKBUFFER_SCALE; extern bool GRAPHICS_WINDOW_BACKBUFFER_SCALE;
extern bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS; extern bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS;
extern std::optional<HWND> GRAPHICS_HOOKED_WINDOW; extern std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
extern bool GRAPHICS_IIDX_WSUB; extern bool GRAPHICS_IIDX_WSUB;
extern std::optional<std::string> GRAPHICS_WSUB_SIZE; extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
@@ -115,6 +148,16 @@ bool graphics_window_resize_breaks_game();
bool graphics_window_move_and_resize_breaks_game(); bool graphics_window_move_and_resize_breaks_game();
void graphics_load_windowed_subscreen_parameters(); void graphics_load_windowed_subscreen_parameters();
void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height); void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height);
bool graphics_gitadora_is_borderless_windowed();
void graphics_gitadora_apply_window_style(DWORD &style, DWORD &style_ex);
bool graphics_gitadora_apply_window_monitor(
const std::string &window_name,
int &x,
int &y,
int &width,
int &height,
bool log_change);
bool graphics_gitadora_has_window_monitor(const std::string &window_name);
void change_primary_monitor(const std::string &monitor_name); void change_primary_monitor(const std::string &monitor_name);
void update_monitor_on_boot( void update_monitor_on_boot(
@@ -123,4 +166,4 @@ void update_monitor_on_boot(
std::optional<std::pair<uint32_t, uint32_t>> target_resolution); std::optional<std::pair<uint32_t, uint32_t>> target_resolution);
void update_monitor_at_runtime(); void update_monitor_at_runtime();
void reset_monitor_on_exit(); void reset_monitor_on_exit();
@@ -26,6 +26,10 @@ bool GRAPHICS_WINDOW_ALWAYS_ON_TOP = false;
bool GRAPHICS_WINDOW_BACKBUFFER_SCALE = false; bool GRAPHICS_WINDOW_BACKBUFFER_SCALE = false;
bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS = false; bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS = false;
std::optional<HWND> GRAPHICS_HOOKED_WINDOW; std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
std::string GRAPHICS_GITADORA_MAIN_MONITOR;
std::string GRAPHICS_GITADORA_LEFT_MONITOR;
std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
std::string GRAPHICS_GITADORA_SMALL_MONITOR;
// IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed // IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
bool GRAPHICS_IIDX_WSUB = false; bool GRAPHICS_IIDX_WSUB = false;
@@ -51,6 +55,173 @@ static const DWORD SETWINDOWPOS_NOOP =
SWP_NOZORDER | SWP_NOZORDER |
SWP_ASYNCWINDOWPOS; SWP_ASYNCWINDOWPOS;
static int graphics_effective_window_decoration() {
if (GRAPHICS_WINDOW_STYLE.has_value()) {
return GRAPHICS_WINDOW_STYLE.value();
}
if (cfg::SCREENRESIZE != nullptr) {
return cfg::SCREENRESIZE->window_decoration;
}
return cfg::WindowDecorationMode::Default;
}
bool graphics_gitadora_is_borderless_windowed() {
return GRAPHICS_WINDOWED &&
graphics_effective_window_decoration() == cfg::WindowDecorationMode::Borderless;
}
static const std::string &graphics_gitadora_monitor_for_window_name(
const std::string &window_name) {
if (window_name == "GITADORA") {
return GRAPHICS_GITADORA_MAIN_MONITOR;
}
if (window_name == "LEFT") {
return GRAPHICS_GITADORA_LEFT_MONITOR;
}
if (window_name == "RIGHT") {
return GRAPHICS_GITADORA_RIGHT_MONITOR;
}
if (window_name == "SMALL") {
return GRAPHICS_GITADORA_SMALL_MONITOR;
}
static const std::string empty;
return empty;
}
bool graphics_gitadora_has_window_monitor(const std::string &window_name) {
const auto &monitor_name = graphics_gitadora_monitor_for_window_name(window_name);
return !monitor_name.empty();
}
static bool graphics_monitor_rect_from_name(
const std::string &monitor_name,
RECT &rect,
bool log_change) {
DEVMODEA devmode {};
devmode.dmSize = sizeof(devmode);
if (!EnumDisplaySettingsA(monitor_name.c_str(), ENUM_CURRENT_SETTINGS, &devmode)) {
if (log_change) {
log_warning(
"graphics-windowed",
"failed to get monitor settings for {}",
monitor_name);
}
return false;
}
rect.left = devmode.dmPosition.x;
rect.top = devmode.dmPosition.y;
rect.right = rect.left + static_cast<LONG>(devmode.dmPelsWidth);
rect.bottom = rect.top + static_cast<LONG>(devmode.dmPelsHeight);
return true;
}
static bool graphics_gitadora_apply_monitor_rect(
const std::string &monitor_name,
const std::string &window_name,
int &x,
int &y,
int &width,
int &height,
bool log_change) {
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
return false;
}
if (monitor_name.empty()) {
return false;
}
RECT rect {};
if (!graphics_monitor_rect_from_name(monitor_name, rect, log_change)) {
return false;
}
x = rect.left;
y = rect.top;
width = rect.right - rect.left;
height = rect.bottom - rect.top;
if (log_change) {
log_info(
"graphics-windowed",
"GITADORA {} monitor override: {} => pos=({}, {}), size={}x{}",
window_name,
monitor_name,
x,
y,
width,
height);
}
return true;
}
static bool graphics_gitadora_apply_main_monitor_to_config() {
if (cfg::SCREENRESIZE == nullptr || !graphics_gitadora_is_borderless_windowed()) {
return false;
}
int x = 0;
int y = 0;
int width = 0;
int height = 0;
if (!graphics_gitadora_apply_monitor_rect(
GRAPHICS_GITADORA_MAIN_MONITOR,
"GITADORA",
x,
y,
width,
height,
false)) {
return false;
}
cfg::SCREENRESIZE->enable_window_resize = true;
cfg::SCREENRESIZE->client_keep_aspect_ratio = false;
cfg::SCREENRESIZE->window_offset_x = x;
cfg::SCREENRESIZE->window_offset_y = y;
cfg::SCREENRESIZE->client_width = width;
cfg::SCREENRESIZE->client_height = height;
return true;
}
void graphics_gitadora_apply_window_style(DWORD &style, DWORD &style_ex) {
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
return;
}
switch (graphics_effective_window_decoration()) {
case cfg::WindowDecorationMode::Borderless:
style &= ~WS_OVERLAPPEDWINDOW;
style_ex &= ~WS_EX_CLIENTEDGE;
style_ex &= ~WS_EX_WINDOWEDGE;
break;
case cfg::WindowDecorationMode::ResizableFrame:
style |= WS_OVERLAPPEDWINDOW;
break;
case cfg::WindowDecorationMode::Default:
default:
break;
}
}
bool graphics_gitadora_apply_window_monitor(
const std::string &window_name,
int &x,
int &y,
int &width,
int &height,
bool log_change) {
return graphics_gitadora_apply_monitor_rect(
graphics_gitadora_monitor_for_window_name(window_name),
window_name,
x,
y,
width,
height,
log_change);
}
void graphics_capture_initial_window(HWND hWnd) { void graphics_capture_initial_window(HWND hWnd) {
if (!GRAPHICS_WINDOWED) { if (!GRAPHICS_WINDOWED) {
return; return;
@@ -181,6 +352,8 @@ void graphics_load_windowed_parameters() {
} }
} }
graphics_gitadora_apply_main_monitor_to_config();
// only override if true; don't stomp on user setting // only override if true; don't stomp on user setting
if (GRAPHICS_WINDOW_ALWAYS_ON_TOP) { if (GRAPHICS_WINDOW_ALWAYS_ON_TOP) {
cfg::SCREENRESIZE->window_always_on_top = true; cfg::SCREENRESIZE->window_always_on_top = true;
+130
View File
@@ -5,6 +5,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string> #include <string>
#include <mutex> #include <mutex>
#include <stddef.h>
#include "avs/core.h" #include "avs/core.h"
#include "avs/ea3.h" #include "avs/ea3.h"
@@ -18,12 +19,14 @@
// hooking related stuff // hooking related stuff
static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr; static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
static decltype(GetIpAddrTable) *GetIpAddrTable_orig = nullptr;
static decltype(bind) *bind_orig = nullptr; static decltype(bind) *bind_orig = nullptr;
// settings // settings
std::string NETWORK_ADDRESS = "10.9.0.0"; std::string NETWORK_ADDRESS = "10.9.0.0";
std::string NETWORK_SUBNET = "255.255.0.0"; std::string NETWORK_SUBNET = "255.255.0.0";
static bool GetAdaptersInfo_log = true; static bool GetAdaptersInfo_log = true;
static bool GetIpAddrTable_log = true;
// network structs // network structs
static struct in_addr network; static struct in_addr network;
@@ -43,6 +46,80 @@ static void defer_network_adapter_error() {
}); });
} }
static bool is_valid_ipaddr_row(const MIB_IPADDRROW &row) {
static const auto loopback = inet_addr("127.0.0.1");
return row.dwAddr != 0 && row.dwAddr != loopback;
}
static MIB_IPADDRROW *find_preferred_ipaddr_row(PMIB_IPADDRTABLE table) {
if (table == nullptr || table->dwNumEntries == 0) {
return nullptr;
}
// prefer the row matching -adapternetwork/-adaptersubnet
for (DWORD i = 0; i < table->dwNumEntries; i++) {
auto &row = table->table[i];
if (!is_valid_ipaddr_row(row)) {
continue;
}
auto row_prefix = row.dwAddr & row.dwMask;
if (row_prefix == prefix.s_addr && row.dwMask == subnet.s_addr) {
return &row;
}
}
// fall back to the interface Windows would route through by default
PMIB_IPFORWARDTABLE pIpForwardTable = (MIB_IPFORWARDTABLE *) malloc(sizeof(MIB_IPFORWARDTABLE));
DWORD dwSize = 0;
if (GetIpForwardTable(pIpForwardTable, &dwSize, TRUE) == ERROR_INSUFFICIENT_BUFFER) {
free(pIpForwardTable);
pIpForwardTable = (MIB_IPFORWARDTABLE *) malloc(dwSize);
}
if (GetIpForwardTable(pIpForwardTable, &dwSize, TRUE) != NO_ERROR || pIpForwardTable->dwNumEntries == 0) {
free(pIpForwardTable);
return nullptr;
}
DWORD best = pIpForwardTable->table[0].dwForwardIfIndex;
free(pIpForwardTable);
for (DWORD i = 0; i < table->dwNumEntries; i++) {
auto &row = table->table[i];
if (row.dwIndex == best && is_valid_ipaddr_row(row)) {
return &row;
}
}
// last resort: keep a deterministic valid row instead of exposing all adapters
for (DWORD i = 0; i < table->dwNumEntries; i++) {
auto &row = table->table[i];
if (is_valid_ipaddr_row(row)) {
return &row;
}
}
return nullptr;
}
static void keep_only_ipaddr_row(PMIB_IPADDRTABLE table, MIB_IPADDRROW *row) {
if (table == nullptr || row == nullptr) {
return;
}
if (GetIpAddrTable_log) {
in_addr addr {};
addr.s_addr = row->dwAddr;
log_info("network", "Using preferred IP address row: {}", inet_ntoa(addr));
}
table->table[0] = *row;
table->dwNumEntries = 1;
GetIpAddrTable_log = false;
}
static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen) { static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen) {
// call orig // call orig
@@ -183,6 +260,50 @@ static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG p
return ret; return ret;
} }
static DWORD WINAPI GetIpAddrTable_hook(PMIB_IPADDRTABLE pIpAddrTable, PULONG pdwSize, BOOL bOrder) {
auto input_size = pdwSize != nullptr ? *pdwSize : 0;
auto ret = GetIpAddrTable_orig(pIpAddrTable, pdwSize, bOrder);
if (ret == NO_ERROR) {
auto row = find_preferred_ipaddr_row(pIpAddrTable);
if (row != nullptr) {
keep_only_ipaddr_row(pIpAddrTable, row);
}
return ret;
}
if (ret != ERROR_INSUFFICIENT_BUFFER || pIpAddrTable == nullptr || pdwSize == nullptr) {
return ret;
}
// If the caller's buffer is large enough for the filtered single-row table,
// satisfy the call even when Windows needed more room for all adapters.
const auto one_row_size = offsetof(MIB_IPADDRTABLE, table) + sizeof(MIB_IPADDRROW);
if (input_size < one_row_size) {
return ret;
}
auto full_size = *pdwSize;
auto table = (PMIB_IPADDRTABLE) malloc(full_size);
if (table == nullptr) {
return ret;
}
auto full_ret = GetIpAddrTable_orig(table, &full_size, bOrder);
if (full_ret == NO_ERROR) {
auto row = find_preferred_ipaddr_row(table);
if (row != nullptr) {
keep_only_ipaddr_row(pIpAddrTable, row);
*pdwSize = one_row_size;
ret = NO_ERROR;
}
}
free(table);
return ret;
}
static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen) { static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen) {
#ifdef __clang__ #ifdef __clang__
@@ -243,6 +364,15 @@ void networkhook_init() {
GetAdaptersInfo_orig = orig_addr; GetAdaptersInfo_orig = orig_addr;
} }
// GetIpAddrTable hook
auto ip_addr_table_orig_addr = detour::iat_try(
"GetIpAddrTable", GetIpAddrTable_hook, nullptr);
if (!ip_addr_table_orig_addr) {
log_warning("network", "Could not hook GetIpAddrTable");
} else if (GetIpAddrTable_orig == nullptr) {
GetIpAddrTable_orig = ip_addr_table_orig_addr;
}
/* /*
* Bind Hook * Bind Hook
*/ */
+196 -15
View File
@@ -3,6 +3,8 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <cmath>
#include <cstdlib>
#include <assert.h> #include <assert.h>
#include <shlwapi.h> #include <shlwapi.h>
#include <windows.h> #include <windows.h>
@@ -51,6 +53,7 @@
#include "games/sc/sc.h" #include "games/sc/sc.h"
#include "games/scotto/scotto.h" #include "games/scotto/scotto.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "games/sdvx/sdvx_live2d.h"
#include "games/shared/printer.h" #include "games/shared/printer.h"
#include "games/silentscope/silentscope.h" #include "games/silentscope/silentscope.h"
#include "games/mfc/mfc.h" #include "games/mfc/mfc.h"
@@ -70,6 +73,8 @@
#include "games/museca/museca.h" #include "games/museca/museca.h"
#include "hooks/avshook.h" #include "hooks/avshook.h"
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/asio_proxy.h"
#include "hooks/audio/backends/wasapi/downmix.h"
#include "hooks/debughook.h" #include "hooks/debughook.h"
#include "hooks/devicehook.h" #include "hooks/devicehook.h"
#include "hooks/graphics/nvenc_hook.h" #include "hooks/graphics/nvenc_hook.h"
@@ -95,8 +100,10 @@
#include "misc/sde.h" #include "misc/sde.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/notifications.h"
#include "overlay/windows/patch_manager.h" #include "overlay/windows/patch_manager.h"
#include "overlay/windows/iidx_seg.h" #include "overlay/windows/iidx_seg.h"
#include "overlay/windows/obs.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "rawinput/touch.h" #include "rawinput/touch.h"
#include "reader/reader.h" #include "reader/reader.h"
@@ -374,7 +381,7 @@ int main_implementation(int argc, char *argv[]) {
} }
if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) { if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) {
GRAPHICS_FORCE_SINGLE_ADAPTER = true; GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOW = true; GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
} }
if (options[launcher::Options::DXDisplayAdapter].is_active() && if (options[launcher::Options::DXDisplayAdapter].is_active() &&
options[launcher::Options::DXDisplayAdapter].value_uint32() != D3DADAPTER_DEFAULT) { options[launcher::Options::DXDisplayAdapter].value_uint32() != D3DADAPTER_DEFAULT) {
@@ -492,6 +499,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_SDVXAsioDriver].is_active()) { if (options[launcher::Options::spice2x_SDVXAsioDriver].is_active()) {
games::sdvx::ASIO_DRIVER = options[launcher::Options::spice2x_SDVXAsioDriver].value_text(); games::sdvx::ASIO_DRIVER = options[launcher::Options::spice2x_SDVXAsioDriver].value_text();
} }
if (options[launcher::Options::SDVXAsioTwoChannel].value_bool()) {
WrappedAsio::STEREO_DOWNMIX = WrappedAsio::StereoDownmix::Front;
}
if (options[launcher::Options::spice2x_SDVXSubPos].is_active()) { if (options[launcher::Options::spice2x_SDVXSubPos].is_active()) {
auto txt = options[launcher::Options::spice2x_SDVXSubPos].value_text(); auto txt = options[launcher::Options::spice2x_SDVXSubPos].value_text();
if (txt == "top") { if (txt == "top") {
@@ -627,7 +637,7 @@ int main_implementation(int argc, char *argv[]) {
} }
if (options[launcher::Options::PopnNoSub].value_bool()) { if (options[launcher::Options::PopnNoSub].value_bool()) {
GRAPHICS_FORCE_SINGLE_ADAPTER = true; GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOW = true; GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
} }
if (options[launcher::Options::PopnSubMonitorOverride].is_active()) { if (options[launcher::Options::PopnSubMonitorOverride].is_active()) {
sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text(); sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text();
@@ -635,6 +645,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::PopnNativeTouch].value_bool()) { if (options[launcher::Options::PopnNativeTouch].value_bool()) {
games::popn::NATIVE_TOUCH = true; games::popn::NATIVE_TOUCH = true;
} }
if (options[launcher::Options::PopnSubRedraw].value_bool()) {
SUBSCREEN_FORCE_REDRAW = true;
}
if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) { if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) {
attach_mga = true; attach_mga = true;
} }
@@ -644,12 +657,22 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::GitaDoraCabinetType].is_active()) { if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32(); games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
} }
// gitadora arena layout
if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool()) { if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool()) {
// for full screen games::gitadora::ARENA_WINDOW_COUNT = 1;
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
// for windowed
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
} }
if (options[launcher::Options::GitaDoraArenaWindowLayout].is_active()) {
const auto window_count = options[launcher::Options::GitaDoraArenaWindowLayout].value_text();
if (window_count == "1") {
games::gitadora::ARENA_WINDOW_COUNT = 1;
} else if (window_count == "2") {
games::gitadora::ARENA_WINDOW_COUNT = 2;
} else if (window_count == "4") {
games::gitadora::ARENA_WINDOW_COUNT = 4;
}
}
if (options[launcher::Options::GitaDoraWailHold].is_active()) { if (options[launcher::Options::GitaDoraWailHold].is_active()) {
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32(); socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
} }
@@ -666,6 +689,12 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) { if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) {
games::gitadora::SUBSCREEN_OVERLAY_SIZE = options[launcher::Options::GitaDoraSubOverlaySize].value_text(); games::gitadora::SUBSCREEN_OVERLAY_SIZE = options[launcher::Options::GitaDoraSubOverlaySize].value_text();
} }
if (options[launcher::Options::GitaDoraArenaAsioDriver].is_active()) {
games::gitadora::ASIO_DRIVER = options[launcher::Options::GitaDoraArenaAsioDriver].value_text();
}
if (options[launcher::Options::GitaDoraArenaRealtekAccess].value_bool()) {
games::gitadora::ALLOW_REALTEK_AUDIO = true;
}
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) { if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
attach_nostalgia = true; attach_nostalgia = true;
} }
@@ -686,6 +715,7 @@ int main_implementation(int argc, char *argv[]) {
} }
if (options[launcher::Options::GitaDoraTwoChannelAudio].value_bool()) { if (options[launcher::Options::GitaDoraTwoChannelAudio].value_bool()) {
games::gitadora::TWOCHANNEL = true; games::gitadora::TWOCHANNEL = true;
WrappedAsio::STEREO_DOWNMIX = WrappedAsio::StereoDownmix::Center;
} }
if (options[launcher::Options::GitaDoraLefty].is_active()) { if (options[launcher::Options::GitaDoraLefty].is_active()) {
const auto text = options[launcher::Options::GitaDoraLefty].value_text(); const auto text = options[launcher::Options::GitaDoraLefty].value_text();
@@ -1017,6 +1047,20 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::SDVXForce720p].value_bool()) { if (options[launcher::Options::SDVXForce720p].value_bool()) {
GRAPHICS_SDVX_FORCE_720 = true; GRAPHICS_SDVX_FORCE_720 = true;
} }
#ifdef SPICE64
// only the Live2D-capable SDVX versions are 64-bit, so this whole feature is
// gated out of 32-bit builds and only armed for SDVX (model KFC).
if (avs::game::is_model("KFC")) {
auto live2d = options[launcher::Options::SDVXDisableLive2D].value_text();
if (live2d == "always") {
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Always;
} else if (live2d == "ingame") {
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::InGame;
// scene detection runs independently of the SDVX game module
games::sdvx::live2d_scene_detection_init();
}
}
#endif // SPICE64
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) { if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
rawinput::touch::INVERTED = true; rawinput::touch::INVERTED = true;
} }
@@ -1043,7 +1087,7 @@ int main_implementation(int argc, char *argv[]) {
// do not explicitly set GRAPHICS_9_ON_12_STATE to default here; must respect // do not explicitly set GRAPHICS_9_ON_12_STATE to default here; must respect
// legacy Graphics9On12 option from above if set // legacy Graphics9On12 option from above if set
} }
if (options[launcher::Options::NoLegacy].value_bool() && !cfg_run) { if (options[launcher::Options::NoLegacy].value_bool() && !cfg_run && !cfg::CONFIGURATOR_STANDALONE) {
rawinput::NOLEGACY = true; rawinput::NOLEGACY = true;
} }
if (options[launcher::Options::RichPresence].value_bool()) { if (options[launcher::Options::RichPresence].value_bool()) {
@@ -1078,6 +1122,38 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_DisableVolumeHook].value_bool()) { if (options[launcher::Options::spice2x_DisableVolumeHook].value_bool()) {
hooks::audio::VOLUME_HOOK_ENABLED = false; hooks::audio::VOLUME_HOOK_ENABLED = false;
} }
if (options[launcher::Options::DownmixAudioToStereo].is_active()) {
auto &name = options[launcher::Options::DownmixAudioToStereo].value_text();
hooks::audio::DOWNMIX_ALGORITHM = hooks::audio::Downmix::name_to_algorithm(name.c_str());
}
if (options[launcher::Options::AsioDownmixToStereo].is_active()) {
// routes the selected pair onto the device's 2.0 front output; a non-None selection
// implies force-two-channel (see WrappedAsio::force_two_channels)
auto &name = options[launcher::Options::AsioDownmixToStereo].value_text();
WrappedAsio::STEREO_DOWNMIX = WrappedAsio::name_to_stereo_downmix(name.c_str());
}
if (options[launcher::Options::VolumeBoost].is_active()) {
const double decibels = std::strtod(
options[launcher::Options::VolumeBoost].value_text().c_str(), nullptr);
if (decibels > 0.0) {
hooks::audio::VOLUME_BOOST = (float) std::pow(10.0, decibels / 20.0);
}
}
if (options[launcher::Options::AudioResample].is_active()) {
const uint32_t rate = options[launcher::Options::AudioResample].value_uint32();
if (rate > 0) {
hooks::audio::RESAMPLE_RATE = rate;
}
}
if (options[launcher::Options::AudioExclusiveBuffer].is_active()) {
const uint32_t ms = options[launcher::Options::AudioExclusiveBuffer].value_uint32();
if (ms > 0) {
hooks::audio::EXCLUSIVE_BUFFER_MS = ms;
}
}
if (options[launcher::Options::AudioShared].value_bool()) {
hooks::audio::WASAPI_COMPATIBILITY_MODE = true;
}
if (options[launcher::Options::AudioBackend].is_active()) { if (options[launcher::Options::AudioBackend].is_active()) {
auto &name = options[launcher::Options::AudioBackend].value_text(); auto &name = options[launcher::Options::AudioBackend].value_text();
@@ -1128,7 +1204,20 @@ int main_implementation(int argc, char *argv[]) {
overlay::AUTO_SHOW_FPS = true; overlay::AUTO_SHOW_FPS = true;
} }
if (options[launcher::Options::spice2x_FpsOpposite].value_bool()) { if (options[launcher::Options::spice2x_FpsOpposite].value_bool()) {
overlay::FPS_SHOULD_FLIP = true; // deprecated flag: equivalent to anchoring the FPS window top-left
overlay::FPS_LOCATION = overlay::FpsLocation::TopLeft;
}
if (options[launcher::Options::FpsLocation].is_active()) {
const auto txt = options[launcher::Options::FpsLocation].value_text();
if (txt == "topright") {
overlay::FPS_LOCATION = overlay::FpsLocation::TopRight;
} else if (txt == "topleft") {
overlay::FPS_LOCATION = overlay::FpsLocation::TopLeft;
} else if (txt == "bottomleft") {
overlay::FPS_LOCATION = overlay::FpsLocation::BottomLeft;
} else if (txt == "bottomright") {
overlay::FPS_LOCATION = overlay::FpsLocation::BottomRight;
}
} }
if (options[launcher::Options::spice2x_SubScreenAutoShow].value_bool()) { if (options[launcher::Options::spice2x_SubScreenAutoShow].value_bool()) {
overlay::AUTO_SHOW_SUBSCREEN = true; overlay::AUTO_SHOW_SUBSCREEN = true;
@@ -1168,6 +1257,22 @@ int main_implementation(int argc, char *argv[]) {
GRAPHICS_WINDOW_ALWAYS_ON_TOP = options[launcher::Options::spice2x_WindowAlwaysOnTop].value_bool(); GRAPHICS_WINDOW_ALWAYS_ON_TOP = options[launcher::Options::spice2x_WindowAlwaysOnTop].value_bool();
GRAPHICS_WINDOW_BACKBUFFER_SCALE = options[launcher::Options::WindowForceScaling].value_bool(); GRAPHICS_WINDOW_BACKBUFFER_SCALE = options[launcher::Options::WindowForceScaling].value_bool();
GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS = options[launcher::Options::WindowDisableRoundedCorners].value_bool(); GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS = options[launcher::Options::WindowDisableRoundedCorners].value_bool();
if (options[launcher::Options::GitaDoraWindowedMainMonitor].is_active()) {
GRAPHICS_GITADORA_MAIN_MONITOR =
options[launcher::Options::GitaDoraWindowedMainMonitor].value_text();
}
if (options[launcher::Options::GitaDoraWindowedLeftMonitor].is_active()) {
GRAPHICS_GITADORA_LEFT_MONITOR =
options[launcher::Options::GitaDoraWindowedLeftMonitor].value_text();
}
if (options[launcher::Options::GitaDoraWindowedRightMonitor].is_active()) {
GRAPHICS_GITADORA_RIGHT_MONITOR =
options[launcher::Options::GitaDoraWindowedRightMonitor].value_text();
}
if (options[launcher::Options::GitaDoraWindowedSmallMonitor].is_active()) {
GRAPHICS_GITADORA_SMALL_MONITOR =
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text();
}
// IIDX/SDVX Windowed Subscreen // IIDX/SDVX Windowed Subscreen
if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) { if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) {
@@ -1344,6 +1449,30 @@ int main_implementation(int argc, char *argv[]) {
timeutils::TIMER_HACKS_DISABLE = true; timeutils::TIMER_HACKS_DISABLE = true;
} }
if (options[launcher::Options::CfgForceSoftwareRender].value_bool()) {
cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true;
}
// OBS WebSocket overlay settings
if (options[launcher::Options::OBSWebSocketEnabled].value_bool()) {
overlay::windows::OBS_CONTROL_ENABLED = true;
}
if (options[launcher::Options::OBSWebSocketHost].is_active()) {
overlay::windows::OBS_CONTROL_HOST = options[launcher::Options::OBSWebSocketHost].value_text();
}
if (options[launcher::Options::OBSWebSocketPort].is_active()) {
const auto obs_port = options[launcher::Options::OBSWebSocketPort].value_uint32();
if (obs_port > 0 && obs_port <= 65535) {
overlay::windows::OBS_CONTROL_PORT = static_cast<uint16_t>(obs_port);
}
}
if (options[launcher::Options::OBSWebSocketPassword].is_active()) {
overlay::windows::OBS_CONTROL_PASSWORD = options[launcher::Options::OBSWebSocketPassword].value_text();
}
if (options[launcher::Options::OBSWebSocketDebug].value_bool()) {
overlay::windows::OBS_CONTROL_DEBUG = true;
}
// API debugging // API debugging
if (api_debug && !cfg::CONFIGURATOR_STANDALONE) { if (api_debug && !cfg::CONFIGURATOR_STANDALONE) {
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty); API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
@@ -1398,17 +1527,22 @@ int main_implementation(int argc, char *argv[]) {
#else #else
#ifdef SPICE64 #ifdef SPICE64
log_info("launcher", "SpiceTools Bootstrap (x64) (spice2x)"); log_info("launcher", "SpiceTools Bootstrap (x64) (spice2x)");
#elif SPICE32_LARGE_ADDRESS_AWARE
log_info("launcher", "SpiceTools Bootstrap (x32 - Large Address Aware) (spice2x)");
#else #else
log_info("launcher", "SpiceTools Bootstrap (x32) (spice2x)"); // spice.exe and spice_laa.exe share the same compiled objects; the only
// difference is the large-address-aware bit set at link time. detect it
// at runtime so the log line stays accurate without a separate compile.
if (sysutils::is_large_address_aware()) {
log_info("launcher", "SpiceTools Bootstrap (x32 - Large Address Aware) (spice2x)");
} else {
log_info("launcher", "SpiceTools Bootstrap (x32) (spice2x)");
}
#endif #endif
#endif #endif
log_info("launcher", "{}", VERSION_STRING); log_info("launcher", "{}", VERSION_STRING);
// note: distribution of modified version of this software without providing source is GPLv3 license violation. // note: distribution of modified version of this software without providing source is GPLv3 license violation.
log_info("launcher", "spice2x is free & open source; if you paid money for it, you got scammed"); log_info("launcher", "spice2x is free & open source software; if you paid for it, you were scammed");
log_info("launcher", "visit https://spice2x.github.io to download the latest version for free"); log_info("launcher", "visit https://spice2x.github.io to download the latest version for free");
// log command line arguments // log command line arguments
@@ -1451,8 +1585,19 @@ int main_implementation(int argc, char *argv[]) {
// print out conflicts // print out conflicts
size_t conflicts = 0; size_t conflicts = 0;
for (const auto &option : options) { for (size_t i = 0; i < options.size(); i++) {
if (option.conflicting && option.get_definition().type != OptionType::Bool) { // InjectHook / EarlyInjectHook accept multiple values, so command line and
// spicecfg entries are merged rather than conflicting; don't warn about them
if (i == (size_t) launcher::Options::InjectHook ||
i == (size_t) launcher::Options::EarlyInjectHook) {
continue;
}
const auto &option = options[i];
// ignore Boolean values
if (option.get_definition().type == OptionType::Bool) {
continue;
}
if (option.conflicting) {
conflicts += 1; conflicts += 1;
const auto& value = option.get_definition().sensitive ? "*****" : option.value; const auto& value = option.get_definition().sensitive ? "*****" : option.value;
if (launcher::USE_CMD_OVERRIDE) { if (launcher::USE_CMD_OVERRIDE) {
@@ -1493,6 +1638,13 @@ int main_implementation(int argc, char *argv[]) {
"!!! errors and loss of functionality. !!!\n" "!!! errors and loss of functionality. !!!\n"
"!!! !!!\n" "!!! !!!\n"
); );
deferredlogs::defer_error_messages({
"-exec option disables all game-specific hooks and I/O emulation",
" you must combine this with other flags or have appropriate arcade hardware",
" this also turns off many auto-troubleshooter checks",
" which means that this analysis will be incomplete",
" you need to manually check the logs for failures"
});
} }
if (launcher::signal::DISABLE && !cfg::CONFIGURATOR_STANDALONE) { if (launcher::signal::DISABLE && !cfg::CONFIGURATOR_STANDALONE) {
@@ -1542,12 +1694,23 @@ int main_implementation(int argc, char *argv[]) {
GRAPHICS_FS_ORIENTATION_SWAP = true; GRAPHICS_FS_ORIENTATION_SWAP = true;
} }
// for cab usage - set environment variables (outside of -iidx module)
if (games::iidx::DISABLE_CAMS.has_value() && if (games::iidx::DISABLE_CAMS.has_value() &&
games::iidx::DISABLE_CAMS.value() && games::iidx::DISABLE_CAMS.value() &&
!cfg::CONFIGURATOR_STANDALONE) { !cfg::CONFIGURATOR_STANDALONE) {
log_misc("launcher", "CONNECT_CAMERA env var set to 0"); log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0"); SetEnvironmentVariable("CONNECT_CAMERA", "0");
} }
if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() &&
games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" &&
!cfg::CONFIGURATOR_STANDALONE) {
log_info(
"launcher::iidx",
"using user-supplied \"{}\" for SOUND_OUTPUT_DEVICE",
games::iidx::SOUND_OUTPUT_DEVICE.value());
SetEnvironmentVariable("SOUND_OUTPUT_DEVICE", games::iidx::SOUND_OUTPUT_DEVICE.value().c_str());
}
// deleted options // deleted options
if (options[launcher::Options::OpenKFControl].value_bool() && !cfg::CONFIGURATOR_STANDALONE) { if (options[launcher::Options::OpenKFControl].value_bool() && !cfg::CONFIGURATOR_STANDALONE) {
@@ -2467,6 +2630,24 @@ int main_implementation(int argc, char *argv[]) {
// eamuse init // eamuse init
eamuse_autodetect_game(); eamuse_autodetect_game();
// notification position: apply per-game default first, then the explicit
// user option (if set) wins over the default.
overlay::notifications::apply_game_default_position(eamuse_get_game());
if (options[launcher::Options::NotificationPosition].is_active()) {
const auto txt = options[launcher::Options::NotificationPosition].value_text();
if (txt == "topleft") {
overlay::notifications::POSITION = overlay::notifications::Position::TopLeft;
} else if (txt == "topright") {
overlay::notifications::POSITION = overlay::notifications::Position::TopRight;
} else if (txt == "bottomleft") {
overlay::notifications::POSITION = overlay::notifications::Position::BottomLeft;
} else if (txt == "bottomright") {
overlay::notifications::POSITION = overlay::notifications::Position::BottomRight;
} else if (txt == "off") {
overlay::notifications::ENABLED = false;
}
}
// unis device hook // unis device hook
unisintrhook_init(); unisintrhook_init();
File diff suppressed because it is too large Load Diff
+36 -6
View File
@@ -20,6 +20,8 @@ namespace launcher {
Player2Card, Player2Card,
Player1PinMacro, Player1PinMacro,
Player2PinMacro, Player2PinMacro,
AutoPinMacroTrigger0,
AutoPinMacroTrigger1,
WindowedMode, WindowedMode,
InjectHook, InjectHook,
EarlyInjectHook, EarlyInjectHook,
@@ -38,6 +40,7 @@ namespace launcher {
spice2x_Dx9On12, spice2x_Dx9On12,
NoLegacy, NoLegacy,
RichPresence, RichPresence,
DiscordAppID,
SmartEAmusement, SmartEAmusement,
EAmusementMaintenance, EAmusementMaintenance,
spice2x_EAmusementMaintenance, spice2x_EAmusementMaintenance,
@@ -51,8 +54,10 @@ namespace launcher {
VREnable, VREnable,
DisableOverlay, DisableOverlay,
OverlayScaling, OverlayScaling,
NotificationPosition,
spice2x_FpsAutoShow, spice2x_FpsAutoShow,
spice2x_FpsOpposite, spice2x_FpsOpposite,
FpsLocation,
spice2x_SubScreenAutoShow, spice2x_SubScreenAutoShow,
spice2x_IOPanelAutoShow, spice2x_IOPanelAutoShow,
spice2x_KeypadAutoShow, spice2x_KeypadAutoShow,
@@ -91,6 +96,8 @@ namespace launcher {
spice2x_SDVXDigitalKnobSensitivity, spice2x_SDVXDigitalKnobSensitivity,
SDVXDigitalKnobSocd, SDVXDigitalKnobSocd,
spice2x_SDVXAsioDriver, spice2x_SDVXAsioDriver,
SDVXAsioTwoChannel,
SDVXDisableLive2D,
spice2x_SDVXSubPos, spice2x_SDVXSubPos,
SDVXSubMonitorOverride, SDVXSubMonitorOverride,
LoadDDRModule, LoadDDRModule,
@@ -102,15 +109,19 @@ namespace launcher {
PopnNoSub, PopnNoSub,
PopnSubMonitorOverride, PopnSubMonitorOverride,
PopnNativeTouch, PopnNativeTouch,
PopnSubRedraw,
LoadHelloPopnMusicModule, LoadHelloPopnMusicModule,
LoadGitaDoraModule, LoadGitaDoraModule,
GitaDoraTwoChannelAudio, GitaDoraTwoChannelAudio,
GitaDoraCabinetType, GitaDoraCabinetType,
GitaDoraArenaSingleWindow,
GitaDoraLefty, GitaDoraLefty,
GitaDoraWailHold, GitaDoraWailHold,
GitaDoraPickAlgo, GitaDoraPickAlgo,
GitaDoraSubOverlaySize, GitaDoraSubOverlaySize,
GitaDoraArenaSingleWindow,
GitaDoraArenaWindowLayout,
GitaDoraArenaAsioDriver,
GitaDoraArenaRealtekAccess,
LoadJubeatModule, LoadJubeatModule,
LoadReflecBeatModule, LoadReflecBeatModule,
LoadShogikaiModule, LoadShogikaiModule,
@@ -190,10 +201,17 @@ namespace launcher {
spice2x_NvapiProfile, spice2x_NvapiProfile,
DisableAudioHooks, DisableAudioHooks,
spice2x_DisableVolumeHook, spice2x_DisableVolumeHook,
AudioShared,
spice2x_LowLatencySharedAudio,
AudioBackend, AudioBackend,
AsioDriverId, AsioDriverId,
AsioDriverName, AsioDriverName,
AudioDummy, AudioDummy,
DownmixAudioToStereo,
VolumeBoost,
AudioResample,
AudioExclusiveBuffer,
AsioDownmixToStereo,
DelayBy5Seconds, DelayBy5Seconds,
spice2x_DelayByNSeconds, spice2x_DelayByNSeconds,
LoadStubs, LoadStubs,
@@ -203,7 +221,6 @@ namespace launcher {
LogLevel, LogLevel,
EAAutomap, EAAutomap,
EANetdump, EANetdump,
DiscordAppID,
BlockingLogger, BlockingLogger,
DebugCreateFile, DebugCreateFile,
VerboseGraphicsLogging, VerboseGraphicsLogging,
@@ -235,6 +252,10 @@ namespace launcher {
spice2x_WindowAlwaysOnTop, spice2x_WindowAlwaysOnTop,
WindowForceScaling, WindowForceScaling,
WindowDisableRoundedCorners, WindowDisableRoundedCorners,
GitaDoraWindowedMainMonitor,
GitaDoraWindowedLeftMonitor,
GitaDoraWindowedRightMonitor,
GitaDoraWindowedSmallMonitor,
spice2x_IIDXWindowedSubscreenSize, spice2x_IIDXWindowedSubscreenSize,
spice2x_IIDXWindowedSubscreenPosition, spice2x_IIDXWindowedSubscreenPosition,
IIDXWindowedSubscreenBorderless, IIDXWindowedSubscreenBorderless,
@@ -255,9 +276,6 @@ namespace launcher {
IIDXSubMonitorOverride, IIDXSubMonitorOverride,
spice2x_IIDXEmulateSubscreenKeypadTouch, spice2x_IIDXEmulateSubscreenKeypadTouch,
spice2x_AutoCard, spice2x_AutoCard,
AutoPinMacroTrigger0,
AutoPinMacroTrigger1,
spice2x_LowLatencySharedAudio,
spice2x_TapeLedAlgorithm, spice2x_TapeLedAlgorithm,
spice2x_NoNVAPI, spice2x_NoNVAPI,
spice2x_NoD3D9DeviceHook, spice2x_NoD3D9DeviceHook,
@@ -288,11 +306,21 @@ namespace launcher {
DisableHighResTimer, DisableHighResTimer,
EnableICMPHook, EnableICMPHook,
AutoElevate, AutoElevate,
CfgForceSoftwareRender,
OBSWebSocketEnabled,
OBSWebSocketHost,
OBSWebSocketPort,
OBSWebSocketPassword,
OBSWebSocketDebug
}; };
enum class OptionsCategory { enum class OptionsCategory {
Everything, Everything,
Basic, GameOptions,
Display,
Audio,
Network,
Overlay,
Advanced, Advanced,
Dev, Dev,
API API
@@ -302,7 +330,9 @@ namespace launcher {
extern bool USE_CMD_OVERRIDE; extern bool USE_CMD_OVERRIDE;
const std::vector<std::string> &get_categories(Options::OptionsCategory category); const std::vector<std::string> &get_categories(Options::OptionsCategory category);
const std::vector<std::string> &get_quick_setting_categories();
const std::vector<OptionDefinition> &get_option_definitions(); const std::vector<OptionDefinition> &get_option_definitions();
void validate_option_categories();
std::unique_ptr<std::vector<Option>> parse_options(int argc, char *argv[]); std::unique_ptr<std::vector<Option>> parse_options(int argc, char *argv[]);
std::vector<Option> merge_options(const std::vector<Option> &options, const std::vector<Option> &overrides); std::vector<Option> merge_options(const std::vector<Option> &options, const std::vector<Option> &overrides);
+5
View File
@@ -8,6 +8,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/logging.h" #include "util/logging.h"
@@ -26,6 +27,10 @@ namespace launcher {
sdk::fini_sdk_modules(); sdk::fini_sdk_modules();
// stop dx11 background workers (poll thread, LDR notification)
// before anything else, so they can't race against the teardown.
graphics_d3d11_shutdown();
// reset monitor settings // reset monitor settings
reset_monitor_on_exit(); reset_monitor_on_exit();
+4
View File
@@ -14,6 +14,7 @@
#include "util/detour.h" #include "util/detour.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/memutils.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "logger.h" #include "logger.h"
@@ -175,6 +176,9 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
log_warning("signal", "minidump creation function not available, skipping"); log_warning("signal", "minidump creation function not available, skipping");
} }
// dump memory information
memutils::show_available_memory();
// this will stall all UI threads for this process // this will stall all UI threads for this process
show_popup_for_crash(); show_popup_for_crash();
+22
View File
@@ -1327,6 +1327,28 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
easywsclient (MIT)
-------------------------------------------
Copyright (c) 2012 Dhruv Matani, Daniel Baird
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Contributions Contributions
------------------------------------------- -------------------------------------------
cardio - Felix - MIT License cardio - Felix - MIT License
+38 -2
View File
@@ -14,14 +14,16 @@
#include "util/time.h" #include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/notifications.h"
#include "bt5api.h" #include "bt5api.h"
// state // state
static constexpr double NOTIFICATION_THROTTLE_SECONDS = 3.0;
static bool CARD_INSERT[2] = {false, false}; static bool CARD_INSERT[2] = {false, false};
static double CARD_INSERT_TIME[2] = {0, 0}; static double CARD_INSERT_TIME[2] = {0, 0};
static double CARD_INSERT_TIMEOUT = 2.0; static double CARD_INSERT_TIMEOUT = 2.0;
static char CARD_INSERT_UID[2][8]; static char CARD_INSERT_UID[2][8] = {{0}, {0}};
static char CARD_INSERT_UID_ENABLE[2] = {false, false}; static char CARD_INSERT_UID_ENABLE[2] = {false, false};
static int COIN_STOCK = 0; static int COIN_STOCK = 0;
static bool COIN_BLOCK = false; static bool COIN_BLOCK = false;
@@ -119,6 +121,12 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index
"{} card override contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)", "{} card override contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
card_override, n); card_override, n);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Error,
fmt::format("eamuse.card_override_error.p{}", index + 1),
NOTIFICATION_THROTTLE_SECONDS,
fmt::format("[P{}] invalid card override", index + 1));
return false; return false;
} }
} }
@@ -149,6 +157,11 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
std::ifstream f(path); std::ifstream f(path);
if (!f) { if (!f) {
log_warning("eamuse", "{} can not be opened!", path); log_warning("eamuse", "{} can not be opened!", path);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Error,
fmt::format("eamuse.card_file_error.p{}", index + 1),
NOTIFICATION_THROTTLE_SECONDS,
fmt::format("[P{}] can't open card file", index + 1));
return false; return false;
} }
@@ -160,6 +173,12 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
// check size // check size
if (length < 16) { if (length < 16) {
log_warning("eamuse", "{} is too small (must be at least 16 characters)", path); log_warning("eamuse", "{} is too small (must be at least 16 characters)", path);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Error,
fmt::format("eamuse.card_file_error.p{}", index + 1),
NOTIFICATION_THROTTLE_SECONDS,
fmt::format("[P{}] card file error", index + 1));
return false; return false;
} }
@@ -179,6 +198,11 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
"{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)", "{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
path, n); path, n);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Error,
fmt::format("eamuse.card_file_error.p{}", index + 1),
NOTIFICATION_THROTTLE_SECONDS,
fmt::format("[P{}] card file error", index + 1));
return false; return false;
} }
} }
@@ -251,8 +275,16 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
auto offset = unit_id * games::KeypadButtons::Size; auto offset = unit_id * games::KeypadButtons::Size;
if ((CARD_INSERT[index] && fabs(get_performance_seconds() - CARD_INSERT_TIME[index]) < CARD_INSERT_TIMEOUT) if ((CARD_INSERT[index] && fabs(get_performance_seconds() - CARD_INSERT_TIME[index]) < CARD_INSERT_TIMEOUT)
|| GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::InsertCard + offset))) { || GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::InsertCard + offset))) {
log_info("eamuse", "[P{}] Card insert on reader (total active count: {})", unit_id+1, active_count); log_info("eamuse", "[P{}] Card insert on reader (total active count: {})", unit_id+1, active_count);
CARD_INSERT[index] = false; CARD_INSERT[index] = false;
overlay::notifications::add_throttled(
overlay::notifications::Severity::Info,
fmt::format("eamuse.card_inserted.p{}", unit_id + 1),
NOTIFICATION_THROTTLE_SECONDS,
fmt::format("[P{}] card inserted", unit_id + 1));
return true; return true;
} }
@@ -394,6 +426,10 @@ void eamuse_pin_macro_start_thread() {
log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1); log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1);
} }
if (key_press || auto_request) { if (key_press || auto_request) {
overlay::notifications::add(
overlay::notifications::Severity::Info,
fmt::format("[P{}] PIN macro fired ({})",
unit + 1, auto_request ? "auto" : "manual"));
active_unit = unit; active_unit = unit;
// Reset key index // Reset key index
pin_index[unit] = 0; pin_index[unit] = 0;
@@ -595,7 +631,7 @@ void eamuse_update_keypad_bindings() {
KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME); KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
} }
std::string eamuse_get_game() { const std::string &eamuse_get_game() {
return EAMUSE_GAME_NAME; return EAMUSE_GAME_NAME;
} }
+1 -1
View File
@@ -78,7 +78,7 @@ bool eamuse_keypad_state_naive();
void eamuse_set_game(std::string game); void eamuse_set_game(std::string game);
std::string eamuse_get_game(); const std::string &eamuse_get_game();
int eamuse_get_game_keypads(); int eamuse_get_game_keypads();
int eamuse_get_game_keypads_name(); int eamuse_get_game_keypads_name();
+1 -1
View File
@@ -416,7 +416,7 @@ namespace wintouchemu {
// same as iidx case above // same as iidx case above
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen"); log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
USE_MOUSE = true; USE_MOUSE = true;
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOW) { } else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen"); log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
USE_MOUSE = true; USE_MOUSE = true;
} else { } else {
+72 -12
View File
@@ -1,7 +1,10 @@
#include "extensions.h" #include "extensions.h"
#include <algorithm>
#include <cmath> #include <cmath>
#include "external/imgui/imgui.h" #include "external/imgui/imgui.h"
#include "external/imgui/imgui_internal.h"
#include "overlay/overlay.h"
namespace ImGui { namespace ImGui {
@@ -9,6 +12,15 @@ namespace ImGui {
const auto fg = ImVec4(0.910f, 0.914f, 0.922f, 1.0f); const auto fg = ImVec4(0.910f, 0.914f, 0.922f, 1.0f);
const auto bg = ImVec4(0.192f, 0.212f, 0.220f, 1.0f); const auto bg = ImVec4(0.192f, 0.212f, 0.220f, 1.0f);
// FramePadding shared by the config tab bar and its items so the bar height
// matches the padded tabs; gives the labels a bit more breathing room.
static ImVec2 PaddedTabFramePadding() {
const ImVec2 base = ImGui::GetStyle().FramePadding;
return ImVec2(
base.x + overlay::apply_scaling(10.0f),
base.y + overlay::apply_scaling(1.0f));
}
void HelpTooltip(const char* desc) { void HelpTooltip(const char* desc) {
ImGui::PushStyleColor(ImGuiCol_Border, bg); ImGui::PushStyleColor(ImGuiCol_Border, bg);
ImGui::PushStyleColor(ImGuiCol_BorderShadow, bg); ImGui::PushStyleColor(ImGuiCol_BorderShadow, bg);
@@ -66,7 +78,9 @@ namespace ImGui {
void DummyMarker() { void DummyMarker() {
// dummy marker that is the same width as HelpMarker/WarnMarker. // dummy marker that is the same width as HelpMarker/WarnMarker.
ImGui::Dummy(ImVec2(22, 0)); // "(?)" and "(!)" render to the same width, so calc it so the spacing
// tracks the current font size/scale instead of a fixed pixel count.
ImGui::Dummy(ImVec2(ImGui::CalcTextSize("(?)").x, 0));
} }
void Knob(float fraction, float size, float thickness, float pos_x, float pos_y) { void Knob(float fraction, float size, float thickness, float pos_x, float pos_y) {
@@ -148,6 +162,19 @@ namespace ImGui {
return clicked; return clicked;
} }
bool ColoredButton(const char* label, const ImVec4& base, const ImVec2& size) {
const auto brighten = [](const ImVec4& c, float d) {
return ImVec4((std::min)(c.x + d, 1.0f), (std::min)(c.y + d, 1.0f),
(std::min)(c.z + d, 1.0f), c.w);
};
ImGui::PushStyleColor(ImGuiCol_Button, base);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, brighten(base, 0.12f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, brighten(base, 0.22f));
const bool clicked = ImGui::Button(label, size);
ImGui::PopStyleColor(3);
return clicked;
}
bool ClearButton(const std::string& tooltip) { bool ClearButton(const std::string& tooltip) {
ImGui::PushID(tooltip.c_str()); ImGui::PushID(tooltip.c_str());
// same colors as a checkbox // same colors as a checkbox
@@ -164,17 +191,50 @@ namespace ImGui {
return clicked; return clicked;
} }
void InvisibleTableRowSelectable() { bool BeginPaddedTabItem(const char* label) {
ImGui::TableSetColumnIndex(0); // fixed uniform label width (scaled for DPI) so all tabs are equally sized;
ImGui::PushStyleColor(ImGuiCol_Header, 0); // wide enough to fit the longest label ("Controller")
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, 0); const float uniform_width = overlay::apply_scaling(70.0f);
ImGui::PushStyleColor(ImGuiCol_HeaderActive, 0);
ImGui::PushTabStop(false); // prevent tab navigation // ImGui renders tab labels left-aligned, so a forced width would leave short
ImGui::Selectable("##row", false, // labels hugging the left edge. Instead we pad the label with equal leading/
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap); // trailing spaces to reach a uniform width, which both keeps all tabs the same
ImGui::PopTabStop(); // size and centers the text. A stable "##" id suffix keeps each tab's identity
ImGui::PopStyleColor(3); // independent of the padding.
if (ImGui::IsItemHovered()) { const float space_w = ImGui::CalcTextSize(" ").x;
const float label_w = ImGui::CalcTextSize(label).x;
const int pad = (space_w > 0.0f)
? (int) ((uniform_width - label_w) * 0.5f / space_w)
: 0;
const std::string padded = (pad > 0)
? std::string(pad, ' ') + label + std::string(pad, ' ') + "##" + label
: std::string(label);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, PaddedTabFramePadding());
const bool open = ImGui::BeginTabItem(padded.c_str());
ImGui::PopStyleVar();
return open;
}
bool BeginPaddedTabBar(const char* str_id, ImGuiTabBarFlags flags) {
// push the same padding used by the tab items so the bar height matches
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, PaddedTabFramePadding());
const bool open = ImGui::BeginTabBar(str_id, flags);
ImGui::PopStyleVar();
return open;
}
void HighlightTableRowOnHover() {
// hit-test the row rect directly so the row layout and height are untouched
ImGuiTable *table = ImGui::GetCurrentTable();
if (table == nullptr) {
return;
}
if (ImGui::IsWindowHovered() &&
ImGui::IsMouseHoveringRect(
ImVec2(table->WorkRect.Min.x, table->RowPosY1),
ImVec2(table->WorkRect.Max.x, table->RowPosY2),
false)) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(200, 200, 200, 24)); ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(200, 200, 200, 24));
} }
} }
+11 -1
View File
@@ -20,5 +20,15 @@ namespace ImGui {
void TextTruncated(const std::string& p_text, float p_truncated_width); void TextTruncated(const std::string& p_text, float p_truncated_width);
bool DeleteButton(const std::string& tooltip); bool DeleteButton(const std::string& tooltip);
bool ClearButton(const std::string& tooltip); bool ClearButton(const std::string& tooltip);
void InvisibleTableRowSelectable(); void HighlightTableRowOnHover();
// a Button with the given base fill color; the hovered/active shades are
// derived by brightening the base. size defaults to auto (fit the label).
bool ColoredButton(const char* label, const ImVec4& base,
const ImVec2& size = ImVec2(0, 0));
// Config tab bar with extra label padding and uniform, centered tab widths.
// Wrap items in BeginPaddedTabItem between BeginPaddedTabBar/EndTabBar.
bool BeginPaddedTabBar(const char* str_id, ImGuiTabBarFlags flags = 0);
bool BeginPaddedTabItem(const char* label);
} }
+137 -61
View File
@@ -41,6 +41,8 @@ static INT64 g_TicksPerSecond = 0;
static ImGuiMouseCursor g_LastMouseCursor = ImGuiMouseCursor_COUNT; static ImGuiMouseCursor g_LastMouseCursor = ImGuiMouseCursor_COUNT;
static double g_LastMouseMovement = 0.f; static double g_LastMouseMovement = 0.f;
static bool g_MouseCursorAutoHide = false; static bool g_MouseCursorAutoHide = false;
static float g_DisplaySizeOverrideW = 0.0f;
static float g_DisplaySizeOverrideH = 0.0f;
constexpr size_t VKEY_MAX = 255; constexpr size_t VKEY_MAX = 255;
static std::array<bool, VKEY_MAX> g_KeysDown; static std::array<bool, VKEY_MAX> g_KeysDown;
@@ -179,12 +181,25 @@ void ImGui_ImplSpice_Shutdown() {
void ImGui_ImplSpice_UpdateDisplaySize() { void ImGui_ImplSpice_UpdateDisplaySize() {
// backends that render to a surface different from the window's client
// area (e.g. dx11 swapchain backbuffer) can override the size here so
// ImGui's viewport matches the actual render target.
if (g_DisplaySizeOverrideW > 0.0f && g_DisplaySizeOverrideH > 0.0f) {
ImGui::GetIO().DisplaySize = ImVec2(g_DisplaySizeOverrideW, g_DisplaySizeOverrideH);
return;
}
// get display size // get display size
RECT rect; RECT rect;
::GetClientRect(g_hWnd, &rect); ::GetClientRect(g_hWnd, &rect);
ImGui::GetIO().DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top)); ImGui::GetIO().DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top));
} }
void ImGui_ImplSpice_SetDisplaySizeOverride(float w, float h) {
g_DisplaySizeOverrideW = w;
g_DisplaySizeOverrideH = h;
}
bool ImGui_ImplSpice_UpdateMouseCursor() { bool ImGui_ImplSpice_UpdateMouseCursor() {
// check if cursor should be changed // check if cursor should be changed
@@ -195,7 +210,15 @@ bool ImGui_ImplSpice_UpdateMouseCursor() {
// update cursor // update cursor
ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor(); ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) {
// in auto-hide mode imgui owns cursor drawing, so the OS cursor must stay
// hidden. this function is also called from the game window's WM_SETCURSOR
// handler, which fires on every mouse move; without forcing it hidden here,
// the else branch below would set IDC_ARROW on the next mouse move once the
// overlay is closed. that arrow is conspicuous on games whose window class
// has hCursor==NULL (e.g. DDR), since those normally show no client-area
// cursor at all.
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor || g_MouseCursorAutoHide) {
// hide OS mouse cursor if imgui is drawing it or if it wants no cursor // hide OS mouse cursor if imgui is drawing it or if it wants no cursor
::SetCursor(nullptr); ::SetCursor(nullptr);
@@ -337,6 +360,52 @@ static void ImGui_ImplSpice_UpdateMousePos() {
} }
} }
// Decide whether ImGui should draw its software cursor this frame. Kept out of
// ImGui_ImplSpice_NewFrame so the visibility rules live in one place.
//
// Only matters when ImGui owns cursor drawing (g_MouseCursorAutoHide, i.e. the
// game hid the OS cursor); otherwise the OS draws the cursor and
// io.MouseDrawCursor stays false throughout. In that mode:
// - overlay hidden -> never draw the cursor (input belongs to the game)
// - overlay shown -> visible immediately, auto-hide after 2s idle, reappear
// on any mouse movement / wheel / button
static void ImGui_ImplSpice_UpdateCursorVisibility(
bool overlay_visible,
const ImVec2 &old_mouse_pos,
const ImVec2 &new_mouse_pos,
long wheel_diff)
{
if (!g_MouseCursorAutoHide) {
return;
}
auto &io = ImGui::GetIO();
static bool overlay_visible_old = false;
const bool mouse_activity =
old_mouse_pos.x != new_mouse_pos.x ||
old_mouse_pos.y != new_mouse_pos.y ||
wheel_diff != 0 ||
g_MouseDown[ImGuiMouseButton_Left] ||
g_MouseDown[ImGuiMouseButton_Right] ||
g_MouseDown[ImGuiMouseButton_Middle];
if (!overlay_visible) {
// overlay hidden: the game owns the cursor, don't draw ImGui's
io.MouseDrawCursor = false;
} else if (!overlay_visible_old || mouse_activity) {
// overlay just opened, or the mouse moved: show the cursor and
// (re)start the idle timer
g_LastMouseMovement = get_performance_milliseconds();
io.MouseDrawCursor = true;
} else if ((get_performance_milliseconds() - g_LastMouseMovement) > 2000) {
// mouse idle for more than 2 seconds while the overlay is open: hide it
io.MouseDrawCursor = false;
}
overlay_visible_old = overlay_visible;
}
void ImGui_ImplSpice_NewFrame() { void ImGui_ImplSpice_NewFrame() {
// check if font is built // check if font is built
@@ -354,6 +423,14 @@ void ImGui_ImplSpice_NewFrame() {
const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_active(); const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_active();
const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE && overlay_visible; const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE && overlay_visible;
// when running as standalone spicecfg.exe the configurator window proc feeds
// ImGui directly via WM_KEY*/WM_MOUSE*/WM_MOUSEWHEEL messages. The per-frame
// rawinput device walk and the 256-VK scan below are pure CPU waste in that
// case (and the dominant per-frame cost on low-end PCs), so short-circuit
// them entirely. Rebind dialogs that explicitly need fresh device state call
// RI_MGR->devices_get_updated() themselves; see overlay/windows/config.cpp.
const bool drive_input_from_rawinput = !cfg::CONFIGURATOR_STANDALONE;
// remember old state // remember old state
std::array<BYTE, VKEY_MAX> KeysDownOld; std::array<BYTE, VKEY_MAX> KeysDownOld;
for (size_t i = 0; i < VKEY_MAX; i++) { for (size_t i = 0; i < VKEY_MAX; i++) {
@@ -363,11 +440,13 @@ void ImGui_ImplSpice_NewFrame() {
const auto MouseDownOld = g_MouseDown; const auto MouseDownOld = g_MouseDown;
// reset keys state // reset keys state
g_MouseDown.fill(false); if (drive_input_from_rawinput) {
g_KeysDown.fill(false); g_MouseDown.fill(false);
g_KeysDown.fill(false);
}
// apply windows mouse buttons // apply windows mouse buttons
if (accept_new_input) { if (accept_new_input && drive_input_from_rawinput) {
g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse(); g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse();
g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse(); g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse();
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0; g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
@@ -376,7 +455,7 @@ void ImGui_ImplSpice_NewFrame() {
// read new keys state // read new keys state
static long mouse_wheel_last = 0; static long mouse_wheel_last = 0;
long mouse_wheel = 0; long mouse_wheel = 0;
if (RI_MGR != nullptr) { if (drive_input_from_rawinput && RI_MGR != nullptr) {
auto devices = RI_MGR->devices_get(); auto devices = RI_MGR->devices_get();
for (auto &device : devices) { for (auto &device : devices) {
switch (device.type) { switch (device.type) {
@@ -432,41 +511,45 @@ void ImGui_ImplSpice_NewFrame() {
} }
} }
// process keyboard input from all keyboard collapsed into one state (g_KeysDown) // process keyboard input from all keyboards collapsed into one state (g_KeysDown).
for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) { // Skipped entirely in standalone configurator mode where Win32 messages already
const bool state = g_KeysDown[vKey]; // drive io.AddKeyEvent / io.AddInputCharacter directly.
const auto imgui_key = get_imgui_key(vKey); if (drive_input_from_rawinput) {
const auto changed = for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) {
(state && !KeysDownOld[vKey]) || const bool state = g_KeysDown[vKey];
(!state && KeysDownOld[vKey]); const auto imgui_key = get_imgui_key(vKey);
const auto changed =
(state && !KeysDownOld[vKey]) ||
(!state && KeysDownOld[vKey]);
if (imgui_key != ImGuiKey_None && changed) { if (imgui_key != ImGuiKey_None && changed) {
io.AddKeyEvent(imgui_key, state); io.AddKeyEvent(imgui_key, state);
log_debug("imgui_impl_spice", "vkey {:#x} added as navigation event, state: {}", static_cast<uint64_t>(vKey), state); log_debug("imgui_impl_spice", "vkey {:#x} added as navigation event, state: {}", static_cast<uint64_t>(vKey), state);
// mod key must also be processed separately // mod key must also be processed separately
const auto imgui_mod_key = get_imgui_mod_key(vKey); const auto imgui_mod_key = get_imgui_mod_key(vKey);
if (imgui_mod_key != ImGuiMod_None) { if (imgui_mod_key != ImGuiMod_None) {
io.AddKeyEvent(imgui_mod_key, state); io.AddKeyEvent(imgui_mod_key, state);
}
} }
}
// generate character input, but only if WM_CHAR didn't take over the functionality // generate character input, but only if WM_CHAR didn't take over the functionality
// only detecting rising edges here - this means holding a key won't work // only detecting rising edges here - this means holding a key won't work
// (it's better than repeating a character input every frame - cost we pay for providing input // (it's better than repeating a character input every frame - cost we pay for providing input
// on top of rawinput instead of WM_CHAR) // on top of rawinput instead of WM_CHAR)
if (!overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT && !KeysDownOld[vKey] && state) { if (!overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT && !KeysDownOld[vKey] && state) {
UCHAR buf[2]; UCHAR buf[2];
auto ret = ToAscii( auto ret = ToAscii(
static_cast<UINT>(vKey), static_cast<UINT>(vKey),
0, 0,
static_cast<const BYTE *>(KeysDownOld.data()), static_cast<const BYTE *>(KeysDownOld.data()),
reinterpret_cast<LPWORD>(buf), reinterpret_cast<LPWORD>(buf),
0); 0);
if (ret > 0) { if (ret > 0) {
for (int i = 0; i < ret; i++) { for (int i = 0; i < ret; i++) {
overlay::OVERLAY->input_char(buf[i]); overlay::OVERLAY->input_char(buf[i]);
log_debug("imgui_impl_spice", "vkey {:#x} inputted as character", vKey); log_debug("imgui_impl_spice", "vkey {:#x} inputted as character", vKey);
}
} }
} }
} }
@@ -475,42 +558,31 @@ void ImGui_ImplSpice_NewFrame() {
// set mouse wheel // set mouse wheel
long wheel_diff = mouse_wheel - mouse_wheel_last; long wheel_diff = mouse_wheel - mouse_wheel_last;
mouse_wheel_last = mouse_wheel; mouse_wheel_last = mouse_wheel;
if (wheel_diff != 0 && accept_new_input) { if (wheel_diff != 0 && accept_new_input && drive_input_from_rawinput) {
io.AddMouseWheelEvent(0, wheel_diff); io.AddMouseWheelEvent(0, wheel_diff);
} }
// update OS mouse position // update OS mouse position. The standalone configurator gets mouse position
// straight from WM_MOUSEMOVE, so skip the per-frame cursor poll there.
const auto old_mouse_pos = io.MousePos; const auto old_mouse_pos = io.MousePos;
if (accept_new_input) { if (accept_new_input && drive_input_from_rawinput) {
ImGui_ImplSpice_UpdateMousePos(); ImGui_ImplSpice_UpdateMousePos();
} }
const auto new_mouse_pos = io.MousePos; const auto new_mouse_pos = io.MousePos;
// update mouse buttons // update mouse buttons
// doing this after ImGui_ImplSpice_UpdateMousePos since it can set g_MouseDown for touch input // doing this after ImGui_ImplSpice_UpdateMousePos since it can set g_MouseDown for touch input
for (size_t i = 0; i < g_MouseDown.size(); i++) { if (drive_input_from_rawinput) {
if (MouseDownOld[i] != g_MouseDown[i]) { for (size_t i = 0; i < g_MouseDown.size(); i++) {
io.AddMouseButtonEvent(i, g_MouseDown[i]); if (MouseDownOld[i] != g_MouseDown[i]) {
log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]); io.AddMouseButtonEvent(i, g_MouseDown[i]);
log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]);
}
} }
} }
// automatically hide cursor // mouse cursor: visibility (overlay visibility + auto-hide)
if (g_MouseCursorAutoHide) { ImGui_ImplSpice_UpdateCursorVisibility(overlay_visible, old_mouse_pos, new_mouse_pos, wheel_diff);
if (old_mouse_pos.x != new_mouse_pos.x ||
old_mouse_pos.y != new_mouse_pos.y ||
wheel_diff != 0 ||
g_MouseDown[ImGuiMouseButton_Left] || g_MouseDown[ImGuiMouseButton_Right] || g_MouseDown[ImGuiMouseButton_Middle]) {
// mouse moved, update time and show the cursor
g_LastMouseMovement = get_performance_milliseconds();
io.MouseDrawCursor = true;
} else if ((get_performance_milliseconds() - g_LastMouseMovement) > 2000) {
// mouse idle for more than 2 seconds, hide the cursor
io.MouseDrawCursor = false;
}
}
if (cfg::CONFIGURATOR_STANDALONE) { if (cfg::CONFIGURATOR_STANDALONE) {
// if cursor is inside the client area, always set the OS cursor to what ImGui wants // if cursor is inside the client area, always set the OS cursor to what ImGui wants
@@ -528,8 +600,12 @@ void ImGui_ImplSpice_NewFrame() {
} }
} }
} else { } else {
// update OS mouse cursor with the cursor requested by imgui // in auto-hide mode imgui owns the cursor, so keep the OS cursor hidden
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor(); // even when the overlay is closed (io.MouseDrawCursor is false then).
ImGuiMouseCursor mouse_cursor =
(io.MouseDrawCursor || g_MouseCursorAutoHide)
? ImGuiMouseCursor_None
: ImGui::GetMouseCursor();
if (g_LastMouseCursor != mouse_cursor) { if (g_LastMouseCursor != mouse_cursor) {
g_LastMouseCursor = mouse_cursor; g_LastMouseCursor = mouse_cursor;
ImGui_ImplSpice_UpdateMouseCursor(); ImGui_ImplSpice_UpdateMouseCursor();
+1
View File
@@ -6,5 +6,6 @@
IMGUI_IMPL_API bool ImGui_ImplSpice_Init(HWND hWnd); IMGUI_IMPL_API bool ImGui_ImplSpice_Init(HWND hWnd);
IMGUI_IMPL_API void ImGui_ImplSpice_Shutdown(); IMGUI_IMPL_API void ImGui_ImplSpice_Shutdown();
IMGUI_IMPL_API void ImGui_ImplSpice_UpdateDisplaySize(); IMGUI_IMPL_API void ImGui_ImplSpice_UpdateDisplaySize();
IMGUI_IMPL_API void ImGui_ImplSpice_SetDisplaySizeOverride(float w, float h);
IMGUI_IMPL_API bool ImGui_ImplSpice_UpdateMouseCursor(); IMGUI_IMPL_API bool ImGui_ImplSpice_UpdateMouseCursor();
IMGUI_IMPL_API void ImGui_ImplSpice_NewFrame(); IMGUI_IMPL_API void ImGui_ImplSpice_NewFrame();
+265
View File
@@ -0,0 +1,265 @@
#include "notifications.h"
#include <atomic>
#include <deque>
#include <mutex>
#include <unordered_map>
#include "external/imgui/imgui.h"
#include "external/imgui/imgui_internal.h"
#include "external/fmt/include/fmt/format.h"
#include "overlay/overlay.h"
#include "util/time.h"
namespace overlay::notifications {
bool ENABLED = true;
Position POSITION = Position::BottomRight;
struct Notification {
uint64_t id;
std::string text;
Severity severity;
double created_ms;
float duration_s;
};
static std::mutex g_mutex;
static std::deque<Notification> g_items;
static std::atomic<uint64_t> g_next_id { 1 };
static std::atomic<size_t> g_count { 0 };
// duration in seconds each notification stays visible
static constexpr float DURATION_S = 3.0f;
// maximum number of notifications kept in the queue (oldest dropped beyond this)
static constexpr size_t MAX_NOTIFICATIONS = 6;
// time (ms) over which a toast fades out at the end of its lifetime
static constexpr float FADE_OUT_MS = 400.0f;
// fixed width of each toast window, in unscaled pixels
static constexpr float TOAST_WIDTH = 320.0f;
// gap between the toast stack and the screen edges (right + bottom)
static constexpr float TOAST_MARGIN = 20.0f;
// vertical gap between stacked toasts
static constexpr float TOAST_SPACING = 8.0f;
// inner padding inside a toast window (horizontal / vertical)
static constexpr float TOAST_PAD_X = 10.0f;
static constexpr float TOAST_PAD_Y = 8.0f;
// width of the colored severity accent bar drawn on the left edge
static constexpr float TOAST_ACCENT_W = 6.0f;
// base opacity of the toast background (0..1), multiplied by the fade alpha
static constexpr float TOAST_BG_ALPHA = 0.85f;
static constexpr ImGuiWindowFlags TOAST_FLAGS =
ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoInputs
| ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_NoFocusOnAppearing
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_AlwaysAutoResize;
static ImU32 severity_accent(Severity sev) {
switch (sev) {
case Severity::Success: return IM_COL32(80, 200, 120, 255);
case Severity::Warning: return IM_COL32(230, 180, 60, 255);
case Severity::Error: return IM_COL32(220, 60, 60, 255);
case Severity::Info:
default: return IM_COL32(90, 160, 230, 255);
}
}
static bool is_expired(const Notification &n, double now_ms) {
return (now_ms - n.created_ms) >= (n.duration_s * 1000.0);
}
// returns 0.0 .. 1.0 fade alpha based on time remaining
static float compute_alpha(const Notification &n, double now_ms) {
const double remaining_ms = (n.duration_s * 1000.0) - (now_ms - n.created_ms);
if (remaining_ms >= FADE_OUT_MS) {
return 1.0f;
}
if (remaining_ms <= 0.0) {
return 0.0f;
}
return static_cast<float>(remaining_ms / FADE_OUT_MS);
}
// drop expired items and copy the rest under a single lock acquisition
static std::vector<Notification> snapshot_and_prune(double now_ms) {
std::vector<Notification> snapshot;
std::lock_guard<std::mutex> lock(g_mutex);
for (auto it = g_items.begin(); it != g_items.end();) {
if (is_expired(*it, now_ms)) {
it = g_items.erase(it);
} else {
++it;
}
}
g_count.store(g_items.size(), std::memory_order_release);
snapshot.assign(g_items.begin(), g_items.end());
return snapshot;
}
// is the configured anchor on the right edge of the screen?
static bool position_is_right(Position p) {
return p == Position::BottomRight || p == Position::TopRight;
}
// is the configured anchor on the bottom edge of the screen?
static bool position_is_bottom(Position p) {
return p == Position::BottomRight || p == Position::BottomLeft;
}
// draw a single toast anchored to the configured corner; `cursor_y` is the
// y-coordinate of the toast edge nearest the anchor (top edge for Top* anchors,
// bottom edge for Bottom* anchors). returns its height in pixels.
static float draw_toast(const Notification &n, float cursor_y, float alpha) {
const float toast_width = apply_scaling(TOAST_WIDTH);
const float margin = apply_scaling(TOAST_MARGIN);
const ImVec2 &display = ImGui::GetIO().DisplaySize;
const Position pos = POSITION;
const auto window_id = fmt::format("##spice_notif_{}", n.id);
// anchor x/pivot.x select the screen edge; pivot.y matches cursor_y semantics
const float anchor_x = position_is_right(pos) ? (display.x - margin) : margin;
const float pivot_x = position_is_right(pos) ? 1.0f : 0.0f;
const float pivot_y = position_is_bottom(pos) ? 1.0f : 0.0f;
ImGui::SetNextWindowPos(ImVec2(anchor_x, cursor_y),
ImGuiCond_Always, ImVec2(pivot_x, pivot_y));
ImGui::SetNextWindowSize(ImVec2(toast_width, 0.f), ImGuiCond_Always);
ImGui::SetNextWindowBgAlpha(TOAST_BG_ALPHA * alpha);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
ImVec2(apply_scaling(TOAST_PAD_X), apply_scaling(TOAST_PAD_Y)));
float height = 0.f;
if (ImGui::Begin(window_id.c_str(), nullptr, TOAST_FLAGS)) {
// keep toasts above other overlay windows (e.g. the persistent FPS
// window, which may be toggled on after a toast already exists), but
// tuck them behind a blocking modal so they get dimmed/occluded by the
// modal backdrop instead of floating on top of it.
ImGuiWindow *toast_window = ImGui::GetCurrentWindow();
if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) {
ImGui::BringWindowToDisplayBehind(toast_window, modal);
} else {
ImGui::BringWindowToDisplayFront(toast_window);
}
const ImVec2 win_pos = ImGui::GetWindowPos();
const ImVec2 win_size = ImGui::GetWindowSize();
// accent bar on the left edge of the window
const ImU32 accent = severity_accent(n.severity);
const ImU32 accent_faded =
(accent & 0x00FFFFFFu) | (static_cast<ImU32>(alpha * 255.0f) << 24);
ImGui::GetWindowDrawList()->AddRectFilled(
win_pos,
ImVec2(win_pos.x + apply_scaling(TOAST_ACCENT_W), win_pos.y + win_size.y),
accent_faded);
// small gutter past the accent bar, then wrapped text
ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f));
ImGui::SameLine();
ImGui::PushTextWrapPos(win_pos.x + win_size.x - apply_scaling(TOAST_PAD_X));
ImGui::TextUnformatted(n.text.c_str());
ImGui::PopTextWrapPos();
height = ImGui::GetWindowSize().y;
}
ImGui::End();
ImGui::PopStyleVar(2);
return height;
}
uint64_t add(Severity severity, std::string text) {
if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) {
return 0;
}
Notification n {
.id = g_next_id.fetch_add(1, std::memory_order_relaxed),
.text = std::move(text),
.severity = severity,
.created_ms = get_performance_milliseconds(),
.duration_s = DURATION_S,
};
{
std::lock_guard<std::mutex> lock(g_mutex);
g_items.push_back(std::move(n));
while (g_items.size() > MAX_NOTIFICATIONS) {
g_items.pop_front();
}
g_count.store(g_items.size(), std::memory_order_release);
}
return n.id;
}
uint64_t add_throttled(Severity severity, const std::string &key,
double cooldown_seconds, std::string text) {
if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) {
return 0;
}
// per-key last-emit timestamps live behind their own lock so we don't
// hold g_mutex across the map lookup.
static std::mutex throttle_mutex;
static std::unordered_map<std::string, double> last_emit_ms;
const double now_ms = get_performance_milliseconds();
{
std::lock_guard<std::mutex> lock(throttle_mutex);
auto it = last_emit_ms.find(key);
if (it != last_emit_ms.end()
&& (now_ms - it->second) < (cooldown_seconds * 1000.0)) {
return 0;
}
last_emit_ms[key] = now_ms;
}
return add(severity, std::move(text));
}
bool has_pending() {
return g_count.load(std::memory_order_acquire) > 0;
}
void draw() {
const double now_ms = get_performance_milliseconds();
const auto snapshot = snapshot_and_prune(now_ms);
if (snapshot.empty()) {
return;
}
// stack from the anchored edge with newest toast at the anchor.
// Bottom* anchors stack upward; Top* anchors stack downward.
const float spacing = apply_scaling(TOAST_SPACING);
const float margin = apply_scaling(TOAST_MARGIN);
const bool bottom = position_is_bottom(POSITION);
float cursor_y = bottom
? (ImGui::GetIO().DisplaySize.y - margin)
: margin;
for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) {
const float alpha = compute_alpha(*it, now_ms);
const float height = draw_toast(*it, cursor_y, alpha);
cursor_y += bottom ? -(height + spacing) : (height + spacing);
}
}
void apply_game_default_position(const std::string &game_name) {
if (game_name == "Reflec Beat") {
POSITION = Position::TopRight;
}
// others keep the default (BottomRight)
}
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace overlay::notifications {
// master switch for the notification system; when false, add() is a no-op.
// controlled by selecting "none" for the -notifypos launcher option.
extern bool ENABLED;
enum class Severity {
Info,
Success,
Warning,
Error,
};
// screen anchor for the toast stack. toasts stack away from the anchored edge.
enum class Position {
BottomRight,
BottomLeft,
TopRight,
TopLeft,
};
// current toast anchor. defaults to BottomRight; may be reassigned by
// apply_game_default_position() or by the user via -notifypos.
extern Position POSITION;
// apply the default toast position appropriate for a game (by display name,
// as returned by eamuse_get_game()). called once after game autodetect, before
// any user -notifypos override is applied.
void apply_game_default_position(const std::string &game_name);
// add a notification (thread-safe). returns the assigned id, or 0 if the
// notification was dropped (overlay disabled or notifications disabled).
uint64_t add(Severity severity, std::string text);
// rate-limited variant of add(). suppresses the toast if another call with
// the same `key` succeeded within the last `cooldown_seconds`. useful for
// events that can fire every frame (e.g. a button held down). returns the
// assigned id, or 0 if the toast was suppressed or dropped. thread-safe.
uint64_t add_throttled(Severity severity, const std::string &key,
double cooldown_seconds, std::string text);
// true if there is at least one notification that still needs to be drawn.
// safe to call from the render thread without locking the underlying store.
bool has_pending();
// draw all active notifications and prune expired ones.
// must be called from the ImGui render thread inside a NewFrame/EndFrame pair.
void draw();
}
+323 -53
View File
@@ -15,8 +15,18 @@
#include "build/resource.h" #include "build/resource.h"
#include "external/imgui/backends/imgui_impl_dx9.h" #include "external/imgui/backends/imgui_impl_dx9.h"
#ifdef SPICE_D3D11
#include "external/imgui/backends/imgui_impl_dx11.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
#endif
#include "overlay/imgui/impl_spice.h" #include "overlay/imgui/impl_spice.h"
#include "overlay/imgui/impl_sw.h" #include "overlay/imgui/impl_sw.h"
#include "overlay/notifications.h"
#ifdef SPICE_D3D11
#include <d3d11.h>
#include <dxgi.h>
#endif
#include "window.h" #include "window.h"
#ifdef SPICE64 #ifdef SPICE64
@@ -40,6 +50,7 @@
#include "windows/sdvx_sub.h" #include "windows/sdvx_sub.h"
#include "windows/keypad.h" #include "windows/keypad.h"
#include "windows/log.h" #include "windows/log.h"
#include "windows/obs.h"
#include "windows/patch_manager.h" #include "windows/patch_manager.h"
#include "windows/exitprompt.cpp" #include "windows/exitprompt.cpp"
@@ -56,7 +67,7 @@ namespace overlay {
bool AUTO_SHOW_KEYPAD_P1 = false; bool AUTO_SHOW_KEYPAD_P1 = false;
bool AUTO_SHOW_KEYPAD_P2 = false; bool AUTO_SHOW_KEYPAD_P2 = false;
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false; bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
bool FPS_SHOULD_FLIP = false; FpsLocation FPS_LOCATION = FpsLocation::TopRight;
std::optional<uint32_t> UI_SCALE_PERCENT; std::optional<uint32_t> UI_SCALE_PERCENT;
// global // global
@@ -104,6 +115,21 @@ void overlay::create_d3d9(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device)
} }
} }
#ifdef SPICE_D3D11
void overlay::create_d3d11(HWND hWnd, ID3D11Device *device, ID3D11DeviceContext *context,
IDXGISwapChain *swapchain) {
if (!overlay::ENABLED) {
return;
}
const std::lock_guard<std::mutex> lock(OVERLAY_MUTEX);
if (!overlay::OVERLAY) {
overlay::OVERLAY = std::make_unique<overlay::SpiceOverlay>(hWnd, device, context, swapchain);
}
}
#endif
void overlay::create_software(HWND hWnd) { void overlay::create_software(HWND hWnd) {
if (!overlay::ENABLED) { if (!overlay::ENABLED) {
return; return;
@@ -164,6 +190,27 @@ overlay::SpiceOverlay::SpiceOverlay(HWND hWnd)
this->init(); this->init();
} }
#ifdef SPICE_D3D11
overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, ID3D11Device *d3d11_device,
ID3D11DeviceContext *d3d11_context,
IDXGISwapChain *d3d11_swapchain)
: renderer(OverlayRenderer::D3D11),
hWnd(hWnd),
d3d11_device(d3d11_device),
d3d11_context(d3d11_context),
d3d11_swapchain(d3d11_swapchain) {
log_info("overlay", "initializing (D3D11)");
// increment reference counts
this->d3d11_device->AddRef();
this->d3d11_context->AddRef();
this->d3d11_swapchain->AddRef();
// init
this->init();
}
#endif
void overlay::SpiceOverlay::init() { void overlay::SpiceOverlay::init() {
// init imgui // init imgui
@@ -228,6 +275,7 @@ void overlay::SpiceOverlay::init() {
// Interactables (The High-Intensity Red) // Interactables (The High-Intensity Red)
colors[ImGuiCol_CheckMark] = ImVec4(0.85f, 0.15f, 0.15f, 1.00f); // Sharp Red colors[ImGuiCol_CheckMark] = ImVec4(0.85f, 0.15f, 0.15f, 1.00f); // Sharp Red
colors[ImGuiCol_CheckboxSelectedBg] = colors[ImGuiCol_FrameBg];
colors[ImGuiCol_SliderGrab] = ImVec4(0.60f, 0.12f, 0.12f, 1.00f); colors[ImGuiCol_SliderGrab] = ImVec4(0.60f, 0.12f, 0.12f, 1.00f);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.85f, 0.15f, 0.15f, 1.00f); colors[ImGuiCol_SliderGrabActive] = ImVec4(0.85f, 0.15f, 0.15f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.30f, 0.12f, 0.12f, 1.00f); colors[ImGuiCol_Button] = ImVec4(0.30f, 0.12f, 0.12f, 1.00f);
@@ -252,6 +300,7 @@ void overlay::SpiceOverlay::init() {
colors[ImGuiCol_Separator] = ImVec4(0.32f, 0.22f, 0.22f, 1.00f); colors[ImGuiCol_Separator] = ImVec4(0.32f, 0.22f, 0.22f, 1.00f);
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.42f, 0.22f, 0.22f, 1.00f); colors[ImGuiCol_SeparatorHovered] = ImVec4(0.42f, 0.22f, 0.22f, 1.00f);
colors[ImGuiCol_SeparatorActive] = ImVec4(0.52f, 0.22f, 0.22f, 1.00f); colors[ImGuiCol_SeparatorActive] = ImVec4(0.52f, 0.22f, 0.22f, 1.00f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.5f);
#ifdef IMGUI_HAS_DOCK #ifdef IMGUI_HAS_DOCK
colors[ImGuiCol_DockingPreview] = ImVec4(0.85f, 0.15f, 0.15f, 0.40f); colors[ImGuiCol_DockingPreview] = ImVec4(0.85f, 0.15f, 0.15f, 0.40f);
@@ -261,16 +310,10 @@ void overlay::SpiceOverlay::init() {
// configure IO // configure IO
auto &io = ImGui::GetIO(); auto &io = ImGui::GetIO();
io.UserData = this; io.UserData = this;
io.ConfigFlags = ImGuiConfigFlags_NavEnableKeyboard io.ConfigFlags = ImGuiConfigFlags_None;
| ImGuiConfigFlags_NavEnableGamepad
| ImGuiConfigFlags_NavEnableSetMousePos;
if (!cfg::CONFIGURATOR_STANDALONE) { if (!cfg::CONFIGURATOR_STANDALONE) {
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
} }
if (is_touch_available("SpiceOverlay::init")) {
io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
}
// temporarily turn this off as it can cause crashes during font load failures // temporarily turn this off as it can cause crashes during font load failures
// turns back on in ImGui_ImplSpice_Init // turns back on in ImGui_ImplSpice_Init
@@ -281,8 +324,9 @@ void overlay::SpiceOverlay::init() {
// disable config // disable config
io.IniFilename = nullptr; io.IniFilename = nullptr;
// allow CTRL+WHEEL scaling // allow CTRL+WHEEL scaling in the in-game overlay; disable for the standalone
io.FontAllowUserScaling = true; // configurator so the mouse wheel always scrolls the configuration window.
io.FontAllowUserScaling = !cfg::CONFIGURATOR_STANDALONE;
// add default font // add default font
io.Fonts->AddFontDefaultBitmap(); io.Fonts->AddFontDefaultBitmap();
@@ -316,6 +360,11 @@ void overlay::SpiceOverlay::init() {
case OverlayRenderer::D3D9: case OverlayRenderer::D3D9:
ImGui_ImplDX9_Init(this->device); ImGui_ImplDX9_Init(this->device);
break; break;
#ifdef SPICE_D3D11
case OverlayRenderer::D3D11:
ImGui_ImplDX11_Init(this->d3d11_device, this->d3d11_context);
break;
#endif
case OverlayRenderer::SOFTWARE: case OverlayRenderer::SOFTWARE:
imgui_sw::bind_imgui_painting(); imgui_sw::bind_imgui_painting();
break; break;
@@ -326,6 +375,11 @@ void overlay::SpiceOverlay::init() {
case OverlayRenderer::D3D9: case OverlayRenderer::D3D9:
ImGui_ImplDX9_NewFrame(); ImGui_ImplDX9_NewFrame();
break; break;
#ifdef SPICE_D3D11
case OverlayRenderer::D3D11:
ImGui_ImplDX11_NewFrame();
break;
#endif
case OverlayRenderer::SOFTWARE: case OverlayRenderer::SOFTWARE:
break; break;
} }
@@ -339,14 +393,14 @@ void overlay::SpiceOverlay::init() {
bool set_overlay_active = false; bool set_overlay_active = false;
// referenced windows // owned separately from `windows` so it never affects overlay activation/input gating
this->window_add(window_fps = new overlay::windows::FPS(this)); window_fps = std::make_unique<overlay::windows::FPS>(this);
if (!cfg::CONFIGURATOR_STANDALONE && AUTO_SHOW_FPS) { if (!cfg::CONFIGURATOR_STANDALONE && AUTO_SHOW_FPS) {
window_fps->set_active(true); window_fps->set_active(true);
set_overlay_active = true;
} }
this->window_add(window_main_menu = new overlay::windows::ExitPrompt(this)); // owned separately from `windows` so it is not part of the overlay window layer
window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this);
// add default windows // add default windows
this->window_add(window_config = new overlay::windows::Config(this)); this->window_add(window_config = new overlay::windows::Config(this));
@@ -363,6 +417,12 @@ void overlay::SpiceOverlay::init() {
} }
this->window_add(new overlay::windows::PatchManager(this)); this->window_add(new overlay::windows::PatchManager(this));
// OBS control spawns a background WebSocket worker; skip it in the standalone
// configurator, where there is no running game to stream/record
if (!cfg::CONFIGURATOR_STANDALONE) {
this->window_add(window_obs = new overlay::windows::OBSControl(this));
}
{ {
window_keypad1 = new overlay::windows::Keypad(this, 0); window_keypad1 = new overlay::windows::Keypad(this, 0);
this->window_add(window_keypad1); this->window_add(window_keypad1);
@@ -448,6 +508,21 @@ overlay::SpiceOverlay::~SpiceOverlay() {
this->d3d->Release(); this->d3d->Release();
break; break;
#ifdef SPICE_D3D11
case OverlayRenderer::D3D11:
if (this->d3d11_rtv) {
this->d3d11_rtv->Release();
this->d3d11_rtv = nullptr;
}
ImGui_ImplDX11_Shutdown();
// drop references
this->d3d11_swapchain->Release();
this->d3d11_context->Release();
this->d3d11_device->Release();
break;
#endif
case OverlayRenderer::SOFTWARE: case OverlayRenderer::SOFTWARE:
imgui_sw::unbind_imgui_painting(); imgui_sw::unbind_imgui_painting();
break; break;
@@ -465,8 +540,20 @@ void overlay::SpiceOverlay::new_frame() {
ImGui_ImplSpice_NewFrame(); ImGui_ImplSpice_NewFrame();
this->total_elapsed += ImGui::GetIO().DeltaTime; this->total_elapsed += ImGui::GetIO().DeltaTime;
// check if inactive // notifications draw on top of the game without flipping `active`, so the input gates
if (!this->active) { // (see touch/touch.cpp WndProc, touch_get_points/events, graphics.cpp WM_CHAR) stay
// disabled and input continues to flow to the game.
// SOFTWARE renderer (spicecfg / configurator) never gets notifications - no room.
const bool draw_notifications = this->renderer != OverlayRenderer::SOFTWARE
&& overlay::notifications::has_pending();
// persistent FPS window: drawn whenever active, independent of the overlay
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
&& this->window_fps->get_active();
// check if there is nothing to draw
this->has_pending_frame = false;
if (!this->active && !draw_notifications && !draw_fps_persistent) {
return; return;
} }
@@ -475,29 +562,103 @@ void overlay::SpiceOverlay::new_frame() {
case OverlayRenderer::D3D9: case OverlayRenderer::D3D9:
ImGui_ImplDX9_NewFrame(); ImGui_ImplDX9_NewFrame();
break; break;
#ifdef SPICE_D3D11
case OverlayRenderer::D3D11:
// refresh DisplaySize each frame; the dx11 swapchain hook pushes
// the current backbuffer dimensions into impl_spice via the
// size override so ImGui's viewport tracks ResizeBuffers calls.
ImGui_ImplSpice_UpdateDisplaySize();
ImGui_ImplDX11_NewFrame();
break;
#endif
case OverlayRenderer::SOFTWARE: case OverlayRenderer::SOFTWARE:
ImGui_ImplSpice_UpdateDisplaySize(); ImGui_ImplSpice_UpdateDisplaySize();
break; break;
} }
ImGui::NewFrame(); ImGui::NewFrame();
this->has_pending_frame = true;
// build windows // build windows only when the overlay itself is active
for (auto &window : this->windows) { if (this->active) {
window->build(); for (auto &window : this->windows) {
window->build();
}
// draw the main menu on top of the overlay windows
this->window_main_menu->build();
if (SHOW_DEBUG_LOG_WINDOW) {
ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW);
}
} }
if (SHOW_DEBUG_LOG_WINDOW) { if (draw_fps_persistent) {
ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW); this->window_fps->build();
}
// draw notifications last so they paint on top of any overlay windows
if (draw_notifications) {
overlay::notifications::draw();
} }
// end frame // end frame
ImGui::EndFrame(); ImGui::EndFrame();
} }
// FNV-1a 64-bit hash helper used by the software renderer to detect idle frames.
static inline uint64_t overlay_fnv1a64(uint64_t h, const void *data, size_t len) {
const auto *p = static_cast<const uint8_t *>(data);
for (size_t i = 0; i < len; i++) {
h ^= p[i];
h *= 0x100000001B3ULL;
}
return h;
}
// Compute a hash over the visual content of ImDrawData. Only the geometry the
// software rasterizer actually consumes (vertex/index buffers + display size)
// feeds into the hash, so frames where ImGui produces identical draw data
// (i.e. nothing animated this tick) can skip the full pixel rasterization.
static uint64_t overlay_hash_draw_data(const ImDrawData *draw_data) {
uint64_t h = 0xcbf29ce484222325ULL;
if (draw_data == nullptr) {
return h;
}
const float display[4] = {
draw_data->DisplayPos.x,
draw_data->DisplayPos.y,
draw_data->DisplaySize.x,
draw_data->DisplaySize.y,
};
h = overlay_fnv1a64(h, display, sizeof(display));
const int cmd_lists = draw_data->CmdListsCount;
h = overlay_fnv1a64(h, &cmd_lists, sizeof(cmd_lists));
for (int i = 0; i < draw_data->CmdListsCount; i++) {
const ImDrawList *cmd_list = draw_data->CmdLists[i];
if (cmd_list == nullptr) {
continue;
}
const int vtx_count = cmd_list->VtxBuffer.Size;
const int idx_count = cmd_list->IdxBuffer.Size;
h = overlay_fnv1a64(h, &vtx_count, sizeof(vtx_count));
h = overlay_fnv1a64(h, &idx_count, sizeof(idx_count));
if (vtx_count > 0) {
h = overlay_fnv1a64(h, cmd_list->VtxBuffer.Data,
static_cast<size_t>(vtx_count) * sizeof(ImDrawVert));
}
if (idx_count > 0) {
h = overlay_fnv1a64(h, cmd_list->IdxBuffer.Data,
static_cast<size_t>(idx_count) * sizeof(ImDrawIdx));
}
}
return h;
}
void overlay::SpiceOverlay::render() { void overlay::SpiceOverlay::render() {
// check if inactive // skip if new_frame() didn't begin a frame this tick
if (!this->active) { if (!this->has_pending_frame) {
return; return;
} }
@@ -507,8 +668,35 @@ void overlay::SpiceOverlay::render() {
// implementation render // implementation render
switch (this->renderer) { switch (this->renderer) {
case OverlayRenderer::D3D9: case OverlayRenderer::D3D9:
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData()); if (cfg::CONFIGURATOR_STANDALONE) {
const auto *draw_data = ImGui::GetDrawData();
const uint64_t draw_hash = overlay_hash_draw_data(draw_data);
const auto &io = ImGui::GetIO();
const int display_w = static_cast<int>(std::ceil(io.DisplaySize.x));
const int display_h = static_cast<int>(std::ceil(io.DisplaySize.y));
const bool size_matches = (this->d3d9_last_display_w == display_w
&& this->d3d9_last_display_h == display_h);
if (this->d3d9_has_last_draw_hash
&& draw_hash == this->d3d9_last_draw_hash
&& size_matches) {
this->d3d9_frame_dirty = false;
break;
}
this->d3d9_last_draw_hash = draw_hash;
this->d3d9_has_last_draw_hash = true;
this->d3d9_last_display_w = display_w;
this->d3d9_last_display_h = display_h;
this->d3d9_frame_dirty = true;
} else {
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
}
break; break;
#ifdef SPICE_D3D11
case OverlayRenderer::D3D11:
overlay::d3d11::render(this->d3d11_device, this->d3d11_context,
this->d3d11_swapchain, &this->d3d11_rtv);
break;
#endif
case OverlayRenderer::SOFTWARE: { case OverlayRenderer::SOFTWARE: {
// get display metrics // get display metrics
@@ -517,6 +705,23 @@ void overlay::SpiceOverlay::render() {
auto height = static_cast<size_t>(std::ceil(io.DisplaySize.y)); auto height = static_cast<size_t>(std::ceil(io.DisplaySize.y));
auto pixels = width * height; auto pixels = width * height;
// skip the (expensive) full software rasterization when the draw data
// is byte-identical to the previous frame and the existing pixel
// buffer still matches the current display size.
const auto *draw_data = ImGui::GetDrawData();
const uint64_t draw_hash = overlay_hash_draw_data(draw_data);
const bool size_matches = (this->pixel_data_width == width
&& this->pixel_data_height == height
&& this->pixel_data.size() >= pixels);
if (this->sw_has_last_draw_hash
&& draw_hash == this->sw_last_draw_hash
&& size_matches) {
this->sw_pixels_dirty = false;
break;
}
this->sw_last_draw_hash = draw_hash;
this->sw_has_last_draw_hash = true;
// make sure buffer is big enough // make sure buffer is big enough
if (this->pixel_data.size() < pixels) { if (this->pixel_data.size() < pixels) {
this->pixel_data.resize(pixels, 0); this->pixel_data.resize(pixels, 0);
@@ -533,6 +738,7 @@ void overlay::SpiceOverlay::render() {
imgui_sw::paint_imgui(&this->pixel_data[0], width, height, options); imgui_sw::paint_imgui(&this->pixel_data[0], width, height, options);
pixel_data_width = width; pixel_data_width = width;
pixel_data_height = height; pixel_data_height = height;
this->sw_pixels_dirty = true;
break; break;
} }
@@ -541,17 +747,35 @@ void overlay::SpiceOverlay::render() {
for (auto &window : this->windows) { for (auto &window : this->windows) {
window->after_render(); window->after_render();
} }
this->has_pending_frame = false;
}
void overlay::SpiceOverlay::d3d9_render_draw(const bool force_submit) {
if (this->renderer != OverlayRenderer::D3D9) {
return;
}
if (!force_submit && !this->d3d9_frame_dirty) {
return;
}
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
} }
void overlay::SpiceOverlay::update() { void overlay::SpiceOverlay::update() {
// check overlay toggle // there are three layers -
// bottommost layer - FPS, notifications (non-interactable)
// overlay layer - most windows
// topmost layer - main menu (popup)
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game()); auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
bool toggle_down_new = overlay_buttons
// check overlay toggle
const bool toggle_down_new = overlay_buttons
&& this->hotkeys_triggered() && this->hotkeys_triggered()
&& GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(games::OverlayButtons::ToggleOverlay)); && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(games::OverlayButtons::ToggleAllWindows));
if (toggle_down_new && !this->toggle_down) { if (toggle_down_new && !this->toggle_down) {
toggle_active(true); toggle_active();
} }
this->toggle_down = toggle_down_new; this->toggle_down = toggle_down_new;
@@ -564,17 +788,35 @@ void overlay::SpiceOverlay::update() {
} }
this->main_menu_down = main_menu_down_new; this->main_menu_down = main_menu_down_new;
// check FPS toggle - controls the persistent FPS window only, never the overlay
const auto fps_down_new = overlay_buttons
&& this->hotkeys_triggered()
&& GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(games::OverlayButtons::ToggleFps));
if (fps_down_new && !this->fps_down) {
this->window_fps->toggle_active();
}
this->fps_down = fps_down_new;
// update windows // update windows
for (auto &window : this->windows) { for (auto &window : this->windows) {
window->update(); window->update();
} }
// deactivate if no windows are shown // FPS window
bool window_active = false; this->window_fps->update();
for (auto &window : this->windows) {
if (window->get_active()) { // main menu (owned separately from the overlay window layer)
window_active = true; this->window_main_menu->update();
break;
// deactivate if nothing is shown - the main menu keeps the overlay active
// while open even though it is not part of `windows`
bool window_active = this->window_main_menu->get_active();
if (!window_active) {
for (auto &window : this->windows) {
if (window->get_active()) {
window_active = true;
break;
}
} }
} }
if (!window_active) { if (!window_active) {
@@ -586,20 +828,9 @@ bool overlay::SpiceOverlay::update_cursor() {
return ImGui_ImplSpice_UpdateMouseCursor(); return ImGui_ImplSpice_UpdateMouseCursor();
} }
void overlay::SpiceOverlay::toggle_active(bool overlay_key) { void overlay::SpiceOverlay::toggle_active() {
// invert active state // invert active state
this->active = !this->active; this->active = !this->active;
// get rid of main menu if it was visible
if (this->window_main_menu) {
this->window_main_menu->set_active(false);
}
// show FPS window if toggled with overlay key
if (overlay_key) {
this->window_fps->set_active(this->active);
}
} }
void overlay::SpiceOverlay::show_main_menu() { void overlay::SpiceOverlay::show_main_menu() {
@@ -611,7 +842,10 @@ void overlay::SpiceOverlay::show_main_menu() {
this->window_main_menu->set_active(false); this->window_main_menu->set_active(false);
return; return;
} }
if (ImGui::IsPopupOpen(0, ImGuiPopupFlags_AnyPopup)) {
// don't open on top of another genuinely-visible popup, but ONLY guard while
// the overlay is active
if (this->get_active() && ImGui::IsPopupOpen(0, ImGuiPopupFlags_AnyPopup)) {
return; return;
} }
@@ -642,9 +876,8 @@ bool overlay::SpiceOverlay::has_focus() {
} }
bool overlay::SpiceOverlay::hotkeys_triggered() { bool overlay::SpiceOverlay::hotkeys_triggered() {
// prevent hotkeys in spicecfg
// check if disabled first if (cfg::CONFIGURATOR_STANDALONE) {
if (!this->hotkeys_enable) {
return false; return false;
} }
@@ -686,11 +919,48 @@ bool overlay::SpiceOverlay::hotkeys_triggered() {
} }
void overlay::SpiceOverlay::reset_invalidate() { void overlay::SpiceOverlay::reset_invalidate() {
ImGui_ImplDX9_InvalidateDeviceObjects(); if (!overlay::OVERLAY) {
return;
}
switch (overlay::OVERLAY->renderer) {
case OverlayRenderer::D3D9:
if (cfg::CONFIGURATOR_STANDALONE) {
overlay::OVERLAY->d3d9_has_last_draw_hash = false;
overlay::OVERLAY->d3d9_frame_dirty = true;
}
ImGui_ImplDX9_InvalidateDeviceObjects();
break;
#ifdef SPICE_D3D11
case OverlayRenderer::D3D11:
// for DX11 a ResizeBuffers only invalidates the backbuffer RTV; the imgui
// device objects (shaders, buffers, textures) remain valid.
if (overlay::OVERLAY->d3d11_rtv) {
overlay::OVERLAY->d3d11_rtv->Release();
overlay::OVERLAY->d3d11_rtv = nullptr;
}
break;
#endif
case OverlayRenderer::SOFTWARE:
break;
}
} }
void overlay::SpiceOverlay::reset_recreate() { void overlay::SpiceOverlay::reset_recreate() {
ImGui_ImplDX9_CreateDeviceObjects(); if (!overlay::OVERLAY) {
return;
}
switch (overlay::OVERLAY->renderer) {
case OverlayRenderer::D3D9:
ImGui_ImplDX9_CreateDeviceObjects();
break;
#ifdef SPICE_D3D11
case OverlayRenderer::D3D11:
// RTV is lazily recreated on the next render()
break;
#endif
case OverlayRenderer::SOFTWARE:
break;
}
} }
void overlay::SpiceOverlay::input_char(unsigned int c) { void overlay::SpiceOverlay::input_char(unsigned int c) {
@@ -700,7 +970,7 @@ void overlay::SpiceOverlay::input_char(unsigned int c) {
uint32_t *overlay::SpiceOverlay::sw_get_pixel_data(int *width, int *height) { uint32_t *overlay::SpiceOverlay::sw_get_pixel_data(int *width, int *height) {
// check if active // check if active (notifications never draw in software renderer, so no extra gate here)
if (!this->active) { if (!this->active) {
*width = 0; *width = 0;
*height = 0; *height = 0;

Some files were not shown because too many files have changed in this diff Show More