Compare commits

...

168 Commits

Author SHA1 Message Date
bicarus 64600826b7 nost: touch piano mode (#830)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Touch piano mode. Allows piano keys to be played with the touch screen.
Since the touch screen is still needed for menu navigation (including
during songs) a toggle switch is added so that the user can flip between
navigation mode and piano mode.

The touch targets are pixel-perfect. Don't let the game's piano
animations fool you.

## Testing
2026-07-23 15:49:50 -07:00
bicarus 80deb4bbbd iidx: hide tdj sub screen in fullscreen single monitor mode (#829)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Prevent the game from launching a blank window when launching fullscreen
with sub disabled.

## Testing
2026-07-22 14:37:29 -07:00
bicarus 00c8bb8ce6 gitadora: use native touch for single-window mode (#828)
## Link to GitHub Issue or related Pull Request, if one exists
#814 

## Description of change
Make gitadora arena model use native touch instead of wintouchemu. Only
single window mode with overlay is affected.

This removes the last consumer of `INJECT_MOUSE_AS_WM_TOUCH` so all of
that code can be deleted.

## Testing
Tested touch screen and mouse interaction with single window mode.
2026-07-22 01:36:06 -07:00
bicarus 09ed8b948c nost: move nostalgia from wintouchemu to native touch hook (#827)
## Link to GitHub Issue or related Pull Request, if one exists
#814

## Description of change
Switch over Nostalgia from wintouchemu to native touch hook.

Nostalgia has some strict timing requirements (touches must be updated
on every acio poll) so this takes a slightly different path to maintain
mouse holds.

Only a handful of consumers of wintouchemu remain:

1. beatstream - but it's off by default, only enabled as errata for
buggy touchscreens, or if the user forces it on (for Show Cursor)
2. gitadora single-window overlay - this is just for mouse so not a big
deal.
3. MFC HG mode - not a priority to fix.

## Testing
Tested full screen and windowed mode with touch / mouse / poke.
2026-07-22 00:56:03 -07:00
bicarus e2daefbb6a overlay: can't press enter to select sub-tab in Controller tab (#826)
## Link to GitHub Issue or related Pull Request, if one exists
Another bug reported by a sea creature

## Description of change
Fix keyboard navigation in Controllers sub tabs.

## Testing
2026-07-21 21:13:18 -07:00
bicarus 39bf17aafe patcher: fix plus minus buttons for number patches (#825)
## Link to GitHub Issue or related Pull Request, if one exists
Reported by a certain sea reptile

## Description of change
Fix the + / - buttons for number patches not working. Regressed by one
of the ImGui version updates.

## Testing
seems fine now
2026-07-21 17:46:18 -07:00
bicarus 4836cd94ab touch: inject touches from spiceapi to native touch hooks (#824)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #814

## Description of change
Touches via the API wasn't being delivered to native touch hook;
implement this, which closes the last gap we have vs. wintouchemu.

While we're here, implement popn subscreen support for companion as
well.

## Testing
Tested for iidx/sdvx/popn
2026-07-21 16:57:11 -07:00
bicarus 9d41ca2515 nost: xact audio hook (#823)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #822

## Description of change
Add hooks for `libxact.dll` which only Nostalgia uses for audio. Detect
cases where the game would fail to launch (wrong number of channels) and
log a warning / deferred log.

## Testing
tested 2024102200
2026-07-21 15:33:30 -07:00
bicarus 18421ffbfe touch: make native touch the default for iidx/sdvx/popn (#820)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #814

## Description of change
Remove iidx/sdvx/popon `native touch` options.

Remove dead code in wintouchemu that deals with these games.

Native touch is now at feature-parity with wintouchemu. Native touch
options work better with touch screens, while rawinput-based wintouchemu
has compat issues on some touchscreens. For these subscreen games
deprecate the usage of wintouchemu and make them use native touch stack
by default.

### For future consideration
Gitadora - not sure what to do with this one. We only use wintouchemu
for the single window case. For all other configurations, the game
accepts mouse and touch input without any hooks, so perhaps nothing
needs to be done here.

Nostalgia - consider moving away from wintouchemu and adopt native touch
hook here as well.

## Testing
WIP
2026-07-21 02:15:56 -07:00
bicarus 3f0921d983 gitadora: more windowed mode fixes (#819)
## Link to GitHub Issue or related Pull Request, if one exists
User reported

## Description of change

Sometimes the game moved the main window after `CreateWindowExA`
returned but before D3D initialization called
`graphics_capture_initial_window`. During this gap,
`GRAPHICS_HOOKED_WINDOW` was not set, so the placement hooks could not
identify the main window and preserve its monitor override. GITADORA
main, LEFT, and RIGHT windows are now registered immediately after
creation so later placement calls cannot undo their overrides.

This also adds `-gdwsmallsize` and `-gdwsmallpos` for explicitly setting
the SMALL window size and position. Explicit values override the
corresponding monitor-derived geometry, allowing the SMALL window's
10:16 aspect ratio to be preserved instead of stretching it to fill a
monitor.

## Testing
2026-07-21 00:54:09 -07:00
bicarus ed7318c270 rawinput: modifiers (key combinations) (#813)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #635

## Description of change
Adds optional **button modifiers**, allowing a binding to require one or
more "modifier" buttons to be held before it activates.

- New "Modifiers" controller page in the overlay config to bind the
Modifier 1–4 source buttons.
- Each binding gains a 4-bit modifier_mask (Modifier 1-4). It is
persisted as an optional modifiers XML attribute on button nodes and
controller-preset entries. The attribute is optional and defaults to 0,
so existing config files and presets load unchanged and the feature is
off unless the user opts in.
- The button Edit properties popup gains a "Modifiers" dropdown to pick
which modifiers a binding requires. Doesn't apply to MIDI though.
- Input evaluation skips a binding whose required modifiers are not
held, falling through to its alternatives; velocity reporting uses the
same gated path.
- Controller-preset templates gain a "Modifiers" group for importing and
exporting.

## Testing
2026-07-21 00:26:36 -07:00
bicarus 623e1e3998 touch: inject mouse and poke into native touch modes (#815)
## Link to GitHub Issue or related Pull Request, if one exists
Part 1 of many fixes for #814.

## Description of change
For the native touch hook - add code to inject synthetic touches using
Windows API (`InjectTouchInput`).

Note that this is Windows 8+ only, but we can make an assumption that we
are running on Win10+ for these games (TDJ/UFC/High Cheers) since the
cabs assume Win10.

Detect mouse events and allow IIDX poke to call into this to inject
synthetic touches.

Also, update the nativetouchhook to independently calculate window size
and rotation, instead of relying on rawinput layer.

## Testing
Tested to work with native touch option on IIDX, SDVX, POPN, all full
screen / windowed / sub on/off combinations.

Edge cases:

* sdvx main monitor rotated 270 deg
* touch invert option
* windowed mode resize / moved

All seem to work.
2026-07-21 00:04:38 -07:00
bicarus 558e3d0cb3 rawinput: avoid enumerationg adapter modes on first touch (#817)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #790

## Description of change
Avoid enumerating every display mode with `EnumDisplaySettings` during
the first raw touchscreen report, as it can take a couple hundred
milliseconds.

The latency-sensitive path now refreshes only the current display size
using `GetWindowRect(GetDesktopWindow())` and orientation using
`EnumDisplaySettingsEx(..., ENUM_CURRENT_SETTINGS, ..., EDS_RAWMODE)`.
Full `EnumDisplaySettings` enumeration runs only when native resolution
is needed for aspect compensation.

Jubeat and Reflec Beat now enable aspect compensation before
initializing touch input.

## Testing
Sanity checked rb/jb on laptops with non-16:9 ratio
2026-07-20 03:00:34 -07:00
bicarus d0db547dd0 sdvx: implement NVAPI for non-NVIDIA GPUs (#816)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

In summary: for SDVX VM mode, this PR eliminates the need for `Note FPS
Target 120Hz` / `Game FPS Target 120Hz` patches typically used for AMD
GPU via synthetic responses provided via a fake NVAPI layer.

Before this change, playing on AMD GPU required patches to force the
game to render at 60Hz (See #107 for relevant discussion). Basically, if
NVAPI query fails, the game shows

`I:NvDisplayConfig: GetMainDisplayRefreshRate = 60.000hz (DUMMY)`

the game renders at 120Hz but the notes render at 60Hz, which would look
stuttery.

After this change, even without a real `nvapi64.dll`, spice will provide
a stub implementation, just enough to convince the game that the main
display is 120Hz, which causes notes to render at 120Hz as well.

This can be overridden by `-graphics-force-refresh` option.

Important note: if user has the `Note FPS Target 60Hz` patch but running
the game at 120Hz, before this change the notes would render at 60Hz,
but after this they will be at 120Hz. I don't know why anyone would
intentionally lower the notes framerate, so this is not going to be
fixed. If the user wants to run the game at 60Hz they can do so via
`-graphics-force-refresh`.

Another change - `-nonvapi` will always force down this synthetic nvapi
path, even if the system has the real `nvapi64.dll`. Even on NVDIA GPU
this option can be used to force this.

## Testing
2026-07-19 23:43:14 -07:00
bicarus be4bb00390 patcher: patch groups (#812)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #245

## Description of change

Introduce patch groups. JSON schema now allows for a `"type": "group"`:

```json
    {
        "type": "group",
        "gameCode": "LDJ",
        "id": "timer-freeze",
        "name": "Timer Freeze Patches",
        "description": "Freezes various timers in the game."
    },
    {
        "name": "Standard/Menu Timer Freeze",
        "description": "Freezes all non-premium area timers.",
        "caution": "",
        "gameCode": "LDJ",
        "type": "memory",
        "group": "timer-freeze",
        "patches": [
            {
                "offset": 9962743,
                "dllName": "bm2dx.dll",
                "dataDisabled": "0F84",
                "dataEnabled": "90E9"
            }
        ]
    },
    {
        "name": "Premium Free Timer Freeze",
        "description": "Freezes all premium area timers.",
        "caution": "",
        "gameCode": "LDJ",
        "type": "memory",
        "group": "timer-freeze",
        "patches": [
            {
                "offset": 9111965,
                "dllName": "bm2dx.dll",
                "dataDisabled": "7E",
                "dataEnabled": "EB"
            }
        ]
    },
```

In the UI, these will show up as tree nodes. The parent is not actually
treated as a patch; e.g., its state is not saved to the patch manager
config file; only the child patches states are managed, the parent's
state only bubble up only in the UI.

In downlevel versions of spice, group parents will show up as a patch of
invalid type. Child patches will still show up as individual patches.

## Testing
*how was the code tested?*
2026-07-19 02:10:13 -07:00
bicarus a8cc85531c overlay: disable subscreen overlay if "native touch" option is on (#811)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change
If IIDX/SDVX/POPN "native touch" option is enabled, show an error
message if user toggles subscreen window.

Those options are meant for users with touch screens, and disables mouse
interactions. Users who like to randomly turn on options often latch on
to this option and get confused when clicks don't register.

## Testing
2026-07-18 21:49:50 -07:00
bicarus 2dea0d27bf rawinput: fix hotplug for keyboard and mouse (#810)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fix a long-standing issue of hotplug not working for keyboards and mice.
The code only registered for HID notifications; we also need to do it
for the keyboard GUID and mouse GUID.

## Testing
Confirmed that keyboards and mice can be hot unplugged and plugged.
2026-07-18 17:38:59 -07:00
bicarus 61a0220c1a audio: retain WASAPI clients only after initialization (#807)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #782 

## Description of change

This addresses an issue only seen on Linux + WINE.

Defer retaining WASAPI clients until `Initialize` succeeds.

Before this PR, temporary clients created during DirectShow device
capability probing were retained as active audio clients, causing
Wine/Linux crashes in DDR and popn.

Also covers IAudioClient3, dummy, ASIO, WaveOut, and null-device
initialization paths, for consistency.

## Testing
WIP
2026-07-18 03:09:37 -07:00
bicarus c080bbe301 rawinput: avoid deadlock when binding analog axis as a button (#809)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #793

## Description of change
Due to lock inversion, when binding an analog axis as a button, spice
deadlocks. Fix that.

Also create a separate `unordered_map` that keeps track of device
handles so that `WM_INPUT` handle can look up devices without having to
acquire the larger `devices_mutex` which could be held by (potentially)
lengthy operations like hotplug.

Fix more synchronization issues around hotplug. Latent bug exposed by
MIDI 2.0 issues.

## Testing
2026-07-18 03:09:27 -07:00
bicarus e1d1b39567 patcher: refactor (#808)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Refactor patch manager.

This one file had the UI logic, config save/load, parsing patches JSON,
applying memory patches, everything all in one cpp file. Refactor and
separate out the layers.

## Testing
Sanity checked patch workflows.
2026-07-17 03:48:01 -07:00
bicarus-dev 2aae8ad9ca move updater scripts 2026-07-15 02:14:19 -07:00
bicarus 1b6d1af951 jb, shogikai: double buffer overlay, run at ~60fps (#805)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
GDI bit blit overlay used exclusively for jubeat & shogikai was running
at 30fps, which caused the Jubeat touch debug UI to not render fast
enough. Run it at 60 fps, and also perform double buffering to avoid
flickering.

This could have been scoped to JB but I made the change for both. XP
builds will keep running at 30fps since those people run on cabs with
ancient hardware.

## Testing
Tested jubeat and shogikai, overlay renders without flickering at 60 fps
2026-07-15 01:31:29 -07:00
bicarus 5c3617c05f rawinput: fix ttl logic in raw touch (#806)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fixes held touch contacts briefly dropping under rapid multi-touch (e.g.
holding several jubeat buttons and tapping elsewhere many times would
make the holds briefly release and the hold again).

A HID digitizer reports a fixed number of contact slots per report and
cycles the rest through later reports, so the touch layer keeps an
unreported contact alive for a few reports (a per-contact TTL). The TTL
was only set for contacts in the current report and had no slack, so
when the contact count rose (from the rapid taps) the reporting cycle
got longer and stationary holds were expired before the device cycled
back to them.

Now, the cycle length is recomputed every report and every tracked
contact's TTL is extended to it (never shrunk), so a rising contact
count can't starve a currently held contact. The 50 ms last-report
timeout still removes genuinely lifted contacts.

## Testing
Tested on jb, rb.
2026-07-14 23:46:17 -07:00
bicarus a9a943fbfe jb: fix card not detected on title screen (#804)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fix a long standing issue with card reader behavior in jubeat.

sciunit emulation has a bug - when on the title screen / demo loop,
inserting a card did not generate an interrupt. Users always had to
press the game start button and card in. Fix this by reversing what the
actual sciunit does and mirroring the state machine behavior.

## Testing
Tested L44 and K44.
2026-07-14 17:06:00 -07:00
bicarus-dev b094a9e352 deprecate insert card button 2026-07-14 02:49:46 -07:00
bicarus 4bd18afb12 rb: fix alignment of vertical lines in debug view (#803)
they were off by third of a column
2026-07-14 00:18:55 -07:00
bicarus 0b24e703a1 overlay: re-enable keyboard navigation (#802)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #800 

## Description of change
Restores keyboard navigation by default in spicecfg, and add an option
to optionally enable it in the overlay.

## Testing
2026-07-14 00:18:21 -07:00
bicarus 1a1be7a393 rb: touch debug overlay, tweak scaling (#801)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Similar to #797, draw an overlay to debug touch issues for reflec beat.

For rb, we draw on the ImGui surface. Draw horizontal and vertical line
for triggered sensors, and also a bounding box if scaling is in use.

Make small tweaks to how scaling works.

## Testing
Verified volzza 2 / reflesia / 1st game (landscape KBR)
2026-07-13 23:30:41 -07:00
bicarus a2d8f60d12 jb: only show debug overlay for jubeat (#799) 2026-07-13 19:42:27 -07:00
bicarus 5d25fdb035 jb: touch debounce (#798)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Debouce option for touch input. Applies to all touch handlers (rawinput,
win8, win7) since it sits at the touch layer, and then consumed by jb
touch logic.

## Testing
2026-07-13 02:06:54 -07:00
bicarus c255e3a1db jb: option to draw debug graphics to show touch interactions (#797)
## Link to GitHub Issue or related Pull Request, if one exists
related to #697 

## Description of change
Add a new option that paints touch targets and touch points for jubeat;
gets enabled if there is a touch screen connected. This is to help
explain / debug issues when users have their touch screen issues, which
is usually due to misalignment.

Fix the touch targets being off by 1 pixel (center gap is 1px wider in
both directions).

Also, simplify the `improved` algorithm to simulate a finger and trigger
nearest-neighbor for the AC dimensions. In terms of touch performance
this is near identical to what we had before, but it allows us to draw a
nice debug overlay instead of just grids.

## Testing
Tested portrait and landscape versions of jb.
2026-07-13 00:31:05 -07:00
bicarus d7c144646f rawinput: deal with high poll rate devices, use 3x3 for default for rb touch emu (#796)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Two changes:

1. Handle all HID events in `WM_INPUT` and not just the first one. This
only really affects 4000Hz / 8000Hz HID devices.
2. Go back to `3x3` as the default for rb IR touch emulation and remove
`1x1` as an option as it does not perform as well as I expected.

## Testing
2026-07-12 10:32:29 -07:00
bicarus-dev 27077aa21f bump action-gh-release version 2026-07-12 01:45:16 -07:00
bicarus-dev 632dcf7b41 clean up option text for consistency 2026-07-12 00:00:31 -07:00
bicarus a25bbdc9c9 rawinput: refactor midi (#795)
pure movement of code only, no functional changes
2026-07-11 23:59:34 -07:00
bicarus 47d886306e rawinput: handle midi scanning asynchronously, fix midi hotplug (#793)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #603

## Description of change
Scanning for midi devices can take a while on Windows 11 with MIDI 2.0
service. Sometimes it'll take a couple seconds. Sometimes, on a PC with
zero MIDI devices, it takes 10-11 seconds.

This was causing two issues:

1. slow startup time
2. crash on invalid memory access due to a race condition

Address both.

Also fix a bug: when a device change event fires, we do a MIDI scan, and
invalidated all existing MIDI devices in favor of creating new handles.
Stop doing this, and instead check for duplicates by matching the ID and
keep existing device handles alone. Properly clean up devices on unplug.

Also: fix MIDI buttons being stuck on when unplugged while holding a
key.

## Testing
Tested with Nostroller in MIDI mode. rtpMIDI works too.
2026-07-11 21:57:55 -07:00
bicarus-dev edda658fa4 update rawtouchaspect text 2026-07-11 21:16:34 -07:00
bicarus-dev 1ab20f1b56 update wintouch option text 2026-07-11 21:06:29 -07:00
bicarus 0e9093a74b xinput: allow for newer xinput DLL (#794)
Fixes #792
2026-07-11 03:04:49 -07:00
bicarus f52eaea76c touch: fix indicators showing in touch overlay, disable additional gestures (#791) 2026-07-11 00:24:36 -07:00
bicarus c3fcf2f417 touch: rawinput touch to compensate for letterboxing (#790)
## Link to GitHub Issue or related Pull Request, if one exists

Fixes #697

## Description of change

Adds aspect-ratio compensation for raw input touch handler.

This addresses the problem with playing rawinput touch games - namely
jubeat and reflec beat - on a display that is not 16:9, and user chose
to preserve the aspect ratio. This happens a lot on:

* 16:10 touch screens
* touch screen laptops (which range from 3:2, 16:10, and so on).

Root cause is a **Windows Raw Input limitation**. Unlike
`WM_POINTER`/`WM_TOUCH`, where the OS already maps digitizer coordinates
to screen space (applying the display's calibration/scaling), Raw Input
delivers the HID digitizer's *raw* logical coordinates, spanning the
whole physical panel, with **no calibration or display-mapping data
attached**. We have to reconstruct that mapping ourselves.

When a game runs at a display mode whose aspect ratio differs from the
panel's native resolution, Windows scales the image with
letterboxing/pillarboxing (black bars). That is standard OS/GPU
behavior. Because Raw Input gives us no mapping to account for those
bars, touches land off-target. This change detects the mismatch and
remaps each touch from panel-space into the displayed image region;
touches inside the black bars are treated as released.

Adds new launcher option **`-rawtouchaspect`**.

Notes:

- Native resolution is the largest-area mode from `EnumDisplaySettings`
on the **primary** display, consistent with the existing primary-only
touch pipeline.
- Math assumes centered, aspect-preserving scaling; only one axis is
ever inset.
- A small epsilon (0.01) makes near-matched panels (e.g. 1366×768 vs
true 16:9) a no-op.
- If the user forces a stretched image, then the touches will be off
again. In this case, the user needs to use the `force off` setting.

## Testing

On my tiny Surface Go tablet, 
* checked that jubeat squares align correctly
* good enough to full combo level 11 for reflec beat
2026-07-10 23:23:45 -07:00
bicarus 8d04eba21d build: check for win7 compat, fix 32-bit xp check (#789)
## Link to GitHub Issue or related Pull Request, if one exists
#787 

## Description of change
Updates usage of `windows_dll_compat_checker`

## Testing
it builds
2026-07-10 01:51:04 -07:00
bicarus 8d42c0ce77 asio: fix XP build; remove call to RegGetValueA (#788)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #787 

## Description of change
Remove the call to `RegGetValueA` as it is Vista and up. Replace with
`RegOpenKeyExA` + `RegQueryValueExA`

## Testing
2026-07-10 00:26:03 -07:00
bicarus-dev 829503568e fix UI text for -sdvxnolive2d 2026-07-06 01:27:18 -07:00
bicarus 746fdb5409 build: manual update scripts (#786)
people asked for this often enough
2026-07-02 23:48:52 -07:00
bicarus 67da892fe9 cfg: aliases for -network and -subnet (#785)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
`-network` and `-subnet` are poorly named. Add new names for them that
are more explicit.

Old command line parameters will continue to work, this is an additive
change.

## Testing
2026-07-01 02:41:14 -07:00
bicarus dc191487e0 Update CONTRIBUTING.md 2026-06-30 17:45:23 -07:00
bicarus 5c0c405800 Update CONTRIBUTING.md 2026-06-30 14:32:30 -07:00
bicarus-dev 60166e6ba1 update gh actions 2026-06-30 02:29:44 -07:00
bicarus cb0b59600a build: GitHub action for creating a draft relase (#783) 2026-06-30 00:53:08 -07:00
bicarus b9f3be4df2 asio: cache result from CoCreateInstance to work around buggy asio drivers (#781)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
IIDX likes to call `CoCreateInstance` and `init` multiple times on ASIO
drivers, but some ASIO drivers like `Neva Uno` really don't like that
and ends up crashing. In our wrapper, cache the instances and try to
reuse them for better compatibility.

## Testing
Worked fine for FlexASIO / Xonar AE / Realtek ASIO though none of these
drivers repro the crashing behavior.
2026-06-28 22:37:48 -07:00
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
bicarus 2b930ad95f gitadora: warn about 2ch audio option not working on arena model (#702)
shrug
2026-05-25 17:00:49 -07:00
bicarus 3c60f3966b rb: more touch options (#699)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Add two options:

1. option to adjust poll rate
2. option to change emulation size

Also, the default configuration is changing from:

* ~1000Hz ish to 120Hz (default)
* 3x3 point to 1x1 point

In theory this does not make a meaningful change to how the game plays.
But if people want the old behavior they can change it.

## Testing
Tested on Volzza2 and Reflesia.
2026-05-25 16:02:17 -07:00
Biduleman c59d399ab8 Automated PIN Macro after auto-inserting a card (#698)
## Description of change
Adds a way to automatically use the PIN Macro for P1, P2 or both when
Auto Card Insert is used.

### Launcher changes: 
 - Added a checkbox to enable/disable the feature
 - Added a textbox for each player to set what to look for in the logs.

### How it works:
The feature requires Auto Card Insert and PIN Macro to be enabled and
set.
It uses the Auto Card Insert value to enable the feature for P1, P2 or
both, with a "master" checkbox to completely disable the feature. It
then relies on the current PIN Macro code to send the PIN to the games.

The games logs send a message when a new scene is loaded with the name
of the scene. By hooking on the logs, we can see when a PIN is required
and send the relevant macro. Games have different scene names, and
sometimes one for each player, so the field is configurable per game per
player. These could be hard-coded in a lookup table but I didn't want to
recompile the project every time I wanted to test, and I don't have all
the games to get this info from.

The PIN Macro thread is used to hook to the logs and check if the
defined string appears, which triggers the PIN macro just like a button
press would.

Setting nothing for the log trigger disables the feature for that
player. It's useful if someone wants to have Auto Card Insert enabled
for both players but only Auto Pin on 1 player, but is mainly there so
the check won't happen if nothing is set.

## Testing
I used SDVX and DDR to have a single and a two player test with the
feature both enabled and disabled. I also tested multiple combinations
of PIN Macro settings and Auto Card Insert settings with no apparent
issues. The PIN is automatically entered as soon as the game requests
it.

## Discussion

I know that hooking to the logs might not be the best solution. I'm very
open to discussions about this, and understand that this might be a deal
breaker for this feature. I made this to use with my setup and thought
I'd try opening this PR to see if the feature would be useful for
others.

I haven't found another way to make sure we're on the correct screen to
launch the PIN Macro, and sending the macro every time the Auto Card
Insert finishes had sync issues where the end of the PIN would actually
be registered by the game.

It would also be much more convenient for the users to hardcode a lookup
table for every games' log-trigger but I don't have the games currently
so I can't check the logs to see what would be best.

---------

Co-authored-by: Maxime St-Pierre <maxime.stpierre@ticketmaster.com>
2026-05-25 16:00:06 -07:00
bicarus f69e40fa26 gitadora: (arena model) booting fullscreen with one monitor (#696)
## Link to GitHub Issue or related Pull Request, if one exists
#477 

## Description of change
By default the game needs 4 monitors.

With the new option to disable subscreens, you can now boot fullscreen
with a single 4K monitor, with subscreen overlay enabled. Side monitors
are not shown in the overlay; only the touch screen is emulated.

## Future work

- [ ] custom resolution (-forceres) doesn't work properly (hooking
ResetEx does allow the game to boot but will render incorrectly;
probably needs hex edits)
- [x] enable this automatically if the user has fewer than 4 monitors
- [ ] ability to boot with a 4k main screen + small subscreen (with no
side screens which do nothing gameplay wise), but this whole display
emulation is a nightmare to work with & test thoroughly so it'll have to
be in the future.


## Testing
Checklist:

- [x] fs on 1 monitor with option set
- [x] fs on 2 monitor with option set
- [x] windowed - default (4 windows, no overlay)
- [x] windowed - with option set (1 window, overlay)
- [x] fs with 4 monitors?
- [x] check popn hc for regression
2026-05-18 15:28:12 -07:00
bicarus e9dcc5717e launcher: option to run as user without elevation (#695)
## Link to GitHub Issue or related Pull Request, if one exists
#694 

## Description of change
Before this change, we have it set in the executable manifest to always
ask for UAC elevation.

This change removes that and instead attempts to prompt for elevation
when we launch, depending on the new developer option that is being
added here to run as current user instead of elevating.

The default behavior continues to run as admin. This is still the safest
in terms of regression risk.

A bit of history on this: while spicecfg never technically needed to
elevate, we continue to do so to avoid difference in behavior when the
game is launched. For example, if the user is using an input remapper
running without elevation, it would inject input into spicecfg running
as user but fail to do so if the game runs elevated.

WinXP builds are unaffected (there is no UAC).

Some features will straight up fail without elevation so this will
remain as a debug option. To name a few (not a complete list):

* reboot/shutdown of PC
* running at realtime process priority
* `-nvprofile` option
* ...

## Testing
2026-05-16 16:27:59 -07:00
drmext 5bec3d5db7 network: icmp emulation (#694)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Add `-icmphook` option under spicecfg Development/Network for emulating
keepalive ping replies in user mode, so games do not need to open
privileged raw ICMP sockets. This makes connecting to networks possible
on macOS under Whisky, and on Windows with ICMP firewalled or spice
running without Administrator privileges.

## Testing
Tested DDR and SDVX on Windows 10 and macOS Tahoe. Network check status
in test menu shows `CONNECTED` when -icmphook is enabled, and `NOT
CONNECTED` when it is disabled. Actual network functionality works as
expected (under previously impossible conditions).
2026-05-14 00:44:14 -07:00
bicarus-dev 6ec2b47345 fix option text 2026-05-13 17:54:25 -07:00
bicarus ebda16beae rb: update reflec beat to use new timer (#692)
## Link to GitHub Issue or related Pull Request, if one exists
Continuation of #682

## Description of change
RB uses SleepEx, so it was missed during the search while working on
#682.

## Testing
Plays about the same on a 200Hz touch monitor.
2026-05-10 19:47:33 -07:00
bicarus c29a26f183 graphics: hide subscreen window from task bar and alt+tab list when in full screen (#690)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
When booting in fullscreen, hide the subscreen from the alt+tab list and
from the task bar. Alt-tabbing to it does nothing, so this is just to
avoid confusion.

No change to windowed mode.

## Testing
Tested TDJ/SDVX/Popn. Didn't test Gitadora but it's probably fine..
2026-05-10 05:33:18 -07:00
bicarus 2014e9cf75 graphics: option to change monitor resolution (#689)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Add a new option under `Monitor` group to change the monitor resolution
before booting the game, very similar to how orientation / refresh rate
options work.

This would be useful for games like Polaris Chord that boots at current
resolution but manages to have graphical issues when not in 1080p.

## Testing
2026-05-09 21:32:43 -07:00
bicarus 33f2e1d4b7 rawinput, touch: attempt to remove touchscreens injecting mouse events (#687)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #562

## Description of change

### rawinput touch fixes
In rawinput touch handler, if the mouse events (`WM_LBUTTONDOWN`,
`WM_MOUSEMOVE`, `WM_LBUTTONUP`) originated from a touch screen, drop
them on the floor instead of treating them as touch events.

This is an attempt to fix phantom touch issues we see with some screens.

Fix another issue where, if mouse is clicked inside the window, and then
dragged outside the window and button is released, touch point (id 0)
continues to be held.

### wintouchemu fixes
If we detected a touch input recently (500ms), ignore new mouse clicks.
This isn't perfect, but hopefully it helps with filtering out mouse
clicks originating from touch screens.

Also, switch over to detecting the primary mouse button correctly, not
just the left mouse button.

### notes

Future investigation: are there really touchscreens that report a mouse
HID capability & insert mouse events? We could have a way to filter them
at rawinput level but it'll be ugly.

## Testing
Tried jubeat, beatstream..
2026-05-09 17:52:43 -07:00
bicarus eb037542b4 jubeat: introduce new algorithm for touch (#686)
## Description of change
There was enough feedback that the current algorithm is not ideal for
most touch screens because most touch screens are not 24". In the
current algorithm touching the gap is no-op, so people think the game is
dropping touches.

This PR introduces a new algorithm that inspect touches on the gap and
triggers the nearest square.

## Testing
Tested landscape and portrait jubeat.
2026-05-09 04:22:29 -07:00
bicarus 039b42ad53 fix build (#684) 2026-05-08 22:46:29 -07:00
bicarus 33b4718744 overlay: fix mouse events queued up while overlay is hidden (#683)
## Link to GitHub Issue or related Pull Request, if one exists
continuation of #677

## Description of change
When overlay was hidden, when mouse buttons were clicked, they were
queued up and replayed when overlay became visible again.

This is due to improper usage of `GetAsyncKeyState`, per documentation.

## Testing
2026-05-08 03:37:46 -07:00
bicarus 71ba9b6b47 os: implement win10 high-resolution timer as replacement for Sleep() / sleep_for() (#682)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #681 

## Description of change
`Sleep` and `sleep_for()` can be very inaccurate and varies depending on
what the OS gives us...

### `timeBeginPeriod(1)`
On boot, we are now calling `timeBeginPeriod(1)`, which affects the
whole process but makes `Sleep` more accurate. There is some risk here
if any game was relying on doing things like `Sleep(1)` and expecting it
to run for 15.6ms. Most games already call `timeBeginPeriod(1)` in the
game engine, though not the whole time, so I'm hoping that this is not
too impactful.

### Opt out of Win11 power throttling
Ensure that timer resolution change above is respected even when the
window is occluded / minimized by opting out of throttling via
`PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION`.

### Use Win10 high resolution timer instead of Sleep

On Win10 1803 and above, there is a new OS-level API for high resolution
timers; if this is available, use it
(`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`). Worth noting that WINE
doesn't support this currently.

If not, fall back to `Sleep`, which is significantly better than
`sleep_for()` in my experiments.

Callers of Sleep / sleep_for were replaced with this new timer. Most of
them anyway; calls to Sleep() with more than 100ms+ was left alone.

### Add an option as a chicken bit

To opt out I'm adding a new option called `Use Legacy Timers` which will
revert to behavior before this PR. The code paths that switched from
`sleep_for` to `Sleep` will remain in place though, not affected by the
option.

## Expected changes
In some I/O emulation modules, poll threads may run more frequently,
resulting in lower latency.

It also means that spice overall may use more CPU resources and power.
If you don't like this, you can always enable the option to opt out;
e.g., if you're on old arcade cab PC.

## Testing

DDR p4io - ok
drs touch hook - ok
IIDX camera hook - ok
CCJ trackball - ok
2026-05-08 02:25:20 -07:00
bicarus 1957917270 sdk: update cpp sample for DDR, fix DDR MDXF polling when api/sdk is providing input (#679)
## Link to GitHub Issue or related Pull Request, if one exists
#676 
#452 

## Description of change

When spice SDK or API sets or clears button and analog override, we
don't tell MDXF about it, so it may fall behind. This is a special case
for DDR MDXF I/O emulation. For Naive/Xinput I think we are getting
lucky because they usually also generate WM_INPUT events.

Update the C++ SDK sample to provide more interesting example of
emulating DDR arrows with the keyboard.

## Testing
2026-05-07 18:10:14 -07:00
bicarus b9ccddf47d launcher: prevent early hook from launching in standalone configurator (#678)
## Description of change
Seems to be an oversight when it was initially implemented; hooks should
not load when launching `spicecfg` (or `spice -cfg` or `spice64 -cfg`).
2026-05-07 00:26:18 -07:00
bicarus cd0ba51b5a sdk: introduce Spice SDK (#676)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds a flat-C, headers-only library for writing DLL plugins with spice.

This is meant to be an alternative to writing applications over spice
API which doesn't suit some scenarios like writing custom
high-performance I/O modules.

All you have to do is:

1. Create a 32-bit or 64-bit DLL with `spice_sdk_entry_point` as a
DLLExport
1. Include `spicesdk.h`, and optionally `spicesdk_io.h` (see
`extras\sdk\include`)
1. `spice_sdk_entry_point` will be called when hooks get initialized.
Call `init` with valid parameters.
1. When `init` returns, `SPICE_SDK_V0` will be filled out with a series
of function pointers; your DLL can now start calling into it.
1. Compile your DLL and add it as a DLL hook (`-k`) in spice when
launching the game.

See `v0_cpp.cpp` for an example.

V0 implements the following functions:

```
    log;

    get_game_info;
    get_avs_info;

    get_button;
    set_button;

    get_analog;
    set_analog;

    get_light;
    set_light;

    set_touch;
    clear_touch;

    insert_card;
    set_keypad;
```

V0 is focused on providing common utility functions needed in a lot of
DLL hooks (get AVS info, log messages to file), and implementing I/O.

In the future, we could have more interesting things like:

* scanning and modifying memory 
* installing DLL hooks
* having direct hooks into I/O emulation modules
* getting full tape LEDs
* having a callback into DX9 `Present` call
* drawing an ImGui window

More detailed documentation is coming soon in form of a wiki page.

## Testing
See included sample DLL.
2026-05-06 23:29:52 -07:00
bicarus 1f23d88c7a overlay: prevent overlay from queueing up keyboard input while overlay is not visible (#677)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #675

Yet another regression from #604 

## Description of change
When overlay was hidden (not visible) it was still accepting keyboard
input, so when it became visible again the queued up keystrokes got
processed all at once.

Also, fix a bug with monitor settings update kicking in when in windowed
mode - this really should have been only for full screen.

## Testing
Tested DDR windowed / full screen, TDJ with subscreen overlay windowed /
fullscreen.
2026-05-06 01:00:52 -07:00
bicarus-dev 46e43ab09c fix typo 2026-05-04 23:57:35 -07:00
bicarus 96950b6b4e rawinput: reimplement analog relative mode and delay (#674)
## Link to GitHub Issue or related Pull Request, if one exists
#181 

## Description of change
Implement analog relative mode and delay option using a new
millisecond-based algorithm.

## Testing
WIP
2026-05-04 22:50:11 -07:00
bicarus 5f94ef4478 overlay: fix scroll bar jumping around when regaining focus (#673)
## Link to GitHub Issue or related Pull Request, if one exists
Regression caused by #604, probably

## Description of change
When spicecfg or the game loses focus and regains it, scroll bar jumps
around. This was because we weren't keeping proper track of mouse wheel
position while not in focus, so when we gain it back, it's as if the
scroll bar was moved a huge distance.

## Testing
Tested spicecfg and in overlay.
2026-05-03 03:49:52 -07:00
bicarus c4193eddc5 iidx: millisecond-based delay option (#672)
## Link to GitHub Issue or related Pull Request, if one exists
#181 

## Description of change
Use millisecond-based calculation to add artificial delay to the TT
input.

## Testing
Tested 64 bit TDJ and LDJ.
2026-05-03 01:23:45 -07:00
bicarus c8962a0e77 overlay: setting button as always-on (#671)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Add a checkbox to the buttons edit dialog that lets you set a device as
"always on", accomplished by setting the device to `Naive`, vkey to
`0xff` (invalid), and `Invert`.

No backend rawinput changes.

## Testing
2026-05-02 22:42:36 -07:00
din b6a749e5db add snek to lights match map (#670)
## Description of change
add snek board to lights mapping

## Testing
with a snek board


<img width="490" height="477"
alt="DeepinScreenshot_select-area_20260502164249"
src="https://github.com/user-attachments/assets/a86affff-74c6-4d5d-b657-fab12c40aaa4"
/>
2026-05-02 15:49:19 -07:00
bicarus-dev 7dbbe9809e update iidx tt delay option text 2026-05-02 02:50:29 -07:00
bicarus 79a3e32d5d iidx: delay logic for iidx tt (#669)
## Link to GitHub Issue or related Pull Request, if one exists
Re-implements fix for #181 as it was removed by #668

## Description of change
Add two options to add artificial delay to turntable input. These
operate directly on the input logic (`get_tt`) which is called by I/O
emulation code.

Functionally, it's **identical** to the `Delay (experimental)` option we
had in the analog tab, just moved to Options tab.

## Testing
64 bit tdj: ok
64 bit ldj bi2a: ok
32 bit ldj ezusb: ok

Worth noting that TDJ bi2x_hook polls at 120Hz, bi2a at ~500Hz, and
ezusb at ~170Hz
2026-05-02 02:17:51 -07:00
bicarus c5a4e954f9 rawinput: remove experimental analog features (delay and relative axis mode) (#668)
## Link to GitHub Issue or related Pull Request, if one exists
Related to #181 

## Description of change
This PR removes two features marked as "experimental" for analog axis -
`Delay` and `Relative Axis`.

They have been in spice as experimental option for almost 2 years.. but
they have a fundamental problem of being tied to how often the analog is
polled. Not just how often the game's I/O emulation code polls it, but
literally any source (spice API, overlay, etc).

For `Delay`, the only real usage I'm aware of is for #181 which is
delaying IIDX turntable input. For this, I will follow up with a
separate PR that adds back the delay option specifically for IIDX.

For `Relative Axis` - I'm not quite sure if any one actually uses this.
This could be implemented per-game if some game demands it. Polaris
Chord already has one, for example.

## Testing
2026-05-02 00:51:21 -07:00
bicarus 7b862768b0 overlay: show name of analog control in popup title (#667) 2026-05-02 00:01:33 -07:00
bicarus af8d8dae9f rawinput: distinguish between circular and linear analog input (#665)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Currently, all analogs are treated as circular values that wrap around.
This works for knobs and turntables, but can get weird for linear values
(like tilt sensors and sliders) especially once analog options are
enabled, such as sensitivity.

This PR groups analogs into 3 buckets: circular, linear (centered), and
linear (positive). Linear types get different algorithms applied to it
when options are set (e.g., sensitivity caps out at 0, 1 instead of
wrapping around).

In linear mode:
* Sensitivity value applies an exponential response curve (power curve)
to the original value.
* Multiplier/divisor serves as a linear cut-off.
* Relative mode works as you expect, but of course, values don't wrap
around.
* Invert and deadzone work as you expect (since they applied to the
controller input anyway)
* Smoothing is removed from the UI and has no effect.
* Delay is unaffected.

Also, fix a small bug with -/+ button not working for analog `Delay`
option.

## Testing
WIP
2026-05-01 19:02:32 -07:00
llm96 d9d5823fdb rawinput: add missing nullptr check (#666)
## Link to GitHub Issue or related Pull Request, if one exists
N/A

## Description of change

Fixes a crash when opening the properties modal on a Naive button bind.


https://github.com/spice2x/spice2x.github.io/blob/82dda3380039bbaa995ee99e2d3effebbcb58082/src/spice2x/overlay/windows/config.cpp#L2081

Binds:
<img width="800" height="600" alt="image"
src="https://github.com/user-attachments/assets/18d1fcfb-085f-4a2b-8fce-36809a4bbb5a"
/>

Stack trace from Debug build:
```
overlay::windows::Config::edit_button_popup(const std::string &, const std::string &, Button *, float, int) config.cpp:2081
overlay::windows::Config::build_button(const std::string &, Button &, Button *, int, int, int) config.cpp:1050
overlay::windows::Config::build_buttons(const std::string &, std::vector<…> *, int, int) config.cpp:718
overlay::windows::Config::build_content() config.cpp:283
overlay::Window::build() window.cpp:96
overlay::SpiceOverlay::new_frame() overlay.cpp:486
graphics_d3d9_on_present(HWND__ *, IDirect3DDevice9 *, IDirect3DDevice9 *) d3d9_backend.cpp:1447
__ZN23WrappedIDirect3DDevice97PresentEPK7tagRECTS2_P6HWND__PK8_RGNDATA@20 d3d9_device.cpp:396
main_implementation(int, char **) launcher.cpp:2453
main launcher.cpp:2671
```

Relevant snippet from error log:
```
[2026/05/01 18:50:17] M:config: tab selection changed from 0 to 1
[2026/05/01 18:50:48] W:signal: exception raised: EXCEPTION_ACCESS_VIOLATION
[2026/05/01 18:50:48] W:troubleshooter: 

/-------------------------- spice2x auto-troubleshooter -----------------------\

  spice2x version: 2026-04-23
  game version: LDJ:J:A:A:2013090900

  the game has crashed
    * share this entire log file with someone for troubleshooting (log.txt)
    * spice will also attempt to create a minidump (minidump.dmp)
        minidump should only be shared with people you trust as it may contain
        sensitive data (PCBID, card ID, etc)

  log level is set to `disable` (either in avs-config.xml or using -loglevel)
      this log file may have omitted important error messages from the game
      if you are troubleshooting crashes or failures, it is recommended that you
      set AVS Log Level (-loglevel) option to `all`

  unsure what to do next?
    * update to the latest version:
        https://github.com/spice2x/spice2x.github.io/releases
    * check the FAQ:
        https://github.com/spice2x/spice2x.github.io/wiki/Known-issues

\------------------------- spice2x auto-troubleshooter ------------------------/

[2026/05/01 18:50:48] I:signal: printing callstack
[2026/05/01 18:50:48] I:stackwalker: 006A7001 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0068DF8C (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0069A7C1 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0069B857 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0066461F (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 00663B7A (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0045B548 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 004FE1CE (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 1008AD13 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 100934A7 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 10088D24 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 0044B8A6 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 77290CB7 (ntdll): (unknown): RtlGetSystemTimeAndBias
[2026/05/01 18:50:53] F:signal: end
[2026/05/01 18:50:53] I:launcher: stopping subsystems
[2026/05/01 18:50:53] I:logger: stop
[2026/05/01 18:50:53] I:audio: stopping
[2026/05/01 18:50:54] I:rawinput: disposing devices
[2026/05/01 18:50:54] I:xinput: destroyed
```

## Testing
Replicated on upstream build from `spice2x-26-04-23-full.zip`
2026-05-01 14:42:45 -07:00
bicarus 82dda33800 graphics: restore monitor settings when fullscreen is lost then restored (#664)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #663 

## Description of change
Apply monitor rotation and refresh rate again when fullscreen focus is
regained.

## Testing
Tested with sdvx7
2026-04-28 16:04:52 -07:00
bicarus 3876e38636 xinput: small fixes (#661)
## Link to GitHub Issue or related Pull Request, if one exists
#616 

## Description of change
Fix UI labels being wrong for analog stick up/down (they were reversed).

Adjust deadzone so that when sticks are bound as digital buttons, they
are not as sensitive.

Small refactoring of code.

## Testing
Tested via spicecfg using my xbox one controller.
2026-04-27 22:19:23 -07:00
bicarus 6bc1357d6a ddr: add support for analog axis-only pads and handle left+right / up+down input (#659)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Some crappy DDR soft mats (either USB-based or console pads with crappy
adapters that lack a dedicated "DDR pad mode") use X and Y analog axis
for arrows, which means they cannot cleanly report Up+Down or Left+Right
input.

According to some ancient Stepmania code, a lot of these report a middle
value (value between center and right, for example, for left+right).
Borrow this hack from Stepmania and implement it in spice as analog
input for DDR.

Also, for rawinput/xinput automatic bind scenario, update the logic so
that face buttons are always detected before analog axis / dpads for
consistency. This would help with binding any DDR pads that
simultaneously input face buttons / dpad / analog axis, so that we can
prefer face buttons when detected.

## Testing
I don't have any soft mats that have this, but it was tested with an
xbox controller.
2026-04-27 20:17:54 -07:00
Will 37218e7fe0 Try and preserve the wideness of std::filesystem::path more (#660)
As noted in #567, a filesystem path that contains non-ascii will break a
lot if using a clang toolchain.

Luckily, fmtlib has a lossy utf8 convert when you use it to print a path
(after including `fmt/std.h`). The vast majority of this diff is just
removing `.string()` from paths inside loggings calls.

There are some callsites I _didn't_ touch, mainly the options, because
it would be an ABI break to change those to be wide strings and I cbf
looking into settings upgrades. There are also some spots (avs mountpath
remapping, for example) where the path is guaranteed to be ascii, so I
didn't modify them.

ImGui doesn't appear to easily support wide strings (I mean, surely it
does, but I'm not gonna look too far into it) so I mostly just left
those alone too, with a few spots modified to re-use fmtlib's lossy
utf8.

Some of the changes are basically never gonna be hit IRL, like who would
put a file with a non-ascii _extension_ along with their modules? But
the diff is (I hope) pretty easy to validate as OK.

Testing has been somewhat minimal, I fired up the GCC build of spice2x
in a dodgy folder name, got mojibake (running via wine in linux so take
that as you will), ran the unmodified clang spice and crashed the same
way the reporter did. After modification, I get the exact same mojibake
so I assume if the terminal enjoys utf8 it'll display OK.

Claude (only used for review) thinks the commit is fine but is annoyed
that I use `fmt::detail` in the appdata censoring, which is part of the
private API; personally I don't care because it's pretty stable.
2026-04-27 20:07:15 -07:00
bicarus 678e11eade rawinput: fix up devices that report invalid max values (#658)
## Link to GitHub Issue or related Pull Request, if one exists
regression caused by #653

## Description of change
Some controllers - like the Xbox One controller I have - reports
0xffffffff as the max range value for analog, despite reporting 16-bit
width. This causes us to consider 0xffffffff as -1 so the range became
[0, -1] which is invalid.

Our automatic calibration does eventually fix this (-1 will fix itself
to become the highest actual value reported, which would be 0xffff).

This seems to be a common issue with XInput-based controllers in
general: see `Correct_Axis_Max` in
https://github.com/argonlefou/DemulShooter/blob/master/DsCore/RawInput/RawInputController.cs

#653 removed the fix that was masking this issue. Put in a new fix that
fixes these up.

## Testing
Tested with Xbox One controller.
2026-04-27 01:41:44 -07:00
bicarus f72313fe45 sdvx: work around bad logger configuration (#657)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change
SDVX4 data from certain private forum has bad logger configuration and
crashes on boot. Add an explicit workaround to fix up that broken
`avs-config.xml` file, scoped to `soundvoltex.dll` + 32-bit.

## Testing
Tested HH.
2026-04-23 01:33:49 -07:00
290 changed files with 31296 additions and 7634 deletions
+5 -3
View File
@@ -11,9 +11,7 @@ assignees: ''
*name of game, version of game*
## 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
*what's the issue?*
@@ -22,3 +20,7 @@ assignees: ''
> [!WARNING]
> 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.
## 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.
+3 -3
View File
@@ -12,12 +12,12 @@ jobs:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set ccache environment variables
run: |
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
- name: Install ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1.2.23
- name: Calculate commit SHA
id: vars
run: |
@@ -25,7 +25,7 @@ jobs:
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Compile
run: ./build_docker.sh
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
+49
View File
@@ -0,0 +1,49 @@
name: Draft Release
on:
workflow_dispatch:
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build and Draft Release
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0
- name: Clean leftover build artifacts
run: |
rm -rf .ccache dist bin cmake-build-*
- name: Compile
run: ./build_docker.sh
- name: Determine release name from dist filename
run: |
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
name="${dist%.zip}"
# the tag is the date portion, e.g. 26-06-28
tag="${name#spice2x-}"
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
- name: Create draft release
uses: softprops/action-gh-release@v3
with:
draft: true
prerelease: true
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ env.RELEASE_NAME }}
target_commitish: main
generate_release_notes: true
files: |
src/spice2x/dist/spice2x-*.zip
+7 -3
View File
@@ -11,13 +11,17 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
### Forbidden "features" that will never be accepted
### Pull requests that will not be accepted
* Performing decryption of encrypted files
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data
* Performing decryption of encrypted files.
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data.
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
* This includes automatic updaters.
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
* Built-in EA server that saves data. They should live in a separate project.
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
* Changes to the software license terms.
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
### Avoiding regressions
+160 -19
View File
@@ -235,6 +235,13 @@ add_compile_definitions(
_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
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_LOG")
@@ -401,14 +408,18 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/iidx/mf_wrappers.cpp
games/sdvx/bi2x_hook.cpp
games/sdvx/sdvx.cpp
games/sdvx/sdvx_live2d.cpp
games/sdvx/io.cpp
games/sdvx/camera.cpp
games/jb/jb.cpp
games/jb/jb_touch.cpp
games/jb/io.cpp
games/nost/nost.cpp
games/nost/io.cpp
games/nost/poke.cpp
games/nost/touch_mode.cpp
games/gitadora/gitadora.cpp
games/gitadora/asio.cpp
games/gitadora/io.cpp
games/gitadora/handle.cpp
games/gitadora/j32d.cpp
@@ -422,6 +433,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/rb/rb.cpp
games/rb/io.cpp
games/rb/touch.cpp
games/rb/touch_debug.cpp
games/bs/bs.cpp
games/bs/io.cpp
games/rf3d/rf3d.cpp
@@ -486,16 +498,24 @@ set(SOURCE_FILES ${SOURCE_FILES}
# hooks
hooks/audio/acm.cpp
hooks/audio/audio.cpp
hooks/audio/asio_driver_scan.cpp
hooks/audio/asio_proxy.cpp
hooks/audio/buffer.cpp
hooks/audio/mme.cpp
hooks/audio/util.cpp
hooks/audio/xact.cpp
hooks/audio/backends/dsound/dsound_backend.cpp
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
hooks/audio/backends/mmdevice/device.cpp
hooks/audio/backends/mmdevice/device_collection.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_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_clock.cpp
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
@@ -510,19 +530,28 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/devicehook.cpp
hooks/graphics/graphics.cpp
hooks/graphics/graphics_windowed.cpp
hooks/graphics/nvapi_impl.cpp
hooks/graphics/nvapi_hook.cpp
hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.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_swapchain.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_device.cpp
hooks/input/dinput8/hook.cpp
hooks/lang.cpp
hooks/libraryhook.cpp
hooks/networkhook.cpp
hooks/icmphook_net.cpp
hooks/icmphook_iphlpapi.cpp
hooks/powrprof.cpp
#hooks/rom.cpp
hooks/setupapihook.cpp
@@ -545,7 +574,6 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp
misc/eamuse.cpp
misc/extdev.cpp
misc/nativetouchhook.cpp
misc/sciunit.cpp
misc/sde.cpp
misc/wintouchemu.cpp
@@ -556,6 +584,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
# overlay
overlay/overlay.cpp
overlay/notifications.cpp
overlay/window.cpp
overlay/imgui/extensions.cpp
overlay/imgui/impl_spice.cpp
@@ -582,12 +611,27 @@ set(SOURCE_FILES ${SOURCE_FILES}
overlay/windows/keypad.cpp
overlay/windows/log.cpp
overlay/windows/midi.cpp
overlay/windows/nostalgia_touch_piano.cpp
overlay/windows/obs.cpp
overlay/windows/obs_websocket.cpp
overlay/windows/patch_manager.cpp
overlay/windows/popn_sub.cpp
overlay/windows/wnd_manager.cpp
# patcher
patcher/config.cpp
patcher/lifecycle.cpp
patcher/loader.cpp
patcher/loader_group.cpp
patcher/runtime.cpp
# external
external/easywsclient/easywsclient.cpp
# rawinput
rawinput/rawinput.cpp
rawinput/rawinput_handles.cpp
rawinput/midi.cpp
rawinput/sextet.cpp
rawinput/piuio.cpp
rawinput/touch.cpp
@@ -607,12 +651,21 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/structuredmessage.cpp
reader/crypt.cpp
# sdk
sdk/sdk.cpp
# stubs
stubs/stubs.cpp
# touch
touch/gdi_overlay.cpp
touch/native/inject.cpp
touch/native/inject_mouse.cpp
touch/native/inject_synthetic.cpp
touch/native/nativetouchhook.cpp
touch/native/transform.cpp
touch/touch.cpp
touch/touch_indicators.cpp
touch/touch_gestures.cpp
touch/win7.cpp
touch/win8.cpp
@@ -633,6 +686,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
util/time.cpp
util/cpuutils.cpp
util/netutils.cpp
util/precise_timer.cpp
util/sysutils.cpp
util/lz77.cpp
util/tapeled.cpp
@@ -644,37 +698,74 @@ set(SOURCE_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
###########
set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/Win32D.rc)
add_executable(spicetools_spice ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_spice
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)
add_executable(spicetools_spice ${RESOURCE_FILES})
target_link_libraries(spicetools_spice PRIVATE spicetools_spice_objs)
set_target_properties(spicetools_spice PROPERTIES PREFIX "")
set_target_properties(spicetools_spice PROPERTIES OUTPUT_NAME "spice")
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()
# spice_laa.exe
###########
set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/Win32D.rc)
add_executable(spicetools_spice_laa ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_spice_laa
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)
add_executable(spicetools_spice_laa ${RESOURCE_FILES})
target_link_libraries(spicetools_spice_laa PRIVATE spicetools_spice_objs)
set_target_properties(spicetools_spice_laa PROPERTIES PREFIX "")
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)
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()
# spice_linux.exe
@@ -707,6 +798,10 @@ target_link_libraries(spicetools_spice64 PUBLIC winscard)
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
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)
set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
@@ -726,6 +821,9 @@ set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
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 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)
set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
@@ -738,7 +836,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)
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
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)
target_link_libraries(spicetools_cfg PUBLIC winscard)
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
@@ -756,7 +854,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)
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
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)
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
@@ -860,6 +958,36 @@ if(NOT MSVC)
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif()
# sdk_sample_v0_flat_c.dll (32 bit)
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
add_library(spicetools_sdk_sample_v0_flat_c_32 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif()
# sdk_sample_v0_flat_c.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
add_library(spicetools_sdk_sample_v0_flat_c_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
endif()
# sdk_sample_v0_cpp.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp)
add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
endif()
# output directories
####################
@@ -869,15 +997,28 @@ set_target_properties(spicetools_cfg spicetools_cfg_linux
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools")
# output 32bit
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32")
# output 64bit
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64 spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
LIBRARY_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()
+10
View File
@@ -283,6 +283,16 @@ Python is the only one that is fully spec compliant.
Other libraries may be missing features and contain bugs; please feel free to
contribute code to fill the gaps if you work on a project using these libraries.
## Spice SDK (for DLL hooks / plugins)
As an alternative to Spice API, Spice SDK is available.
This is a flat C, header-only library that can be included in your DLL. Once
initialized, you can call directly into helper routines provided by Spicetools
executable, making low-latency and high-throughput interactions possible.
Check the documentation on the wiki for more details.
## License
Unless otherwise noted, all files are licensed under the GPLv3.
See the LICENSE file for the full license text.
+3 -1
View File
@@ -4,6 +4,7 @@
#include "launcher/launcher.h"
#include "misc/wintouchemu.h"
#include "rawinput/rawinput.h"
#include "touch/native/nativetouchhook.h"
// static stuff
static int ACIO_WARMUP = 0;
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
// flush device output
RI_MGR->devices_flush_output();
// update wintouchemu
// update touch emulation
wintouchemu::update();
nativetouch::refresh_contact_lifetime();
return 1;
}
+23 -8
View File
@@ -1,13 +1,16 @@
#include "mdxf.h"
#include "mdxf_poll.h"
#include <mutex>
#include "avs/game.h"
#include "games/ddr/ddr.h"
#include "games/ddr/io.h"
#include "launcher/launcher.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include <mutex>
#define MDFX_DEBUG_VERBOSE 0
@@ -108,9 +111,10 @@ static void mdxf_thread_start() {
MDXF_THREAD = std::thread([] {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
timeutils::PreciseSleepTimer timer;
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
mdxf_poll(false);
std::this_thread::sleep_for(THREAD_PERIOD);
timer.sleep(THREAD_PERIOD);
}
});
}
@@ -357,14 +361,17 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
// decide on button map
const size_t *button_map = nullptr;
int player = 0;
switch (node) {
case 17:
case 25:
button_map = &buttons_p1[0];
player = 1;
break;
case 18:
case 26:
button_map = &buttons_p2[0];
player = 2;
break;
}
@@ -374,19 +381,27 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
if (source == EXTERNAL_POLL) {
// get buttons
auto &buttons = games::ddr::get_buttons();
// get analogs
bool analog_left = false;
bool analog_right = false;
games::ddr::get_analog_x_axis(player, analog_left, analog_right);
bool analog_up = false;
bool analog_down = false;
games::ddr::get_analog_y_axis(player, analog_up, analog_down);
uint8_t up_down = 0;
uint8_t left_right = 0;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) {
up_down |= 0xF0;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1])) || analog_down) {
up_down |= 0x0F;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2])) || analog_left) {
left_right |= 0xF0;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3])) || analog_right) {
left_right |= 0x0F;
}
current_state = (uint16_t(up_down) << 8) | left_right;
@@ -500,4 +515,4 @@ acio::MDXFModule::~MDXFModule() {
if (IS_THREAD_NEEDED) {
mdxf_thread_stop();
}
}
}
+12
View File
@@ -3,6 +3,7 @@
#include "rawinput/rawinput.h"
#include "games/nost/io.h"
#include "games/nost/nost.h"
#include "games/nost/touch_mode.h"
#include "util/logging.h"
#include "avs/game.h"
@@ -16,6 +17,7 @@ using namespace GameAPI;
// static stuff
static uint8_t STATUS_BUFFER[277];
static bool STATUS_BUFFER_FREEZE = false;
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
/*
* Implementations
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
};
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
// iterate pairs of keys
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
@@ -257,6 +261,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
if (button0 > 0) {
state0 = button0;
}
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2))) &&
state0 < NOST_TOUCH_PIANO_VELOCITY) {
state0 = NOST_TOUCH_PIANO_VELOCITY;
}
const auto button1 = panb_get_button_velocity(
buttons.at(button_mapping[key_pair * 2 + 1]),
@@ -267,6 +275,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
if (button1 > 0) {
state1 = button1;
}
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2 + 1))) &&
state1 < NOST_TOUCH_PIANO_VELOCITY) {
state1 = NOST_TOUCH_PIANO_VELOCITY;
}
// build value
uint8_t value = 0;
+1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <string>
#include "util/circular_buffer.h"
+3 -1
View File
@@ -5,6 +5,7 @@
#include "games/sdvx/sdvx.h"
#include "misc/eamuse.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
using namespace acioemu;
@@ -46,11 +47,12 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
// keypad thread for faster polling
if (keypad_thread) {
this->keypad_thread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
while (this->cards) {
for (int unit = 0; unit < this->node_count; unit++) {
this->update_keypad(unit, false);
}
Sleep(7);
timer.sleep(7);
}
});
}
+8
View File
@@ -11,6 +11,8 @@
#include "util/logging.h"
#include "util/utils.h"
#include "overlay/notifications.h"
#include "module.h"
#include "modules/analogs.h"
#include "modules/buttons.h"
@@ -197,6 +199,9 @@ void Controller::connection_handler(api::ClientState client_state) {
// log connection
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.emplace_back(&client_state);
client_states_m.unlock();
@@ -277,6 +282,9 @@ void Controller::connection_handler(api::ClientState client_state) {
// log disconnect
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.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
client_states_m.unlock();
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -104,6 +105,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) {
analog.override_enabled = false;
}
mdxf_poll(true);
}
return;
}
@@ -148,6 +150,7 @@ namespace api::modules {
if (analog.getName() == name) {
analog.override_state = CLAMP(state, 0.f, 1.f);
analog.override_enabled = true;
mdxf_poll(true);
return true;
}
}
@@ -167,6 +170,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) {
if (analog.getName() == name) {
analog.override_enabled = false;
mdxf_poll(true);
return true;
}
}
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -107,6 +108,7 @@ namespace api::modules {
for (auto &button : *this->buttons) {
button.override_enabled = false;
}
mdxf_poll(true);
}
return;
}
@@ -153,6 +155,7 @@ namespace api::modules {
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
button.override_velocity = CLAMP(state, 0.f, 1.f);
button.override_enabled = true;
mdxf_poll(true);
return true;
}
}
@@ -172,6 +175,7 @@ namespace api::modules {
for (auto &button : *this->buttons) {
if (button.getName() == name) {
button.override_enabled = false;
mdxf_poll(true);
return true;
}
}
+59 -15
View File
@@ -1,5 +1,7 @@
#include "capture.h"
#include <functional>
#include <mutex>
#include <unordered_map>
#include "external/rapidjson/document.h"
#include "hooks/graphics/graphics.h"
#include "util/crypt.h"
@@ -14,6 +16,56 @@ namespace api::modules {
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") {
functions["get_screens"] = std::bind(&Capture::get_screens, 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
*/
void Capture::get_jpg(Request &req, Response &res) {
CAPTURE_BUFFER.clear();
CAPTURE_BUFFER.reserve(1024 * 128);
// settings
@@ -71,24 +124,15 @@ namespace api::modules {
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
CAPTURE_BUFFER.push_back(byte);
}, 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;
}
// encode to base64
auto encoded = crypt::base64_encode(
CAPTURE_BUFFER.data(),
CAPTURE_BUFFER.size());
// clear buffer
// fall back to the last successful frame while the game is busy loading
CAPTURE_BUFFER.clear();
// 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);
try_cached_response(screen, res);
}
}
+4 -2
View File
@@ -7,6 +7,7 @@
#include "avs/game.h"
#include "external/rapidjson/document.h"
#include "misc/eamuse.h"
#include "util/precise_timer.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -58,6 +59,7 @@ namespace api::modules {
// get params
auto keypad = req.params[0].GetUint();
auto input = std::string(req.params[1].GetString());
timeutils::PreciseSleepTimer timer;
// process all chars
for (auto c : input) {
@@ -91,11 +93,11 @@ namespace api::modules {
// set
eamuse_set_keypad_overrides(keypad, state);
Sleep(sleep_time);
timer.sleep(sleep_time);
// unset
eamuse_set_keypad_overrides(keypad, 0);
Sleep(sleep_time);
timer.sleep(sleep_time);
}
}
+3 -3
View File
@@ -58,7 +58,7 @@ namespace api::modules {
// check if file exists in modules
if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string());
return error(res, fmt::format("Couldn't find {}", dll_path));
}
// get module
@@ -118,7 +118,7 @@ namespace api::modules {
// check if file exists in modules
if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string());
return error(res, fmt::format("Couldn't find {}", dll_path));
}
// get module
@@ -196,7 +196,7 @@ namespace api::modules {
// check if file exists in modules
if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string());
return error(res, fmt::format("Couldn't find {}", dll_path));
}
// get module
+84 -2
View File
@@ -1,5 +1,7 @@
#include "touch.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include "external/rapidjson/document.h"
@@ -8,6 +10,8 @@
#include "misc/eamuse.h"
#include "launcher/launcher.h"
#include "touch/touch.h"
#include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/utils.h"
#include "games/iidx/iidx.h"
@@ -17,6 +21,25 @@ using namespace rapidjson;
namespace api::modules {
static bool inject_native_touch(
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
if (touch_points.empty()) {
// no active touches remain; release the synthetic contact
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
}
// only a single synthetic contact is supported, so just use the first touch point
const auto &touch = touch_points.front();
POINT position { touch.x, touch.y };
if (canvas_w > 0 && canvas_h > 0) {
return nativetouch::inject::inject_synthetic_touch_from_canvas(
position,
{ canvas_w, canvas_h },
true);
}
return nativetouch::inject::inject_synthetic_touch(position, true);
}
Touch::Touch() : Module("touch") {
is_sdvx = avs::game::is_model("KFC");
@@ -26,6 +49,25 @@ namespace api::modules {
is_tdj_fhd = false;
}
use_native = nativetouch::is_hooked();
// resolution the API/companion sends native touch coordinates in (after errata)
native_canvas_w = 0;
native_canvas_h = 0;
if (is_sdvx) {
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
native_canvas_w = 1080;
native_canvas_h = 1920;
} else if (avs::game::is_model("LDJ")) {
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
native_canvas_h = is_tdj_fhd ? 1080 : 720;
} else if (avs::game::is_model("M39")) {
// pop'n music API touch surface
native_canvas_w = 1280;
native_canvas_h = 800;
}
functions["read"] = std::bind(&Touch::read, this, _1, _2);
functions["write"] = std::bind(&Touch::write, this, _1, _2);
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
@@ -99,7 +141,11 @@ namespace api::modules {
}
// apply touch points
touch_write_points(&touch_points);
if (use_native) {
write_native(touch_points);
} else {
touch_write_points(&touch_points);
}
}
/**
@@ -123,7 +169,43 @@ namespace api::modules {
}
// remove all IDs
touch_remove_points(&touch_point_ids);
if (use_native) {
write_reset_native(touch_point_ids);
} else {
touch_remove_points(&touch_point_ids);
}
}
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
if (touch_points.empty()) {
return;
}
std::lock_guard<std::mutex> lock(native_touch_mutex);
const auto touch_id = touch_points.front().id;
if (native_touch_id && *native_touch_id != touch_id) {
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
return;
}
native_touch_id.reset();
}
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
native_touch_id = touch_id;
}
}
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
std::lock_guard<std::mutex> lock(native_touch_mutex);
if (!native_touch_id ||
std::find(
touch_point_ids.begin(),
touch_point_ids.end(),
*native_touch_id) == touch_point_ids.end()) {
return;
}
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
native_touch_id.reset();
}
}
void Touch::apply_touch_errata(int &x, int &y) {
+14
View File
@@ -1,8 +1,12 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <optional>
#include <vector>
#include "api/module.h"
#include "api/request.h"
#include "touch/touch.h"
namespace api::modules {
@@ -13,11 +17,21 @@ namespace api::modules {
private:
bool is_sdvx;
bool is_tdj_fhd;
bool use_native;
std::mutex native_touch_mutex;
std::optional<uint32_t> native_touch_id;
// resolution the API/companion sends native touch coordinates in (after errata);
// used to normalize before mapping into the native injection window
int native_canvas_w;
int native_canvas_h;
// function definitions
void read(Request &req, Response &res);
void write(Request &req, Response &res);
void write_reset(Request &req, Response &res);
void write_native(const std::vector<TouchPoint> &touch_points);
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
void apply_touch_errata(int &x, int &y);
};
+3 -1
View File
@@ -5,6 +5,7 @@
#include <utility>
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
@@ -16,6 +17,7 @@ namespace api {
controller->init_state(this->state);
this->thread = new std::thread([this] () {
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
timeutils::PreciseSleepTimer timer;
// read buffer
uint8_t read_buffer[16*1024];
@@ -160,7 +162,7 @@ namespace api {
// slow down on reconnect
if (this->running) {
Sleep(retry_time);
timer.sleep(retry_time);
}
}
});
+7
View File
@@ -5,6 +5,7 @@
#include "util/utils.h"
#include "util/rc4.h"
#include "util/logging.h"
#include "overlay/notifications.h"
#include "controller.h"
using namespace headsocket;
@@ -91,12 +92,18 @@ namespace api {
// log connection
log_info("api::websocket", "client connected");
overlay::notifications::add(
overlay::notifications::Severity::Success,
"API websocket client connected");
}
void WebSocketClient::on_disconnect() {
// log disconnection
log_info("api::websocket", "client disconnected");
overlay::notifications::add(
overlay::notifications::Severity::Info,
"API websocket client disconnected");
// get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
+16 -7
View File
@@ -1168,7 +1168,7 @@ namespace avs {
// Ongaku Paradise
{"arkjc9.dll", 0xA00000},
// KAMUNITY
// KAMUNITY
{"kamunity.dll", 33554432},
};
@@ -1516,7 +1516,7 @@ namespace avs {
" * It's also possible that you have incomplete game data\n"
" * Do NOT copy over random DLLs from another game installation; DLL must match game version\n"
"\n"
, DLL_NAME, MODULE_PATH.string()) };
, DLL_NAME, MODULE_PATH) };
log_warning("avs-ea3", "{}", info_str);
log_fatal("avs-ea3", "Failed to find critical avs DLL on disk (avs2-core.dll OR {})", DLL_NAME);
}
@@ -1757,9 +1757,9 @@ namespace avs {
{
deferredlogs::defer_error_messages({
"AVS filesystem initialization failure was previously detected during boot!",
fmt::format(" ERROR: directory could not be created: {}", src_path.string().c_str()),
fmt::format(" if you see a crash, it may have been caused by bad <mounttable> contents in {}", avs::core::CFG_PATH.c_str()),
" fix the XML file and try again"
fmt::format(" ERROR: directory could not be created: {}", src_path),
fmt::format(" if you see a crash, it may have been caused by bad <mounttable> contents in {}", avs::core::CFG_PATH),
" fix the XML file and try again"
});
}
@@ -1780,14 +1780,14 @@ namespace avs {
auto created = std::filesystem::create_directories(real_path, err);
if (created) {
log_info("avs-core", "created '{}' at '{}'", avs_path, real_path.string());
log_info("avs-core", "created '{}' at '{}'", avs_path, real_path);
}
if (err) {
avs_dir_err(real_path);
log_warning("avs-core", "failed to create '{}' folder at '{}': {}",
avs_path,
real_path.string(),
real_path,
err.message());
}
}
@@ -1921,6 +1921,15 @@ namespace avs {
// create nvram and raw directories if possible for mounttable configurations
create_avs_config_fs_table(config, config_node);
#if !SPICE64
// sdvx4 bad log config fix
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
property_search_safe(config, config_node, "/log/enable_console")) {
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
property_search_remove_safe(config, config_node, "/log/enable_console");
}
#endif
// set log level
if (!LOG_LEVEL_CUSTOM.empty()) {
property_search_remove_safe(config, config_node, "/log/level");
+4 -4
View File
@@ -151,8 +151,8 @@ namespace avs {
" * It's also possible that you have incomplete game data\n"
" * Do NOT copy over random DLLs from another game installation; DLL must match game version\n"
"\n"
, DLL_NAME, MODULE_PATH.string()) };
, DLL_NAME, MODULE_PATH) };
log_warning("avs-ea3", "{}", info_str);
log_fatal("avs-ea3", "Failed to find critical ea3 DLL on disk (avs2-ea3.dll OR {})", DLL_NAME);
}
@@ -390,8 +390,8 @@ namespace avs {
// fall back to default PCBID if node is not found
if (!EA3_PCBID[0] && PCBID_CUSTOM.empty()) {
log_warning("avs-ea3", "no PCBID set, falling back to default PCBID value (04040000000000000000)");
PCBID_CUSTOM = "04040000000000000000";
log_warning("avs-ea3", "no PCBID set, falling back to default PCBID value (01201000000000010101)");
PCBID_CUSTOM = "01201000000000010101";
}
// custom PCBID
+4 -5
View File
@@ -73,11 +73,10 @@ namespace avs {
// load game instance
const auto dll_path = MODULE_PATH / DLL_NAME;
const auto dll_path_s = dll_path.string();
log_info("avs-game", "DLL path: {}", dll_path_s.c_str());
log_info("avs-game", "DLL path: {}", dll_path);
// MAX_PATH is 260
if (130 <= dll_path_s.length()) {
if (const auto dll_path_len = dll_path.wstring().length(); 130 <= dll_path_len) {
log_warning(
"avs-game",
"PATH TOO LONG WARNING\n\n"
@@ -93,7 +92,7 @@ namespace avs {
"long, often resulting in random crashes. Move the game contents to\n"
"a directory with shorter path.\n"
"-------------------------------------------------------------------\n\n",
dll_path_s, dll_path_s.length());
dll_path, dll_path_len);
}
// ddr gamemdx.dll user error
@@ -112,7 +111,7 @@ namespace avs {
// file not found on disk
if (!fileutils::file_exists(dll_path)) {
log_warning("avs-game", "game DLL could not be found on disk: {}", dll_path.string().c_str());
log_warning("avs-game", "game DLL could not be found on disk: {}", dll_path);
log_warning("avs-game", "double check -exec and -modules parameters; unless you know what you're doing, leave them blank");
}
+37 -37
View File
@@ -39,8 +39,8 @@
<button name="VEFX" vkey="10" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
</buttons>
<analogs>
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="P1 1" devid="Player 1" index="0"/>
@@ -117,8 +117,8 @@
<keypad_buttons/>
<!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. -->
<analogs>
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="P1 1" devid="arcin (1p)" index="0"/>
@@ -144,34 +144,34 @@
<buttons/>
<keypad_buttons/>
<analogs>
<analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="Key 1 R" devid="Lights 01-14" index="0"/>
@@ -277,9 +277,9 @@
</buttons>
<keypad_buttons/>
<analogs>
<analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights/>
<!-- Gen 1 FAUCETWO in HID mode; should also work for later gens -->
@@ -296,8 +296,8 @@
</buttons>
<keypad_buttons/>
<analogs>
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="BT-A" devid="Lights (MI_02)" index="24"/>
+1 -1
View File
@@ -23,7 +23,7 @@
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
File diff suppressed because one or more lines are too long
+71 -32
View File
@@ -68,8 +68,8 @@ DIST_FOLDER="./dist"
DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip"
DIST_NAME_EXTRAS="spice2x-$(date +%y)-$(date +%m)-$(date +%d)-full.zip"
DIST_COMMENT=${DIST_NAME}$'\n'"$GIT_BRANCH - $GIT_HEAD"$'\nThank you for playing.'
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm"
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux"
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32"
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64"
TARGETS_XP32="spicetools_cfg spicetools_spice"
TARGETS_XP64="spicetools_spice64"
@@ -91,14 +91,20 @@ fi
# is the XP-compatible toolchain installed?
XP_MUST_BUILD=0
BUILD_XP=0
if [ -f "$TOOLCHAIN_WINXP_32" ] && [ -f "$TOOLCHAIN_WINXP_64" ]; then
BUILD_XP=1;
# 64-bit WinXP builds are disabled by default; set to 1 to opt in
BUILD_XP_64_ENABLE=0
BUILD_XP_32=0
BUILD_XP_64=0
if [ -f "$TOOLCHAIN_WINXP_32" ]; then
BUILD_XP_32=1;
elif ((XP_MUST_BUILD > 0))
then
echo "WinXP toolchain not available, aborting"
echo "WinXP 32bit toolchain not available, aborting"
exit 1
fi
if ((BUILD_XP_64_ENABLE > 0)) && [ -f "$TOOLCHAIN_WINXP_64" ]; then
BUILD_XP_64=1;
fi
# determine number of cores
CORES=$(nproc)
@@ -113,12 +119,17 @@ echo "Git Branch: $GIT_BRANCH"
echo "Git Head: $GIT_HEAD"
echo "Toolchain for 32bit targets: $TOOLCHAIN_32"
echo "Toolchain for 64bit targets: $TOOLCHAIN_64"
if ((BUILD_XP > 0))
if ((BUILD_XP_32 > 0))
then
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"
else
echo "WinXP toolchain not available, skipping WinXP builds"
echo "WinXP 64bit builds disabled, skipping WinXP 64bit builds"
fi
echo "Distribution Name: $DIST_NAME"
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}
popd > /dev/null
if ((BUILD_XP > 0))
if ((BUILD_XP_32 > 0))
then
# 32 bit Windows XP
echo ""
@@ -172,9 +183,15 @@ time (
fi
mkdir -p ${BUILDDIR_WINXP_32}
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
else
echo ""
echo "Skipping WinXP 32bit builds, toolchain not specified"
fi
if ((BUILD_XP_64 > 0))
then
# 64 bit Windows XP
echo ""
echo "Building 64bit targets (WinXP toolchain)..."
@@ -185,11 +202,11 @@ time (
fi
mkdir -p ${BUILDDIR_WINXP_64}
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
else
echo ""
echo "Skipping WinXP builds, toolchain not specified"
echo "Skipping WinXP 64bit builds"
fi
echo ""
@@ -197,7 +214,20 @@ time (
echo "==========================="
)
if ((BUILD_XP > 0))
echo ""
echo "Checking Win7 compatibility..."
echo "============================="
if ! command -v windows_dll_compat_checker &> /dev/null; then
echo "WARNING: windows_dll_compat_checker not found, skipping Win7 compatibility check"
else
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64.ini \
${BUILDDIR_64}/spicetools/64/spice64.exe
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64_32bit_dlls.ini \
${BUILDDIR_32}/spicetools/spicecfg.exe \
${BUILDDIR_32}/spicetools/32/spice.exe
fi
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
echo ""
echo "Checking XP compatibility..."
@@ -205,11 +235,17 @@ then
if ! command -v windows_dll_compat_checker &> /dev/null; then
echo "WARNING: windows_dll_compat_checker not found, skipping XP compatibility check"
else
windows_dll_compat_checker -s PREMADE/winxp_x86_64.ini \
${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe
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
if ((BUILD_XP_64 > 0))
then
windows_dll_compat_checker -s PREMADE/winxp_x86_64.ini \
${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe
fi
if ((BUILD_XP_32 > 0))
then
windows_dll_compat_checker -s PREMADE/konami_winxp_jubeat_i686.ini \
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
fi
fi
fi
@@ -269,7 +305,10 @@ rm -rf ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux
if ((BUILD_XP > 0))
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
mkdir -p ${OUTDIR_EXTRAS}/updater
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
mkdir -p ${OUTDIR_EXTRAS}/winxp
fi
@@ -281,16 +320,8 @@ then
cp ${BUILDDIR_32}/spicetools/spicecfg-pdb.pdb ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.pdb ${OUTDIR} 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.pdb ${OUTDIR} 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
else
# release files
cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null
@@ -298,20 +329,22 @@ else
cp ${BUILDDIR_32}/spicetools/32/spice.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice_laa.exe ${OUTDIR_EXTRAS}/largeaddressaware/spice.exe 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice_linux.exe ${OUTDIR_EXTRAS}/linux/spice.exe 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64_linux.exe ${OUTDIR_EXTRAS}/linux/spice64.exe 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
if ((BUILD_XP > 0))
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_cpp.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_cpp.dll 2>/dev/null
if ((BUILD_XP_32 > 0))
then
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
fi
if ((BUILD_XP_64 > 0))
then
cp ${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
fi
fi
@@ -326,12 +359,18 @@ then
echo "WARNING: Couldn't get git to create the archive. Is this a git repository?"
fi
# sdk headers
cp -r ./sdk/include ${OUTDIR_EXTRAS}/sdk
# copy resources
rm -rf ${OUTDIR_EXTRAS}/api
mkdir ${OUTDIR_EXTRAS}/api
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
# generate the standalone updater scripts from the shared template and drop them
bash ./build_updaters.sh ${OUTDIR_EXTRAS}/updater
# build distribution archive
if ((DIST_ENABLE > 0))
then
+2 -2
View File
@@ -1,7 +1,7 @@
docker build --pull external/docker -t spicetools/deps
docker build --build-context gitroot=%cd%/../../.git . -t spicetools/spice
docker run --rm -it -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
docker run --rm -i -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
@REM to generate PDBs, set DEBUG to 1 in build_all.sh, place cv2pdb in external\cv2pdb, and run below
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spicecfg.exe bin\spice2x\spicecfg-pdb.exe bin\spice2x\spicecfg-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice.exe bin\spice2x\spice-pdb.exe bin\spice2x\spice-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice64.exe bin\spice2x\spice64-pdb.exe bin\spice2x\spice64-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice64.exe bin\spice2x\spice64-pdb.exe bin\spice2x\spice64-pdb.pdb
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# builds the standalone updater scripts from the shared PowerShell logic in
# update_spice.ps1. each output is a self-contained polyglot .bat: a small batch
# header (with the release channel baked in) followed by the shared PowerShell
# body after the marker line.
#
# usage: build_updaters.sh OUT_DIR
# OUT_DIR directory to write the generated .bat files into
set -eu
if [ $# -lt 1 ]; then
echo "usage: build_updaters.sh OUT_DIR" >&2
exit 1
fi
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PS_BODY="${SRC_DIR}/update_spice.ps1"
OUT_DIR="$1"
[ -f "$PS_BODY" ] || { echo "error: $PS_BODY not found" >&2; exit 1; }
MARKER='#___PS___'
gen() {
channel="$1"; label="$2"; out="$3"
{
cat <<HEADER
@echo off
setlocal
REM ============================================================================
REM spice2x updater - updates spice.exe, spice64.exe and spicecfg.exe in
REM this folder to the latest spice2x release from GitHub.
REM
REM release channel: ${label}
REM make sure spice/spicecfg are NOT running before updating.
REM ============================================================================
REM expose the script folder + channel to the embedded PowerShell, then run the
REM PowerShell part: re-read this file and execute everything after the marker.
set "SPICE_DIR=%~dp0"
set "SPICE_CHANNEL=${channel}"
powershell -NoProfile -ExecutionPolicy Bypass -Command "\$m='#___'+'PS'+'___'; \$code=((Get-Content -LiteralPath '%~f0' -Raw) -split \$m)[-1]; Invoke-Expression \$code"
set "RC=%ERRORLEVEL%"
endlocal & exit /b %RC%
${MARKER}
HEADER
cat "$PS_BODY"
} | sed -e 's/\r$//' -e 's/$/\r/' > "${OUT_DIR}/${out}"
echo " generated ${OUT_DIR}/${out}"
}
echo "Generating updater scripts from $(basename "$PS_BODY")..."
gen "" "stable (pre-releases excluded)" "update_spice.bat"
gen "beta" "beta (newest, pre-releases included)" "update_spice_beta.bat"
+102 -15
View File
@@ -207,22 +207,38 @@ float Analog::applyMultiplier(float value) {
}
float Analog::normalizeAnalogValue(float value) {
// effectively the same as fmodf(value, 1.f)
// for small values, this is MUCH faster than fmodf.
float new_value = value;
while (new_value > 1.f) {
new_value -= 1.f;
if (getType() == GameAPI::Analogs::AnalogType::Circular) {
// effectively the same as fmodf(value, 1.f)
// for small values, this is MUCH faster than fmodf.
float new_value = value;
while (new_value > 1.f) {
new_value -= 1.f;
}
while (new_value < 0.f) {
new_value += 1.f;
}
return new_value;
} else {
// clamp to [0, 1] range
return std::clamp(value, 0.f, 1.f);
}
while (new_value < 0.f) {
new_value += 1.f;
}
return new_value;
}
float Analog::applyDeadzone(float raw_value) {
float value = raw_value;
const auto deadzone = this->getDeadzone();
if (deadzone > 0) {
auto deadzone = this->getDeadzone();
// in the past, positive deadzone applied in the center, negative deadzone applied to 0
// after each analog value received a type (circular/linear) this has been simpliifed to
// positive values only since we can figure out where the rest value is
// for back compat, treat negative value as positive
if (deadzone < 0.f) {
deadzone = -deadzone;
}
// relative mode assumes that user is using a stick, so center is neutral regardless of analog type
if (getType() != GameAPI::Analogs::AnalogType::LinearPositive || isRelativeMode()) {
// calculate values
const auto delta = value - 0.5f;
@@ -255,7 +271,7 @@ float Analog::applyDeadzone(float raw_value) {
}
}
} else if (deadzone < 0) {
} else {
// invert for mirror
if (this->getDeadzoneMirror()) {
@@ -263,8 +279,8 @@ float Analog::applyDeadzone(float raw_value) {
}
// deadzone from minimum value
if (deadzone > -1 && value > -deadzone) {
value = std::min(1.f, (value + deadzone) / (1.f + deadzone));
if (deadzone < 1.f && deadzone < value) {
value = std::max(0.f, (value - deadzone) / (1.f - deadzone));
} else {
value = 0.f;
}
@@ -275,4 +291,75 @@ float Analog::applyDeadzone(float raw_value) {
}
}
return value;
}
}
float Analog::getRelativeModeValue(float raw_value) {
const auto now = get_performance_seconds();
auto delta_time = now - this->rel_mode_last_read_time_s;
if (this->rel_mode_last_read_time_s != 0.f) {
// some heuristics to prevent huge jumps:
// * if we went for more than 250ms without polls, discard it (e.g., during loading screens)
// * cap the delta at 100ms to prevent huge jumps in case of very infrequent polling
if (delta_time < 0.f || 0.25f < delta_time) {
delta_time = 0.f;
} else if (delta_time > 0.1f) {
delta_time = 0.1f;
}
// scale [0, 1] to [-1, 1] to simplify calculations
const auto delta_raw_value = (raw_value - 0.5f) * 2.f;
// target is one revolution per second at max speed at 1.0 sensitivity
auto adjusted_delta_value = delta_raw_value * delta_time;
// multiplier / divisor
if (this->getMultiplier() > 1) {
adjusted_delta_value *= this->getMultiplier();
} else if (this->getMultiplier() < -1) {
adjusted_delta_value /= -this->getMultiplier();
}
// sensitivity
if (this->isSensitivitySet()) {
adjusted_delta_value *= this->getSensitivity();
}
// calculate the new absolute value
this->rel_mode_absolute_value += adjusted_delta_value;
}
// update for next poll
this->rel_mode_last_read_time_s = now;
this->rel_mode_absolute_value = normalizeAnalogValue(this->rel_mode_absolute_value);
return this->rel_mode_absolute_value;
}
float Analog::getDelayedValue(float raw_value) {
const double delay_ms = static_cast<double>(this->getDelayMs());
if (delay_ms == 0.0) {
return raw_value;
}
// always push a new value
const auto now = get_performance_milliseconds();
this->delayed_inputs.emplace(now, raw_value);
// drain the queue down to reasonable length to prevent unconstrained growth
// this would accommodate 1 second at ~1000Hz which is overkill
// (UI only allows for 250ms of delay)
while (this->delayed_inputs.size() > 1024) {
this->delayed_inputs.pop();
}
// pop until we find the oldest value still inside the delay window
while (this->delayed_inputs.size() > 1) {
const auto delta_t = now - this->delayed_inputs.front().time_in_ms;
if (delta_t <= delay_ms) {
break;
}
this->delayed_inputs.pop();
}
return this->delayed_inputs.front().value;
}
+46 -18
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <array>
#include <string>
#include <cmath>
@@ -13,12 +14,32 @@ namespace rawinput {
class RawInputManager;
}
namespace GameAPI::Analogs {
enum class AnalogType {
// default; values wrap around (below 0 turns into 1, over 1 is 0)
// knobs, turntables
Circular = 0,
// typical joystick that rests at the center and caps at [0, 1]
LinearCentered = 1,
// one-directional value (sliders and instruments like piano/drum velocity)
// starts at 0 and goes up to 1
LinearPositive = 2,
};
}
struct AnalogMovingAverage {
double time_in_ms;
float sine;
float cosine;
};
struct AnalogDelayEntry {
double time_in_ms;
float value;
};
class Analog {
private:
std::string name;
@@ -31,10 +52,11 @@ private:
float last_state = 0.5f;
bool sensitivity_set = false;
bool deadzone_set = false;
GameAPI::Analogs::AnalogType type = GameAPI::Analogs::AnalogType::Circular;
// smoothing function
bool smoothing = false;
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history;
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history = {};
int vector_history_index = 0;
float smoothed_last_state = 0.f;
@@ -48,12 +70,13 @@ private:
unsigned short divisor_region = 0;
// relative input mode
float absolute_value_for_rel_mode = 0.5f;
float rel_mode_absolute_value = 0.5f;
float rel_mode_last_read_time_s = 0.f;
bool relative_mode = false;
// circular buffer (delayed input)
int delay_buffer_depth = 0;
std::queue<float> delay_buffer;
// delay
uint32_t delay_ms = 0;
std::queue<AnalogDelayEntry> delayed_inputs;
float calculateAngularDifference(float old_rads, float new_rads);
float normalizeAngle(float rads);
@@ -66,7 +89,8 @@ public:
float override_state = 0.5f;
explicit Analog(std::string name) : name(std::move(name)) {
vector_history.fill({0.0, 0.f, 0.f});
};
explicit Analog(std::string name, GameAPI::Analogs::AnalogType type) : name(std::move(name)), type(type) {
};
std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -74,6 +98,8 @@ public:
float applyAngularSensitivity(float raw_rads);
float applyMultiplier(float raw_value);
float applyDeadzone(float raw_value);
float getRelativeModeValue(float raw_value);
float getDelayedValue(float raw_value);
inline bool isSet() {
if (this->override_enabled) {
@@ -90,8 +116,8 @@ public:
deadzone_mirror = false;
setMultiplier(1);
setRelativeMode(false);
setDelayBufferDepth(0);
setLastState(0.5f);
setDelayMs(0);
}
inline void clearBindings() {
@@ -193,25 +219,27 @@ public:
}
inline void setRelativeMode(bool relative_mode) {
if (relative_mode) {
this->smoothing = false;
}
this->relative_mode = relative_mode;
this->absolute_value_for_rel_mode = 0.5f;
this->rel_mode_absolute_value = 0.5f;
this->rel_mode_last_read_time_s = 0.f;
}
inline float getAbsoluteValue(float relative_delta) {
this->absolute_value_for_rel_mode =
normalizeAnalogValue(this->absolute_value_for_rel_mode + relative_delta);
return this->absolute_value_for_rel_mode;
inline GameAPI::Analogs::AnalogType getType() const {
return this->type;
}
inline int getDelayBufferDepth() const {
return this->delay_buffer_depth;
inline void setType(GameAPI::Analogs::AnalogType type) {
this->type = type;
}
inline void setDelayBufferDepth(int depth) {
this->delay_buffer_depth = depth;
inline uint32_t getDelayMs() const {
return this->delay_ms;
}
inline std::queue<float> &getDelayBuffer() {
return this->delay_buffer;
inline void setDelayMs(uint32_t delay_ms) {
this->delay_ms = delay_ms;
}
};
+181 -67
View File
@@ -3,7 +3,9 @@
#include <cassert>
#include <optional>
#include "games/io.h"
#include "launcher/superexit.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "rawinput/piuio.h"
#include "util/time.h"
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
return sorted;
}
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
namespace GameAPI::Buttons {
static State get_button_state(
rawinput::RawInputManager *manager,
Button &button,
bool check_alts,
bool check_modifiers);
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
}
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
const auto modifier_mask = button.getModifierMask();
if (modifier_mask == 0) {
return true;
}
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
if (!modifier_buttons) {
return false;
}
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
(index >= modifier_buttons->size() ||
get_button_state(manager, modifier_buttons->at(index), true, false) !=
GameAPI::Buttons::BUTTON_PRESSED)) {
return false;
}
}
return true;
}
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
rawinput::RawInputManager *manager,
Button &_button,
bool check_alts,
bool check_modifiers) {
// check override
if (_button.override_enabled) {
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
std::optional<bool> window_has_focus;
while (true) {
// skip bindings whose required modifiers are not held
//
// note that modifiers cannot process MIDI as the logic is written
// below, since MIDI buttons are event-based (on event, off event, etc)
// and cannot be correctly handled by a simple early return
// there is no explicit check for MIDI here, but the UI should have
// prevented it
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
button_count++;
if (!alternatives || alternatives->empty() ||
button_count - 1 >= alternatives->size()) {
return BUTTON_NOT_PRESSED;
}
current_button = &alternatives->at(button_count - 1);
continue;
}
// naive behavior
if (current_button->isNaive()) {
GameAPI::Buttons::State state;
@@ -411,6 +466,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
break;
}
case rawinput::DESTROYED:
// device was unplugged. release the button instead of leaving
// state at getLastState(), which would latch a held button on
// forever (e.g. a MIDI note held while the device is removed)
state = BUTTON_NOT_PRESSED;
break;
default:
break;
}
@@ -463,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
}
}
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
return get_button_state(manager, button, check_alts, true);
}
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
if (manager) {
return getState(manager.get(), button, check_alts);
@@ -478,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
return button.override_velocity;
}
// naive behavior
if (button.isNaive()) {
if (button.getInvert()) {
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
} else {
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
}
}
// get button state
Buttons::State button_state = Buttons::getState(manager, button, false);
const auto button_state = Buttons::getState(manager, button, false);
// naive bindings report their digital state as full or zero velocity
if (button.isNaive()) {
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
}
// check if button isn't being pressed
if (button_state != Buttons::BUTTON_PRESSED) {
@@ -679,81 +740,98 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
}
// deadzone
if (analog.isDeadzoneSet()) {
// do not apply deadzone to circular analogs since it doesn't make sense (except in relative mode)
if (analog.isDeadzoneSet() &&
(analog.getType() != AnalogType::Circular || analog.isRelativeMode())) {
value = analog.applyDeadzone(value);
}
if (analog.isRelativeMode()) {
float relative_delta = value - 0.5f;
// built-in scaling to make values reasonable
relative_delta /= 80.f;
if (analog.getType() == AnalogType::Circular) {
// integer multiplier/divisor
const auto mult = analog.getMultiplier();
if (mult < -1) {
relative_delta /= -mult;
} else if (1 < mult) {
relative_delta *= mult;
}
if (analog.isRelativeMode()) {
value = analog.getRelativeModeValue(value);
// sensitivity (ranges from 0.0 to 4.0)
if (analog.isSensitivitySet()) {
relative_delta *= analog.getSensitivity();
}
} else {
// integer multiplier
value = analog.applyMultiplier(value);
// translate relative movement to absolute value
value = analog.getAbsoluteValue(relative_delta);
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
} else {
// integer multiplier
value = analog.applyMultiplier(value);
// smoothing
if (analog.getSmoothing()) {
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
}
// smoothing
if (analog.getSmoothing()) {
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
// calculate angle
rads = analog.getSmoothedValue(rads);
}
// calculate angle
rads = analog.getSmoothedValue(rads);
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
}
// apply to value
value = rads * (float) M_1_TAU;
}
}
} else {
// sensitivity
if (analog.isSensitivitySet()) {
// adjust curve
// values < 1.f : less sensitive around neutral
// values > 1.f : more sensitive around neutral
float curve = analog.getSensitivity();
if (curve <= 0.f) {
curve = 0.01f;
}
curve = 1.f / curve;
if (analog.getType() == AnalogType::LinearCentered) {
// convert 0.0..1.0 to -1.0..+1.0
float signed_raw = (value - 0.5f) * 2.0f;
// apply curve
float sign = signed_raw < 0.0f ? -1.0f : 1.0f;
float magnitude = fabsf(signed_raw);
float curved = sign * powf(magnitude, curve);
// convert back to 0.0..1.0
value = curved * 0.5f + 0.5f;
} else {
value = powf(value, curve);
}
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
value = std::clamp(value, 0.f, 1.f);
}
// multiplier / divisor
if (analog.getMultiplier() < -1) {
if (analog.getType() == AnalogType::LinearCentered) {
value = (value - 0.5f) / (-analog.getMultiplier()) + 0.5f;
} else {
value /= -analog.getMultiplier();
}
// apply to value
value = rads * (float) M_1_TAU;
value = std::clamp(value, 0.f, 1.f);
} else if (analog.getMultiplier() > 1) {
if (analog.getType() == AnalogType::LinearCentered) {
value = (value - 0.5f) * analog.getMultiplier() + 0.5f;
} else {
value *= analog.getMultiplier();
}
value = std::clamp(value, 0.f, 1.f);
}
}
// delay
if (0 < analog.getDelayBufferDepth()) {
auto& queue = analog.getDelayBuffer();
// ensure the queue isn't too long; drop old values
while (analog.getDelayBufferDepth() <= (int)queue.size()) {
queue.pop();
}
// always push new value
queue.push(value);
// get a new value to return
if ((int)queue.size() < analog.getDelayBufferDepth()) {
// not enough in the queue, stall for now, shouldn't happen often
value = analog.getLastState();
} else {
value = queue.front();
queue.pop();
}
if (analog.getDelayMs() > 0) {
value = analog.getDelayedValue(value);
}
break;
@@ -839,6 +917,42 @@ std::vector<Analog> GameAPI::Analogs::sortAnalogs(
return sorted;
}
static std::vector<Analog> sortAnalogsWithTypeInternal(
std::vector<Analog> &analogs,
const std::initializer_list<GameAPI::Analogs::AnalogWithType> list) {
std::vector<Analog> sorted;
bool analog_found;
for (auto &a : list) {
analog_found = false;
for (auto &analog : analogs) {
if (a.name == analog.getName()) {
analog_found = true;
analog.setType(a.type);
sorted.push_back(analog);
break;
}
}
if (!analog_found) {
sorted.emplace_back(a.name, a.type);
}
}
return sorted;
}
void GameAPI::Analogs::sortAnalogsWithType(
std::vector<Analog> *analogs,
const std::initializer_list<AnalogWithType> list) {
if (analogs) {
*analogs = sortAnalogsWithTypeInternal(*analogs, list);
}
}
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) {
// check override
+8 -7
View File
@@ -85,14 +85,15 @@ namespace GameAPI {
const std::vector<Analog> &analogs,
const std::vector<std::string> &analog_names);
template<typename T>
void sortAnalogs(std::vector<Analog> *analogs, T t) {
const std::vector<std::string> analog_names { t };
struct AnalogWithType {
const std::string name;
const AnalogType type;
if (analogs) {
*analogs = GameAPI::Analogs::sortAnalogs(*analogs, analog_names);
}
}
AnalogWithType(std::string name, AnalogType type) :
name(std::move(name)), type(type) {}
};
void sortAnalogsWithType(std::vector<Analog> *analogs, const std::initializer_list<AnalogWithType> list);
template<typename T, typename... Rest>
void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) {
+25 -2
View File
@@ -299,14 +299,37 @@ std::string Button::getMidiNoteString() {
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
}
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
auto binding_display = this->getBindingDisplayString(manager);
auto modifiers = this->getModifierMask();
if (binding_display.empty() || modifiers == 0) {
return binding_display;
}
std::string modifier_display;
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
if ((modifiers & UINT8_C(1)) == 0) {
continue;
}
if (!modifier_display.empty()) {
modifier_display += "+";
}
modifier_display += fmt::format("Mod{}", index);
}
return modifier_display + "+" + binding_display;
}
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
// get VKey string
auto vKey = (uint16_t) this->getVKey();
std::string vKeyString = fmt::format("{:#x}", vKey);
// device must be existing
if (this->device_identifier.empty() && vKey == INVALID_VKEY) {
if (this->isNaive() && vKey == INVALID_VKEY) {
if (this->getInvert()) {
return "(always on)";
}
return "";
}
+11
View File
@@ -49,6 +49,7 @@ private:
std::string name;
std::string device_identifier = "";
unsigned short vKey = INVALID_VKEY;
uint8_t modifier_mask = 0;
// default bindings are always naive
unsigned short vKey_default = INVALID_VKEY;
@@ -65,6 +66,7 @@ private:
unsigned short velocity_threshold = 0;
std::string getMidiNoteString();
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
public:
std::string getVKeyString();
@@ -111,6 +113,7 @@ public:
device_identifier = "";
analog_type = BAT_NONE;
bat_threshold = 0;
modifier_mask = 0;
}
std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -123,6 +126,14 @@ public:
return this->name;
}
inline uint8_t getModifierMask() const {
return this->modifier_mask;
}
inline void setModifierMask(uint8_t new_modifier_mask) {
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
}
inline const std::string &getDeviceIdentifier() const {
return this->device_identifier;
}
+59 -25
View File
@@ -28,7 +28,7 @@ Config::Config() {
this->status = false;
if (CONFIG_PATH_OVERRIDE.length() > 0) {
this->configLocation = CONFIG_PATH_OVERRIDE;
log_info("cfg", "using custom config file: {}", this->configLocation.string());
log_info("cfg", "using custom config file: {}", this->configLocation);
} else {
this->configLocation = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools.xml";
// avoids logging the expanded appdata path as it contains user name
@@ -57,7 +57,7 @@ Config::Config() {
this->firstFillConfigFile();
break;
case tinyxml2::XMLError::XML_ERROR_FILE_COULD_NOT_BE_OPENED:
log_fatal("cfg", "could not open config file: {}", this->configLocation.string());
log_fatal("cfg", "could not open config file: {}", this->configLocation);
break;
case tinyxml2::XMLError::XML_ERROR_FILE_NOT_FOUND:
this->createConfigFile();
@@ -72,7 +72,7 @@ Config::Config() {
case tinyxml2::XMLError::XML_ERROR_PARSING_UNKNOWN:
case tinyxml2::XMLError::XML_ERROR_MISMATCHED_ELEMENT:
case tinyxml2::XMLError::XML_ERROR_PARSING:
log_warning("cfg", "Couldn't read config file: {}", this->configLocation.string());
log_warning("cfg", "Couldn't read config file: {}", this->configLocation);
this->createConfigFile();
this->firstFillConfigFile();
break;
@@ -98,7 +98,7 @@ bool Config::getStatus() {
return this->status;
}
bool Config::addGame(Game &game) {
bool Config::addGame(Game &game, bool save) {
tinyxml2::XMLNode *rootNode = this->configFile.LastChild();
tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game");
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game) {
int bat_threshold = 0;
int velocity_threshold = 0;
bool invert = false;
unsigned int modifier_mask = 0;
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
const char *devid = gameButtonNode->Attribute("devid");
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
gameButtonsNode->DeleteChild(gameButtonNode);
gameButtonNode = this->configFile.NewElement("button");
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
gameButtonNode->SetAttribute("invert", invert);
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
gameButtonsNode->InsertEndChild(gameButtonNode);
} else {
button->setVKey(static_cast<unsigned short int>(vKey));
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game) {
button->setBatThreshold(bat_threshold);
button->setVelocityThreshold(velocity_threshold);
button->setInvert(invert);
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button->setDeviceIdentifier(devid);
}
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode);
}
@@ -253,7 +258,7 @@ bool Config::addGame(Game &game) {
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
int delay_buffer_depth = 0;
uint32_t delay = 0;
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -262,7 +267,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth);
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid");
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
@@ -278,7 +283,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
} else {
it.setIndex(static_cast<unsigned short int>(index));
@@ -290,7 +295,7 @@ bool Config::addGame(Game &game) {
it.setSmoothing(smoothing);
it.setMultiplier(multiplier);
it.setRelativeMode(relative_mode);
it.setDelayBufferDepth(delay_buffer_depth);
it.setDelayMs(delay);
}
} else {
gameAnalogNode = this->configFile.NewElement("analog");
@@ -303,7 +308,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode);
}
@@ -458,7 +464,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -486,13 +492,20 @@ bool Config::addGame(Game &game) {
rootNode->InsertEndChild(gameNode);
}
// save config
this->saveConfigFile();
// save config (skipped when caller batches multiple addGame calls
// and flushes once at the end via Config::save())
if (save) {
this->saveConfigFile();
}
// return success
return true;
}
void Config::save() {
this->saveConfigFile();
}
bool Config::updateBinding(const Game &game, const Button &button, int alternative) {
// get root node
@@ -545,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", button.getInvert());
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
break;
}
}
@@ -562,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", 0);
gameButtonNode->SetAttribute("invert", false);
gameButtonNode->SetAttribute("devid", "");
gameButtonNode->SetAttribute("modifiers", 0);
gameButtonsNode->InsertEndChild(gameButtonNode);
}
}
@@ -692,7 +707,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
} else {
gameAnalogNode = this->configFile.NewElement("analog");
@@ -704,7 +719,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -957,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
int bat_threshold = 0;
int velocity_threshold = 0;
bool invert = false;
unsigned int modifier_mask = 0;
gameButtonNode->QueryIntAttribute("vkey", &vKey);
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
@@ -964,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
const char *devid = gameButtonNode->Attribute("devid");
// find alternative
@@ -979,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
alt.setBatThreshold(bat_threshold);
alt.setVelocityThreshold(velocity_threshold);
alt.setInvert(invert);
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
alt.setDeviceIdentifier(std::string(devid));
}
@@ -998,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
button.setBatThreshold(bat_threshold);
button.setVelocityThreshold(velocity_threshold);
button.setInvert(invert);
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button.setDeviceIdentifier(devid);
}
@@ -1142,7 +1161,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
int delay_buffer_depth = 0;
uint32_t delay = 0;
gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -1151,7 +1170,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth);
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid");
// create analog and add to list
@@ -1164,7 +1183,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
analog.setSmoothing(smoothing);
analog.setMultiplier(multiplier);
analog.setRelativeMode(relative_mode);
analog.setDelayBufferDepth(delay_buffer_depth);
analog.setDelayMs(delay);
if (devid) {
analog.setDeviceIdentifier(devid);
}
@@ -1324,17 +1343,32 @@ bool Config::firstFillConfigFile() {
}
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);
if (xml_result != tinyxml2::XMLError::XML_SUCCESS) {
log_info("cfg", "failed to write file: {}", this->configLocationTemp.string());
log_info("cfg", "failed to write file: {}", this->configLocationTemp);
return;
}
// copy the .tmp file to the main file...
if (CopyFileW(this->configLocationTemp.c_str(), this->configLocation.c_str(), false) == 0) {
log_warning("cfg", "CopyFileA failed: 0x{:08x}", GetLastError());
// ...flush the .tmp file to disk so a crash/power loss can't leave it half-written...
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;
}
// 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 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 Analog &analog);
+94 -4
View File
@@ -1,11 +1,16 @@
#include "configurator.h"
#include "configurator.h"
#include <d3d9.h>
#include "overlay/overlay.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace cfg {
// globals
bool CONFIGURATOR_STANDALONE = false;
bool CONFIGURATOR_FORCE_SOFTWARE_RENDER = false;
ConfigType CONFIGURATOR_TYPE = ConfigType::Config;
Configurator::Configurator() {
@@ -16,13 +21,98 @@ namespace cfg {
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() {
// 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::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->hotkeys_enable = false;
ImGui::GetIO().MouseDrawCursor = false;
// run window
+1
View File
@@ -11,6 +11,7 @@ namespace cfg {
// globals
extern bool CONFIGURATOR_STANDALONE;
extern ConfigType CONFIGURATOR_TYPE;
extern bool CONFIGURATOR_FORCE_SOFTWARE_RENDER;
class Configurator {
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 <windowsx.h>
#include "build/defs.h"
#include "external/imgui/imgui.h"
#include "launcher/shutdown.h"
#include "overlay/overlay.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "cfg/configurator.h"
#include "icon.h"
@@ -14,8 +21,177 @@ static const char *CLASS_NAME = "ConfiguratorWindow";
static std::string WINDOW_TITLE;
static int WINDOW_SIZE_X = 800;
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 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() {
// register the window class
@@ -27,12 +203,22 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
wc.hIcon = WINDOW_ICON;
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
if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")";
WINDOW_SIZE_X = 800;
WINDOW_SIZE_Y = 600;
}
this->client_width = WINDOW_SIZE_X;
this->client_height = WINDOW_SIZE_Y;
// open window
this->hWnd = CreateWindowEx(
@@ -48,27 +234,58 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
if (this->hWnd) {
overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = true;
// force dark title bar
set_window_dark_titlebar(this->hWnd, true);
}
}
cfg::ConfiguratorWindow::~ConfiguratorWindow() {
this->stop_timer();
// close window
DestroyWindow(this->hWnd);
// unregister class
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() {
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
SetWindowPos(this->hWnd, HWND_TOP, 0, 0, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
ShowWindow(this->hWnd, SW_SHOWNORMAL);
UpdateWindow(this->hWnd);
// draw overlay in 60 FPS
SetTimer(this->hWnd, 1, 1000 / 60, nullptr);
// SW_SHOWNORMAL usually activates a top-level window, but not always (e.g.,
// 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
BOOL ret;
@@ -94,6 +311,22 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
}
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: {
// set user data of window to class pointer
@@ -102,6 +335,72 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
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_DESTROY: {
@@ -110,62 +409,192 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
break;
}
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) {
overlay::OVERLAY->update();
overlay::OVERLAY->set_active(true);
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
}
// repaint window
InvalidateRect(hWnd, nullptr, TRUE);
if (use_d3d9) {
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;
}
case WM_ERASEBKGND: {
return 1;
}
case WM_PAINT: {
// render overlay
if (overlay::OVERLAY) {
// get pixel data
int width, height;
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
if (width > 0 && height > 0) {
// create bitmap
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
// 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);
}
paint_software(hWnd);
break;
}
case WM_ACTIVATE: {
const WORD activation = LOWORD(wParam);
if (activation == WA_INACTIVE) {
// dropping any held mouse buttons so we don't get stuck in a
// "button still pressed" state when we come back.
auto &io = ImGui::GetIO();
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Middle, false);
} 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;
}
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:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
+31 -1
View File
@@ -1,6 +1,9 @@
#pragma once
#pragma once
#include <cstdint>
#include <windows.h>
#include <d3d9.h>
namespace cfg {
@@ -9,11 +12,38 @@ namespace cfg {
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();
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);
};
}
+53 -10
View File
@@ -26,6 +26,7 @@ namespace overlay::windows {
el->SetAttribute("debounce_down", entry.debounce_down);
el->SetAttribute("bat_threshold", entry.bat_threshold);
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
el->SetAttribute("modifiers", entry.modifier_mask);
parent->InsertEndChild(el);
}
@@ -54,6 +55,10 @@ namespace overlay::windows {
el->QueryIntAttribute("bat_threshold", &bat);
entry.bat_threshold = bat;
unsigned int modifier_mask = 0;
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
return entry;
}
@@ -70,7 +75,7 @@ namespace overlay::windows {
el->SetAttribute("smoothing", analog.smoothing);
el->SetAttribute("multiplier", analog.multiplier);
el->SetAttribute("relative", analog.relative_mode);
el->SetAttribute("delay", analog.delay_buffer_depth);
el->SetAttribute("delay_ms", analog.delay_ms);
parent->InsertEndChild(el);
}
@@ -93,7 +98,7 @@ namespace overlay::windows {
el->QueryBoolAttribute("smoothing", &a.smoothing);
el->QueryIntAttribute("multiplier", &a.multiplier);
el->QueryBoolAttribute("relative", &a.relative_mode);
el->QueryIntAttribute("delay", &a.delay_buffer_depth);
el->QueryUnsignedAttribute("delay_ms", &a.delay_ms);
return a;
}
@@ -160,6 +165,28 @@ namespace overlay::windows {
}
}
// modifier buttons
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
if (mod_el) {
auto *btn_el = mod_el->FirstChildElement("button");
while (btn_el) {
const char *btn_name = btn_el->Attribute("name");
std::string btn_name_str = btn_name ? btn_name : "";
TemplateButtonBinding *binding = nullptr;
for (auto &b : tmpl.modifier_buttons) {
if (b.name == btn_name_str) { binding = &b; break; }
}
if (!binding) {
tmpl.modifier_buttons.push_back(
{btn_name_str, read_button_entry(btn_el), {}});
} else {
binding->alternatives.push_back(read_button_entry(btn_el));
}
btn_el = btn_el->NextSiblingElement("button");
}
}
// keypad buttons
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
if (kp_el) {
@@ -244,6 +271,7 @@ namespace overlay::windows {
bool save_user_template(
const ControllerTemplate &tmpl,
const bool save_buttons,
const bool save_modifiers,
const bool save_keypads,
const bool save_analogs,
const bool save_lights) {
@@ -300,6 +328,24 @@ namespace overlay::windows {
}
tmpl_el->InsertEndChild(buttons_el);
// modifier buttons - skip unbound entries
if (!tmpl.modifier_buttons.empty()) {
auto *mod_el = doc.NewElement("modifier_buttons");
if (save_modifiers) {
for (auto &btn : tmpl.modifier_buttons) {
if (!btn.primary.is_unbound()) {
write_button_entry(doc, mod_el, btn.name, btn.primary);
}
for (auto &alt : btn.alternatives) {
if (!alt.is_unbound()) {
write_button_entry(doc, mod_el, btn.name, alt);
}
}
}
}
tmpl_el->InsertEndChild(mod_el);
}
// keypad buttons — skip unbound entries
if (!tmpl.keypad_buttons.empty()) {
auto *kp_el = doc.NewElement("keypad_buttons");
@@ -359,13 +405,12 @@ namespace overlay::windows {
return false;
}
std::error_code ec;
std::filesystem::rename(path_tmp, path, ec);
if (ec) {
log_warning("templates", "failed to rename templates file: {}", ec.message());
if (MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) {
log_warning("templates", "failed to rename templates file: 0x{:08x}", GetLastError());
return false;
}
log_info("templates", "templates file saved successfully");
return true;
}
@@ -397,8 +442,7 @@ namespace overlay::windows {
auto path_tmp = path;
path_tmp.replace_extension(L"tmp");
if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
std::error_code ec;
std::filesystem::rename(path_tmp, path, ec);
MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
}
}
@@ -434,8 +478,7 @@ namespace overlay::windows {
auto path_tmp = path;
path_tmp.replace_extension(L"tmp");
if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
std::error_code ec;
std::filesystem::rename(path_tmp, path, ec);
MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
}
}
+5 -5
View File
@@ -20,10 +20,10 @@
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
+2
View File
@@ -46,6 +46,8 @@ struct OptionDefinition {
// for OptionPickerType::FilePath
std::string file_extension = "";
std::string quick_setting_category = "";
};
class Option {
+3 -3
View File
@@ -21,7 +21,7 @@ namespace cfg {
if (SCREEN_RESIZE_CFG_PATH_OVERRIDE.has_value()) {
this->config_path = SCREEN_RESIZE_CFG_PATH_OVERRIDE.value();
if (fileutils::file_exists(this->config_path)) {
log_info("ScreenResize", "loading config from: {}", this->config_path.string());
log_info("ScreenResize", "loading config from: {}", this->config_path);
file_exists = true;
}
} else {
@@ -35,7 +35,7 @@ namespace cfg {
ScreenResize::~ScreenResize() {
}
void ScreenResize::config_load() {
std::string config = fileutils::text_read(this->config_path);
if (config.empty()) {
@@ -78,7 +78,7 @@ namespace cfg {
eamuse_get_game(),
use_game_setting,
root);
load_bool_value(doc, root + "enable_screen_resize", this->enable_screen_resize);
if (this->enable_screen_resize) {
log_misc("ScreenResize", "enabled by config file");
@@ -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 */
+1
View File
@@ -20,6 +20,7 @@ Usage:
#include <memory>
#include <string>
#include <map>
#include <cstdint>
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+6
View File
@@ -21,5 +21,11 @@ set(IMGUI_SOURCES
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})
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
// (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: 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)
// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown.
// 2025-06-11: DirectX9: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas.
@@ -110,7 +111,70 @@ struct render_state_t {
};
// 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();
@@ -125,37 +189,6 @@ static void ImGui_ImplDX9_SetupRenderState(ImDrawData* draw_data, struct render_
LPDIRECT3DDEVICE9 device = bd->pd3dDevice;
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.
device->SetPixelShader(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.
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; }
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;
}
if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount)
{
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
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;
}
@@ -305,9 +343,16 @@ void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data)
device->SetIndices(bd->pIB);
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 = {};
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
// (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)
{
// 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 == ImDrawCallback_ResetRenderState)
ImGui_ImplDX9_SetupRenderState(draw_data, &render_state);
if (pcmd->UserCallback == ImGui_ImplDX9_DrawCallback_ResetRenderState)
ImGui_ImplDX9_SetupRenderState(draw_data);
else
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);
// spice ldj
{
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();
}
}
}
ImGui_ImplDX9_RestoreRenderTargets(device, &render_state);
// Restore the DX9 transform
device->SetTransform(D3DTS_WORLD, &last_world);
@@ -406,50 +430,6 @@ static bool ImGui_ImplDX9_CheckFormatSupport(LPDIRECT3DDEVICE9 pDevice, D3DFORMA
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)
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();
}
// 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()
{
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
@@ -564,29 +572,54 @@ void ImGui_ImplDX9_NewFrame()
// IM_UNUSED(bd);
// spice ldj
{
IDirect3DSwapChain9 *swap_chain = nullptr;
if (SUCCEEDED(bd->pd3dDevice->GetSwapChain(0, &swap_chain))) {
auto &io = ImGui::GetIO();
ImGui_ImplDX9_UpdateDisplaySize(bd);
}
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))) {
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);
// 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)
io.DisplaySize.x = static_cast<float>(rect.right - rect.left);
io.DisplaySize.y = static_cast<float>(rect.bottom - rect.top);
}
}
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
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)
// 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)
- 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.
- 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/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)
@@ -1524,7 +1551,7 @@ ImGuiStyle::ImGuiStyle()
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.
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.
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.
@@ -1541,6 +1568,7 @@ ImGuiStyle::ImGuiStyle()
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.
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().
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).
@@ -1573,6 +1601,7 @@ ImGuiStyle::ImGuiStyle()
// 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.
void ImGuiStyle::ScaleAllSizes(float scale_factor)
{
@@ -1617,6 +1646,7 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor)
DragDropTargetBorderSize = ImTrunc(DragDropTargetBorderSize * scale_factor);
DragDropTargetPadding = ImTrunc(DragDropTargetPadding * scale_factor);
ColorMarkerSize = ImTrunc(ColorMarkerSize * scale_factor);
InputTextCursorSize = ImTrunc(InputTextCursorSize * scale_factor);
SeparatorSize = ImTrunc(SeparatorSize * scale_factor);
SeparatorTextBorderSize = ImTrunc(SeparatorTextBorderSize * scale_factor);
SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor);
@@ -3454,9 +3484,6 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper)
if (clipper->ItemsHeight <= 0.0f)
{
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);
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.
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.
}
@@ -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.
// 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.
min_y -= g.Style.ItemSpacing.y;
max_y += g.Style.ItemSpacing.y;
float pad_y = g.Style.ItemSpacing.y;
min_y -= pad_y;
max_y += pad_y;
// 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)
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
@@ -3720,6 +3756,7 @@ static const ImGuiStyleVarInfo GStyleVarsInfo[] =
{ 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign
{ 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TreeLinesSize)}, // ImGuiStyleVar_TreeLinesSize
{ 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, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign
{ 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_ScrollbarGrabActive: return "ScrollbarGrabActive";
case ImGuiCol_CheckMark: return "CheckMark";
case ImGuiCol_CheckboxSelectedBg: return "CheckboxSelectedBg";
case ImGuiCol_SliderGrab: return "SliderGrab";
case ImGuiCol_SliderGrabActive: return "SliderGrabActive";
case ImGuiCol_Button: return "Button";
@@ -3903,7 +3941,7 @@ void ImGui::RenderText(ImVec2 pos, const char* text, const char* text_end, bool
else
{
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;
}
@@ -3921,7 +3959,7 @@ void ImGui::RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end
ImGuiWindow* window = g.CurrentWindow;
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)
{
@@ -3990,8 +4028,8 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con
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);
//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->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(pos_max.x, pos_min.y - 4, pos_max.y + 6, IM_COL32(0, 0, 255, 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.
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;
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, p_max, GetColorU32(ImGuiCol_Border), 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, border_size);
}
}
@@ -4048,8 +4086,8 @@ void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding)
const float border_size = g.Style.FrameBorderSize;
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, p_max, GetColorU32(ImGuiCol_Border), 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, border_size);
}
}
@@ -4084,7 +4122,7 @@ void ImGui::RenderNavCursor(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFl
const float thickness = 2.0f;
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
{
@@ -4093,7 +4131,7 @@ void ImGui::RenderNavCursor(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFl
bool fully_visible = window->ClipRect.Contains(display_rect);
if (!fully_visible)
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)
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_max = a_min + IM_PI * 1.65f;
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();
}
@@ -4229,7 +4267,7 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas)
IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)();
if (shared_font_atlas == NULL)
IO.Fonts->OwnerContext = this;
WithinEndChildID = 0;
WithinEndChildID = WithinEndPopupID = 0;
TestEngine = NULL;
InputEventsNextMouseSource = ImGuiMouseSource_Mouse;
@@ -4537,7 +4575,7 @@ void ImGui::Shutdown()
for (ImFontAtlas* atlas : g.FontAtlases)
{
UnregisterFontAtlas(atlas);
if (atlas->RefCount == 0)
if (atlas->RefCount == 0 && atlas->OwnerContext == &g)
{
atlas->Locked = false;
IM_DELETE(atlas);
@@ -4795,7 +4833,8 @@ void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window)
g.ActiveIdIsJustActivated = (g.ActiveId != id);
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.ActiveIdHasBeenPressedBefore = false;
g.ActiveIdHasBeenEditedBefore = false;
@@ -4851,8 +4890,12 @@ void ImGui::MarkItemEdited(ImGuiID id)
// 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.
ImGuiContext& g = *GImGui;
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_EditedInternal;
if (g.LastItemData.ItemFlags & ImGuiItemFlags_NoMarkEdited)
return;
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited;
if (g.ActiveId == id || g.ActiveId == 0)
{
// 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)
// 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.CurrentWindow->DC.LastItemId == id);
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited;
}
bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags)
@@ -5042,7 +5082,7 @@ bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flag
{
g.HoveredIdPreviousFrameItemCount++;
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
@@ -5487,14 +5527,15 @@ void ImGui::UpdateMouseMovingWindowEndFrame()
// 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!)
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.
// 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;
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
@@ -5525,7 +5566,7 @@ void ImGui::UpdateMouseMovingWindowEndFrame()
// 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.
// (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.
// This is where we can trim the popup stack.
@@ -5920,7 +5961,7 @@ void ImGui::NewFrame()
g.CurrentWindowStack.resize(0);
g.BeginPopupStack.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.GroupStack.resize(0);
@@ -6090,14 +6131,6 @@ void ImGui::PopClipRect()
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)
{
if ((col & IM_COL32_A_MASK) == 0)
@@ -6199,7 +6232,7 @@ static void ImGui::RenderDimmedBackgrounds()
if (window->DrawList->CmdBuffer.Size == 0)
window->DrawList->AddDrawCmd();
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();
}
@@ -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);
// Round
// FIXME: This has been here since Dec 2015 (7b0bf230) but down the line we want this out.
// FIXME: Investigate using ceilf or e.g.
// - https://git.musl-libc.org/cgit/musl/tree/src/math/ceilf.c
// - https://embarkstudios.github.io/rust-gpu/api/src/libm/math/ceilf.rs.html
text_size.x = IM_TRUNC(text_size.x + 0.99999f);
// (see 7b0bf230, 4622fa4b6, #791 for details about this.)
// FIXME: Add a way to disable this.
text_size.x = ImCeilFast(text_size.x);
return text_size;
}
@@ -6566,11 +6597,12 @@ bool ImGui::IsItemToggledOpen()
return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledOpen) ? true : false;
}
// Call after a Selectable() or TreeNode() involved in multi-selection.
// 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 multi-select, it would be misleading/ambiguous to report this signal, as widgets
// return e.g. a pressed event and user code is in charge of altering selection in ways we cannot predict.)
// 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.
// Outside of a multi-select block:
// - It would be misleading/ambiguous to report this signal, as widgets return e.g. a pressed event,
// 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()
{
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 |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag
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)
window_flags |= ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings;
@@ -6866,6 +6898,14 @@ void ImGui::EndChild()
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)
{
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;
ImVec2 size_pad = window->WindowPadding * 2.0f;
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[ImGuiAxis_Y] = (axis_mask & 2) ? size_contents.y + size_pad.y + decoration_h_without_scrollbars : window->Size.y;
size_desired.x = (axis_mask & 1) ? size_contents.x + size_pad.x + decoration_w_without_scrollbars : window->Size.x;
size_desired.y = (axis_mask & 2) ? size_contents.y + size_pad.y + decoration_h_without_scrollbars : window->Size.y;
// Determine maximum window size
// 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),
// 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);
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);
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_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.
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)
size_auto_fit.y += style.ScrollbarSize;
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);
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->PathStroke(border_col, ImDrawFlags_None, border_size);
window->DrawList->PathStroke(border_col, border_size);
}
static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window)
@@ -7442,7 +7484,7 @@ static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window)
const float border_size = window->WindowBorderSize;
const ImU32 border_col = GetColorU32(ImGuiCol_Border);
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)
{
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)
{
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.
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)
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
@@ -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_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;
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_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_x = use_current_size_for_scrollbar_x ? avail_size_from_current_frame.x : avail_size_from_last_frame.x;
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_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->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->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_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)
// (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;
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);
// 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.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical;
// Default item width. Make it proportional to window size if window manually resizes
const bool is_resizable_window = (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize));
if (is_resizable_window)
// Default item width. Make it proportional to window size if window can be manually resized.
// (we cannot use AutoFitFramesX/AutoFitFramesY which is a temporary state)
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);
else
window->DC.ItemWidthDefault = ImTrunc(g.FontSize * 16.0f);
@@ -8770,6 +8817,8 @@ void ImGui::End()
ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back();
// 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)
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;
}
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)
{
ImGuiContext& g = *GImGui;
@@ -9540,6 +9600,8 @@ static void ImGui::UpdateTexturesNewFrame()
IM_ASSERT(atlas->RendererHasTextures == has_textures);
}
}
for (ImTextureData* tex : g.UserTextures)
ImTextureDataUpdateNewFrame(tex);
}
// Build a single texture list
@@ -9601,7 +9663,7 @@ ImFont* ImGui::GetDefaultFont()
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)
{
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.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;
}
@@ -10936,15 +10998,21 @@ void ImGui::UpdateMouseWheel()
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;
if (!mouse_window || mouse_window->Collapsed)
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
// 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)
@@ -11253,6 +11321,7 @@ bool ImGui::TestKeyOwner(ImGuiKey key, ImGuiID owner_id)
// - SetKeyOwner(..., None) : clears owner
// - SetKeyOwner(..., Any, !Lock) : illegal (assert)
// - 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)
{
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); }
}
// This is more or less equivalent to:
// This is more or less equivalent to a fancier version of:
// if (IsItemHovered() || IsItemActive())
// 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.
// 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.
void ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags)
bool ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags)
{
ImGuiContext& g = *GImGui;
ImGuiID id = g.LastItemData.ID;
if (id == 0 || (g.HoveredId != id && g.ActiveId != id))
return;
return false;
if ((flags & ImGuiInputFlags_CondMask_) == 0)
flags |= ImGuiInputFlags_CondDefault_;
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!
if (!TestKeyOwner(key, id))
return false;
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.
@@ -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
// windows, table column contents size used for auto-resizing columns, group size).
// 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:
// 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;
}
// 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];
IM_ASSERT(extra_window_flags & ImGuiWindowFlags_ChildMenu);
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);
// 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;
g.WithinEndPopupID = window->ID;
if (window->Flags & ImGuiWindowFlags_ChildWindow)
g.WithinEndChildID = window->ID;
End();
g.WithinEndPopupID = backup_within_end_popup_id;
g.WithinEndChildID = backup_within_end_child_id;
}
@@ -14025,7 +14113,7 @@ static void ImGui::NavProcessItem()
const ImGuiID id = g.LastItemData.ID;
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;
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?
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 };
if (nav_gamepad_active)
if (nav_gamepad_active && g.NavInputSource != ImGuiInputSource_Gamepad)
for (ImGuiKey key : nav_gamepad_keys_to_change_source)
if (IsKeyDown(key))
g.NavInputSource = ImGuiInputSource_Gamepad;
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 };
if (nav_keyboard_active)
if (nav_keyboard_active && g.NavInputSource != ImGuiInputSource_Keyboard)
for (ImGuiKey key : nav_keyboard_keys_to_change_source)
if (IsKeyDown(key))
g.NavInputSource = ImGuiInputSource_Keyboard;
@@ -15807,7 +15895,7 @@ const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDrop
IM_ASSERT(viewport != NULL);
ImRect bb = g.DragDropTargetRect;
bb.Expand(-3.5f);
RenderDragDropTargetRectEx(GetForegroundDrawList(viewport), bb);
RenderDragDropTargetRectEx(GetForegroundDrawList(viewport), bb, g.Style.DragDropTargetRounding);
}
else if (draw_target_rect)
{
@@ -15838,16 +15926,16 @@ void ImGui::RenderDragDropTargetRectForItem(const ImRect& bb)
bool push_clip_rect = !window->ClipRect.Contains(bb_display);
if (push_clip_rect)
window->DrawList->PushClipRectFullScreen();
RenderDragDropTargetRectEx(window->DrawList, bb_display);
RenderDragDropTargetRectEx(window->DrawList, bb_display, g.Style.DragDropTargetRounding);
if (push_clip_rect)
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;
draw_list->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTargetBg), g.Style.DragDropTargetRounding, 0);
draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTarget), g.Style.DragDropTargetRounding, 0, g.Style.DragDropTargetBorderSize);
draw_list->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTargetBg), rounding, 0);
draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTarget), rounding, g.Style.DragDropTargetBorderSize);
}
const ImGuiPayload* ImGui::GetDragDropPayload()
@@ -16576,6 +16664,7 @@ void ImGuiPlatformIO::ClearRendererHandlers()
Renderer_CreateWindow = Renderer_DestroyWindow = NULL;
Renderer_SetWindowSize = NULL;
Renderer_RenderWindow = Renderer_SwapBuffers = NULL;
DrawCallback_ResetRenderState = DrawCallback_SetSamplerLinear = DrawCallback_SetSamplerNearest = NULL;
}
ImGuiViewport* ImGui::GetMainViewport()
@@ -16691,7 +16780,7 @@ static bool ImGui::UpdateTryMergeWindowIntoHostViewport(ImGuiWindow* window, ImG
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;
if (viewport_obstructing->GetMainRect().Overlaps(window->Rect()))
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->PlatformIconData = window->WindowClass.PlatformIconData;
// Update parent viewport ID
// (the !IsFallbackWindow test mimic the one done in WindowSelectViewport())
if (window->WindowClass.ParentViewportId != (ImGuiID)-1)
@@ -18883,10 +18974,13 @@ static void ImGui::DockNodeUpdateFlagsAndCollapse(ImGuiDockNode* node)
node->WantHiddenTabBarToggle = false;
// Apply toggles at a single point of the frame (here!)
const ImGuiDockNodeFlags prev_local_flags = node->LocalFlags;
if (node->Windows.Size > 1)
node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_HiddenTabBar);
else if (node->WantHiddenTabBarToggle)
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;
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);
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);
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);
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);
@@ -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);
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();
char buf[128];
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]);
Selectable(buf);
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
@@ -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]);
Selectable(buf);
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();
@@ -22922,7 +23016,7 @@ void ImGui::ShowMetricsWindow(bool* p_open)
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);
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
@@ -23199,7 +23293,7 @@ void ImGui::DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, con
{
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->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;
}
}
@@ -23227,7 +23321,7 @@ void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, co
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));
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
if (show_aabb)
@@ -23508,8 +23602,8 @@ void ImGui::DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label)
{
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->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->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->ScrollingRectMinX, tab_bar->BarRect.Min.y, 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)
{
@@ -23871,8 +23965,8 @@ void ImGui::DebugDrawCursorPos(ImU32 col)
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
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->AddLine(ImVec2(pos.x - 3.0f, pos.y), ImVec2(pos.x + 4.0f, pos.y), col, 1.0f);
window->DrawList->AddLineV(pos.x, pos.y - 3.0f, pos.y + 4.0f, 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
@@ -23883,9 +23977,9 @@ void ImGui::DebugDrawLineExtents(ImU32 col)
float curr_x = window->DC.CursorPos.x;
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);
window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y1), ImVec2(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->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_y1, col, 1.0f);
window->DrawList->AddLineV(curr_x - 0.5f, line_y1, 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)
@@ -24255,7 +24349,7 @@ void ImGui::ShowFontSelector(const char* label)
"- Load additional fonts with io.Fonts->AddFontXXX() functions.\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"
"- 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)
+80 -48
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7
// dear imgui, v1.92.9 WIP
// (headers)
// Help:
@@ -29,8 +29,8 @@
// Library Version
// (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_NUM 19270
#define IMGUI_VERSION "1.92.9 WIP"
#define IMGUI_VERSION_NUM 19282
#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_VIEWPORT // In 'docking' WIP branch.
@@ -621,7 +621,8 @@ namespace ImGui
IMGUI_API ImGuiID GetID(int int_id);
// 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 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();
@@ -845,7 +846,7 @@ namespace ImGui
// Popups, Modals
// - 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.
// - 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().
@@ -1128,10 +1129,11 @@ namespace ImGui
// Inputs Utilities: Key/Input Ownership [BETA]
// - 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.
// 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.
// - 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.
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
// - 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
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
#endif
};
@@ -1448,7 +1450,7 @@ enum ImGuiTabBarFlags_
ImGuiTabBarFlags_DrawSelectedOverline = 1 << 6, // Draw selected overline markers over selected tab
// 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_FittingPolicyScroll = 1 << 9, // Enable scrolling buttons when tabs don't fit
ImGuiTabBarFlags_FittingPolicyMask_ = ImGuiTabBarFlags_FittingPolicyMixed | ImGuiTabBarFlags_FittingPolicyShrink | ImGuiTabBarFlags_FittingPolicyScroll,
@@ -1813,7 +1815,7 @@ enum ImGuiBackendFlags_
ImGuiBackendFlags_RendererHasViewports = 1 << 10, // Backend Renderer 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_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()
@@ -1838,6 +1840,7 @@ enum ImGuiCol_
ImGuiCol_ScrollbarGrabHovered,
ImGuiCol_ScrollbarGrabActive,
ImGuiCol_CheckMark, // Checkbox tick and RadioButton circle
ImGuiCol_CheckboxSelectedBg, // Checkbox background when Selected, otherwise use FrameBg
ImGuiCol_SliderGrab,
ImGuiCol_SliderGrabActive,
ImGuiCol_Button,
@@ -1937,6 +1940,7 @@ enum ImGuiStyleVar_
ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign
ImGuiStyleVar_TreeLinesSize, // float TreeLinesSize
ImGuiStyleVar_TreeLinesRounding, // float TreeLinesRounding
ImGuiStyleVar_DragDropTargetRounding, // float DragDropTargetRounding
ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign
ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign
ImGuiStyleVar_SeparatorSize, // float SeparatorSize
@@ -2405,7 +2409,7 @@ struct ImGuiStyle
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 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 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.
@@ -2414,14 +2418,15 @@ struct ImGuiStyle
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 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 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.
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 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()
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.
@@ -2453,7 +2458,7 @@ struct ImGuiStyle
// Functions
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
#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.
// 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 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 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).
// 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.
#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.
// 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 Flags; // 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
// - 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!
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 CursorPos; // // Read-write // [Completion,History,Always]
int SelectionStart; // // Read-write // [Completion,History,Always] == to SelectionEnd when no selection
int SelectionEnd; // // Read-write // [Completion,History,Always]
int CursorPos; // // Read-write // [Completion,History,Always,CharFilter]
int SelectionStart; // // Read-write // [Completion,History,Always,CharFilter] == to SelectionEnd when no selection
int SelectionEnd; // // Read-write // [Completion,History,Always,CharFilter]
// Helper functions for text manipulation.
// 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.
// Provide hints:
// - 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.
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!)
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?
void* PlatformIconData; // [EXPERIMENTAL] Pass opaque data for Platform backend to handle.
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);
#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)
// - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled,
// 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
// (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_
{
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.
ImDrawFlags_RoundCornersTopRight = 1 << 5, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-right corner only (when rounding > 0.0f, we default to all corners). Was 0x02.
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_RoundCornersBottomRight = 1 << 7, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-right corner only (when rounding > 0.0f, we default to all corners). Wax 0x08.
ImDrawFlags_RoundCornersNone = 1 << 8, // AddRect(), AddRectFilled(), PathRect(): disable rounding on all corners (when rounding > 0.0f). This is NOT zero, NOT an implicit flag!
// Rounding for AddRect(), AddRectFilled(), PathRect()
// - When not specified, we defaults to ImDrawFlags_RoundCornersAll! So you only need to use those flags if you want another configuration.
ImDrawFlags_RoundCornersTopLeft = 1 << 4, // Round top-left corner only (when rounding > 0.0f, we default to all corners).
ImDrawFlags_RoundCornersTopRight = 1 << 5, // Round top-right corner only (when rounding > 0.0f, we default to all corners).
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_RoundCornersBottom = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight,
ImDrawFlags_RoundCornersLeft = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft,
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,
// 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.
@@ -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_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_TextNoPixelSnap = 1 << 4, // Disable automatically snapping AddText() calls to pixel boundaries.
};
// Draw command list
@@ -3459,7 +3468,9 @@ struct ImDrawList
// 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.
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 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);
@@ -3480,7 +3491,7 @@ struct ImDrawList
// General polygon
// - 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.
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 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 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 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 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
@@ -3510,14 +3521,15 @@ struct ImDrawList
// 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).
// - 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.
// - 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).
// - 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.
// - 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
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
#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 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
//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)
@@ -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.
// 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() { DestroyPixels(); }
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* 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 void RemoveFont(ImFont* font);
IMGUI_API void Clear(); // Clear everything (input fonts, output glyphs/textures).
IMGUI_API void CompactCache(); // Compact cached glyphs and texture.
IMGUI_API void RemoveFont(ImFont* font); // Remove a font
IMGUI_API void CompactCache(); // Compact cached glyphs and texture.
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 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.
#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_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_ImplicitRefSize = 1 << 4, // [Internal] Reference size was not set explicitly.
};
// 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 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
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
// [Internal] Don't use!
@@ -4121,6 +4144,7 @@ struct ImGuiViewport
// 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* 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* 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.
@@ -4228,6 +4252,12 @@ struct ImGuiPlatformIO
// Written by some backends during ImGui_ImplXXXX_RenderDrawData() call to point backend_specific ImGui_ImplXXXX_RenderState* structure.
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
//------------------------------------------------------------------
@@ -4421,6 +4451,8 @@ namespace ImGui
//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
// - ImFontAtlasCustomRect::X,Y --> ImTextureRect::x,y
// - 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)
// Help:
@@ -73,6 +73,7 @@ Index of this file:
// [SECTION] Demo Window / ShowDemoWindow()
// [SECTION] DemoWindowMenuBar()
// [SECTION] Helpers: ExampleTreeNode, ExampleMemberInfo (for use by Property Editor & Multi-Select demos)
// [SECTION] Helpers: ExampleImageViewer
// [SECTION] DemoWindowWidgetsBasic()
// [SECTION] DemoWindowWidgetsBullets()
// [SECTION] DemoWindowWidgetsCollapsingHeaders()
@@ -108,6 +109,7 @@ Index of this file:
// [SECTION] User Guide / ShowUserGuide()
// [SECTION] Example App: Main Menu Bar / ShowExampleAppMainMenuBar()
// [SECTION] Example App: Debug Console / ShowExampleAppConsole()
// [SECTION] Example App: Image Viewer / ShowExampleAppImageViewer()
// [SECTION] Example App: Debug Log / ShowExampleAppLog()
// [SECTION] Example App: Simple Layout / ShowExampleAppLayout()
// [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor()
@@ -240,6 +242,7 @@ static void ShowExampleAppConsole(bool* p_open);
static void ShowExampleAppCustomRendering(bool* p_open);
static void ShowExampleAppDockSpace(bool* p_open);
static void ShowExampleAppDocuments(bool* p_open);
static void ShowExampleAppImageViewer(bool* p_open);
static void ShowExampleAppLog(bool* p_open);
static void ShowExampleAppLayout(bool* p_open);
static void ShowExampleAppPropertyEditor(bool* p_open, ImGuiDemoWindowData* demo_data);
@@ -321,6 +324,7 @@ struct ImGuiDemoWindowData
bool ShowAppCustomRendering = false;
bool ShowAppDocuments = false;
bool ShowAppDockSpace = false;
bool ShowAppImageViewer = false;
bool ShowAppLog = false;
bool ShowAppLayout = 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.ShowAppConsole) { ShowExampleAppConsole(&demo_data.ShowAppConsole); }
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.ShowAppLayout) { ShowExampleAppLayout(&demo_data.ShowAppLayout); }
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("Documents", NULL, &demo_data->ShowAppDocuments);
ImGui::MenuItem("Dockspace", NULL, &demo_data->ShowAppDockSpace);
ImGui::MenuItem("Image Viewer", NULL, &demo_data->ShowAppImageViewer);
ImGui::MenuItem("Log", NULL, &demo_data->ShowAppLog);
ImGui::MenuItem("Property editor", NULL, &demo_data->ShowAppPropertyEditor);
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::EndDisabled();
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::MenuItem("Debug Log", NULL, &demo_data->ShowDebugLog, has_debug_tools);
@@ -896,6 +902,87 @@ static ExampleTreeNode* ExampleTree_CreateDemoTree()
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()
//-----------------------------------------------------------------------------
@@ -1873,40 +1960,29 @@ static void DemoWindowWidgetsImages()
// - Read https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
// 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.
float my_tex_w = (float)io.Fonts->TexData->Width;
float my_tex_h = (float)io.Fonts->TexData->Height;
// Basic drawing
ImGui::SeparatorText("Image()/ImageWithBg() function");
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();
{
ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h);
ImVec2 pos = ImGui::GetCursorScreenPos();
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));
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();
}
// Fancy widget
ImGui::SeparatorText("Interactive Image Viewer");
static ExampleImageViewerData image_viewer;
ImVec2 canvas_size(ImGui::GetContentRegionAvail().x, my_tex_h * 2.0f);
ExampleImageViewer_DrawOptions(&image_viewer);
ExampleImageViewer_DrawCanvas(&image_viewer, canvas_size, my_tex_id, (int)my_tex_w, (int)my_tex_h);
IMGUI_DEMO_MARKER("Widgets/Images/Textured buttons");
ImGui::SeparatorText("Textured Buttons");
ImGui::TextWrapped("And now some textured buttons..");
static int pressed_count = 0;
for (int i = 0; i < 8; i++)
@@ -4131,7 +4207,7 @@ static void DemoWindowWidgetsTooltips()
ImGui::BeginDisabled();
ImGui::Button("Disabled item", sz);
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::TreePop();
@@ -4148,9 +4224,9 @@ static void DemoWindowWidgetsTreeNodes()
{
IMGUI_DEMO_MARKER("Widgets/Tree Nodes");
// 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++)
{
// Use SetNextItemOpen() so set the default state of a node to be open. We could
@@ -4174,9 +4250,9 @@ static void DemoWindowWidgetsTreeNodes()
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;
HelpMarker("Default option for DrawLinesXXX is stored in style.TreeLinesFlags");
ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesNone", &base_flags, ImGuiTreeNodeFlags_DrawLinesNone);
@@ -4213,15 +4289,57 @@ static void DemoWindowWidgetsTreeNodes()
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(
"This is a more typical looking tree with selectable nodes.\n"
"Click to select, Ctrl+Click to toggle, click on arrows or double-click to open.");
"Manually implemented selectable nodes.\n"
"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 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_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.");
@@ -4239,44 +4357,30 @@ static void DemoWindowWidgetsTreeNodes()
ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesToNodes", &base_flags, ImGuiTreeNodeFlags_DrawLinesToNodes);
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::Text("Hello!");
ImGui::Checkbox("Make Tree Nodes as drag & drop sources", &use_drag_and_drop);
if (align_label_with_current_x_position)
ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing());
// 'selection_mask' is dumb 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.
// '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++)
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 IsItemClicked() && !IsItemToggledOpen(), so clicking on an arrow doesn't alter selection.
ImGuiTreeNodeFlags node_flags = base_flags;
const bool is_selected = (selection_mask & (1 << i)) != 0;
if (is_selected)
node_flags |= ImGuiTreeNodeFlags_Selected;
if (i < 3)
if (node_n < 3)
{
// Items 0..2 are Tree Node
bool node_open = ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Node %d", i);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen())
node_clicked = i;
if (test_drag_and_drop && ImGui::BeginDragDropSource())
bool is_open = ImGui::TreeNodeEx((void*)(intptr_t)node_n, node_flags, "Selectable Node %d", node_n);
if (use_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::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.
ImGui::SameLine();
if (ImGui::SmallButton("button")) {}
}
if (node_open)
if (is_open)
{
ImGui::BulletText("Blah blah\nBlah Blah");
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
// use BulletText() or advance the cursor by GetTreeNodeToLabelSpacing() and call Text().
node_flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; // ImGuiTreeNodeFlags_Bullet
ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Leaf %d", i);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen())
node_clicked = i;
if (test_drag_and_drop && ImGui::BeginDragDropSource())
ImGui::TreeNodeEx((void*)(intptr_t)node_n, node_flags, "Selectable Leaf %d", node_n);
if (use_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::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)
ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing());
ImGui::TreePop();
@@ -8941,7 +9034,7 @@ static void ShowExampleMenuFile()
IMGUI_DEMO_MARKER("Examples/Menu/Options");
static bool enabled = true;
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++)
ImGui::Text("Scrolling Text %d", i);
ImGui::EndChild();
@@ -9353,6 +9446,28 @@ static void ShowExampleAppConsole(bool* 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()
//-----------------------------------------------------------------------------
@@ -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->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->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, 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, corners_tl_br, th); x += sz + spacing; // Square with two rounded corners
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, th); x += sz + spacing; // Square with all 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.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->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->AddLine(ImVec2(x, y), ImVec2(x, y + sz), col, th); x += spacing; // Vertical 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->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
// Path
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;
// Quadratic Bezier Curve (3 control points)
@@ -10395,9 +10510,9 @@ static void ShowExampleAppCustomRendering(bool* p_open)
{
const float GRID_STEP = 64.0f;
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)
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)
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 AllowBoxSelectInsideSelection = false; // Will set ImGuiMultiSelectFlags_SelectOnClickAlways
bool AllowDragUnselected = false; // Will set ImGuiMultiSelectFlags_SelectOnClickRelease
float IconSize = 32.0f;
float IconSize = 0;
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 UseScrollX = false; // Debug: submit twice the number of items per line (overflow horizontally to exercise ScrollX + box-select)
// State
ImVector<ExampleAsset> Items; // Our items
@@ -11121,12 +11237,15 @@ struct ExampleAssetsBrowser
// Layout: calculate number of icon per line and number of lines
LayoutItemSize = ImVec2(floorf(IconSize), floorf(IconSize));
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.
if (StretchSpacing && LayoutColumnCount > 1)
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);
LayoutSelectableSpacing = IM_MAX(floorf(LayoutItemSpacing) - IconHitSpacing, 0.0f);
LayoutOuterPadding = floorf(LayoutItemSpacing * 0.5f);
@@ -11134,6 +11253,9 @@ struct ExampleAssetsBrowser
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);
if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_MenuBar))
{
@@ -11183,6 +11305,7 @@ struct ExampleAssetsBrowser
ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32);
ImGui::SliderInt("Icon Hit Spacing", &IconHitSpacing, 0, 32);
ImGui::Checkbox("Stretch Spacing", &StretchSpacing);
ImGui::Checkbox("Use ScrollX", &UseScrollX);
ImGui::PopItemWidth();
ImGui::EndMenu();
}
@@ -11212,7 +11335,7 @@ struct ExampleAssetsBrowser
ImGuiIO& io = ImGui::GetIO();
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();
@@ -11356,6 +11479,8 @@ struct ExampleAssetsBrowser
}
}
clipper.End();
if (Items.Size == 0)
ImGui::Dummy(ImVec2(0, 0));
ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing
// 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)
/*
@@ -208,6 +208,7 @@ void ImGui::StyleColorsDark(ImGuiStyle* dst)
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_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_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
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_ScrollbarGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f);
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_SliderGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f);
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_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 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_SliderGrabActive] = ImVec4(0.46f, 0.54f, 0.80f, 0.60f);
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);
ArcFastVtx[i] = ImVec2(ImCos(a), ImSin(a));
}
ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError);
}
ImDrawListSharedData::~ImDrawListSharedData()
@@ -415,17 +417,17 @@ ImDrawListSharedData::~ImDrawListSharedData()
void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error)
{
if (CircleSegmentMaxError == max_error)
if (CircleTessellationMaxError == max_error)
return;
IM_ASSERT(max_error > 0.0f);
CircleSegmentMaxError = max_error;
CircleTessellationMaxError = max_error;
for (int i = 0; i < IM_COUNTOF(CircleSegmentCounts); 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)
@@ -534,9 +536,14 @@ void ImDrawList::_PopUnusedDrawCmd()
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);
ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1];
IM_ASSERT(callback != NULL);
IM_ASSERT(curr_cmd->UserCallback == NULL);
if (curr_cmd->ElemCount != 0)
{
@@ -654,11 +661,11 @@ void ImDrawList::_OnChangedVtxOffset()
int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const
{
// 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))
return _Data->CircleSegmentCounts[radius_idx]; // Use cached value
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)
@@ -668,7 +675,7 @@ void ImDrawList::PushClipRect(const ImVec2& cr_min, const ImVec2& cr_max, bool i
if (intersect_with_current_clip_rect)
{
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.z > current.z) cr.z = current.z;
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.
// 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)
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 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)
{
// 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)
{
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.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)
return;
PathLineTo(p1 + ImVec2(0.5f, 0.5f));
PathLineTo(p2 + ImVec2(0.5f, 0.5f));
PathStroke(col, 0, thickness);
const ImVec2 points[2] = { ImVec2(p1.x + 0.5f, p1.y + 0.5f), ImVec2(p2.x + 0.5f, p2.y + 0.5f) };
AddPolyline(points, 2, col, 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
// 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)
return;
if (Flags & ImDrawListFlags_AntiAliasedLines)
PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.50f, 0.50f), rounding, flags);
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.
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)
@@ -1553,7 +1570,7 @@ void ImDrawList::AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, c
PathLineTo(p2);
PathLineTo(p3);
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)
@@ -1576,7 +1593,7 @@ void ImDrawList::AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p
PathLineTo(p1);
PathLineTo(p2);
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)
@@ -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);
}
PathStroke(col, ImDrawFlags_Closed, thickness);
PathStroke(col, thickness, ImDrawFlags_Closed);
}
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
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);
PathStroke(col, ImDrawFlags_Closed, thickness);
PathStroke(col, thickness, ImDrawFlags_Closed);
}
// 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
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);
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)
@@ -1699,7 +1716,7 @@ void ImDrawList::AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2
PathLineTo(p1);
PathBezierCubicCurveTo(p2, p3, p4, num_segments);
PathStroke(col, 0, thickness);
PathStroke(col, thickness);
}
// Quadratic Bezier takes 3 controls points
@@ -1710,7 +1727,7 @@ void ImDrawList::AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const Im
PathLineTo(p1);
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)
@@ -1782,7 +1799,10 @@ void ImDrawList::AddImageRounded(ImTextureRef tex_ref, const ImVec2& p_min, cons
if ((col & IM_COL32_A_MASK) == 0)
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)
{
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
// - ImFontAtlas()
// - ImFontAtlas::Clear()
// - ImFontAtlas::CompactCache()
// - ImFontAtlas::ClearFonts()
// - ImFontAtlas::ClearInputData()
// - ImFontAtlas::ClearTexData()
// - ImFontAtlas::ClearFonts()
// - ImFontAtlas::CompactCache()
// - ImFontAtlas::SetFontLoader()
//-----------------------------------------------------------------------------
// - ImFontAtlasUpdateNewFrame()
// - ImFontAtlasTextureBlockConvert()
@@ -2547,8 +2568,6 @@ void ImTextureData::DestroyPixels()
// - ImFontAtlasBuildPreloadAllGlyphRanges()
// - ImFontAtlasBuildUpdatePointers()
// - ImFontAtlasBuildRenderBitmapFromString()
// - ImFontAtlasBuildUpdateBasicTexData()
// - ImFontAtlasBuildUpdateLinesTexData()
// - ImFontAtlasBuildAddFont()
// - ImFontAtlasBuildSetupFontBakedEllipsis()
// - ImFontAtlasBuildSetupFontBakedBlanks()
@@ -2565,13 +2584,14 @@ void ImTextureData::DestroyPixels()
// - ImFontAtlasUpdateDrawListsSharedData()
//-----------------------------------------------------------------------------
// - ImFontAtlasBuildSetTexture()
// - ImFontAtlasBuildAddTexture()
// - ImFontAtlasBuildMakeSpace()
// - ImFontAtlasBuildRepackTexture()
// - ImFontAtlasBuildGrowTexture()
// - ImFontAtlasBuildRepackOrGrowTexture()
// - ImFontAtlasBuildGetTextureSizeEstimate()
// - ImFontAtlasBuildCompactTexture()
// - ImFontAtlasBuildUpdateTexData()
// - ImFontAtlasTextureAdd()
// - ImFontAtlasTextureRepack()
// - ImFontAtlasTextureGrow()
// - ImFontAtlasTextureMakeSpace()
// - ImFontAtlasTextureGetSizeEstimate()
// - ImFontAtlasBuildClear()
// - ImFontAtlasTextureCompact()
// - ImFontAtlasBuildInit()
// - ImFontAtlasBuildDestroy()
//-----------------------------------------------------------------------------
@@ -2671,9 +2691,12 @@ ImFontAtlas::~ImFontAtlas()
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()
{
IMGUI_DEBUG_LOG_FONT("[font] ImFontAtlas::Clear()\n");
bool backup_renderer_has_textures = RendererHasTextures;
RendererHasTextures = false; // Full Clear() is supported, but ClearTexData() only isn't.
ClearFonts();
@@ -2681,20 +2704,28 @@ void ImFontAtlas::Clear()
RendererHasTextures = backup_renderer_has_textures;
}
void ImFontAtlas::CompactCache()
void ImFontAtlas::ClearFonts()
{
ImFontAtlasTextureCompact(this);
}
void ImFontAtlas::SetFontLoader(const ImFontLoader* font_loader)
{
ImFontAtlasBuildSetupFontLoader(this, font_loader);
// FIXME-NEWATLAS: Illegal to remove currently bound font.
IMGUI_DEBUG_LOG_FONT("[font] ImFontAtlas::ClearFonts()\n");
IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
for (ImFont* font : Fonts)
ImFontAtlasBuildNotifySetFont(this, font, NULL);
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()
{
IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
for (ImFont* font : Fonts)
ImFontAtlasFontDestroyOutput(this, font);
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.
}
void ImFontAtlas::ClearFonts()
void ImFontAtlas::CompactCache()
{
// FIXME-NEWATLAS: Illegal to remove currently bound font.
IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
for (ImFont* font : Fonts)
ImFontAtlasBuildNotifySetFont(this, font, NULL);
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;
}
ImFontAtlasTextureCompact(this);
}
void ImFontAtlas::SetFontLoader(const ImFontLoader* font_loader)
{
ImFontAtlasBuildSetupFontLoader(this, font_loader);
}
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.
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
if (atlas->RendererHasTextures)
{
@@ -2799,61 +2844,49 @@ void ImFontAtlasUpdateNewFrame(ImFontAtlas* atlas, int frame_count, bool rendere
builder->BakedPool.Size -= builder->BakedDiscardedCount;
builder->BakedDiscardedCount = 0;
}
}
// Update texture status
for (int tex_n = 0; tex_n < atlas->TexList.Size; tex_n++)
bool ImTextureDataUpdateNewFrame(ImTextureData* tex)
{
bool remove_from_list = false;
if (tex->Status == ImTextureStatus_OK)
{
ImTextureData* tex = atlas->TexList[tex_n];
bool remove_from_list = false;
if (tex->Status == ImTextureStatus_OK)
{
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--;
}
tex->Updates.resize(0);
tex->UpdateRect.x = tex->UpdateRect.y = (unsigned short)~0;
tex->UpdateRect.w = tex->UpdateRect.h = 0;
}
// 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)
@@ -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);
}
// Queue texture block update for renderer backend
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(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 };
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.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);
atlas->TexIsBuilt = false;
// No need to queue if status is == ImTextureStatus_WantCreate
if (tex->Status == ImTextureStatus_OK || tex->Status == ImTextureStatus_WantUpdates)
@@ -3056,7 +3093,7 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg_in)
}
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();
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->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);
if (!ImFontAtlasFontSourceInit(this, font_cfg))
@@ -3120,8 +3162,10 @@ static void Decode85(const unsigned char* src, unsigned char* dst)
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);
#endif
#if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_VECTOR)
static const char* GetDefaultCompressedFontDataProggyForever(int* out_size);
#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().
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();
if (!font_cfg_template)
font_cfg.PixelSnapH = true; // Prevents sub-integer scaling factors at lower-level layers.
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.Flags |= ImFontFlags_ImplicitRefSize;
}
if (font_cfg.Name[0] == '\0')
ImFormatString(font_cfg.Name, IM_COUNTOF(font_cfg.Name), "ProggyClean.ttf");
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_UNUSED(font_cfg_template);
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.
// (See build script in https://github.com/ocornut/proggyforever for details)
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();
if (!font_cfg_template)
font_cfg.PixelSnapH = true; // Precisely match ProggyClean, but prevents sub-integer scaling factors at lower-level layers.
if (font_cfg.SizePixels <= 0.0f)
{
font_cfg.SizePixels = 13.0f;
font_cfg.Flags |= ImFontFlags_ImplicitRefSize;
}
if (font_cfg.Name[0] == '\0')
ImFormatString(font_cfg.Name, IM_COUNTOF(font_cfg.Name), "ProggyForever.ttf");
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_UNUSED(font_cfg_template);
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)
@@ -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.
// 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);
}
static void ImFontAtlasBuildUpdateLinesTexData(ImFontAtlas* atlas)
static void ImFontAtlasBuildUpdateTexDataLines(ImFontAtlas* atlas)
{
if (atlas->Flags & ImFontAtlasFlags_NoBakedLines)
return;
@@ -4028,6 +4078,12 @@ static void ImFontAtlasBuildSetTexture(ImFontAtlas* atlas, ImTextureData* tex)
ImFontAtlasUpdateDrawListsTextures(atlas, old_tex_ref, atlas->TexRef);
}
static void ImFontAtlasBuildUpdateTexData(ImFontAtlas* atlas)
{
ImFontAtlasBuildUpdateTexDataBasic(atlas);
ImFontAtlasBuildUpdateTexDataLines(atlas);
}
// Create a new texture, discard previous one
ImTextureData* ImFontAtlasTextureAdd(ImFontAtlas* atlas, int w, int h)
{
@@ -4142,8 +4198,7 @@ void ImFontAtlasTextureRepack(ImFontAtlas* atlas, int w, int h)
}
// Update other cached UV
ImFontAtlasBuildUpdateLinesTexData(atlas);
ImFontAtlasBuildUpdateBasicTexData(atlas);
ImFontAtlasBuildUpdateTexData(atlas);
builder->LockDisableResize = false;
ImFontAtlasUpdateDrawListsSharedData(atlas);
@@ -4292,8 +4347,7 @@ void ImFontAtlasBuildInit(ImFontAtlas* atlas)
ImFontAtlasPackInit(atlas);
// Add required texture data
ImFontAtlasBuildUpdateLinesTexData(atlas);
ImFontAtlasBuildUpdateBasicTexData(atlas);
ImFontAtlasBuildUpdateTexData(atlas);
// Register fonts
ImFontAtlasBuildUpdatePointers(atlas);
@@ -4322,6 +4376,8 @@ void ImFontAtlasBuildDestroy(ImFontAtlas* atlas)
atlas->Builder = NULL;
}
//-----------------------------------------------------------------------------
void ImFontAtlasPackInit(ImFontAtlas * atlas)
{
ImTextureData* tex = atlas->TexData;
@@ -5718,8 +5774,13 @@ void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, Im
if (glyph->Colored)
col |= ~IM_COL32_A_MASK;
float scale = (size >= 0.0f) ? (size / baked->Size) : 1.0f;
float x = IM_TRUNC(pos.x);
float y = IM_TRUNC(pos.y);
float x = pos.x;
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 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().
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:
float x = IM_TRUNC(pos.x);
float y = IM_TRUNC(pos.y);
// Align to be pixel perfect
float x = pos.x;
float y = pos.y;
if (y > clip_rect.w)
return;
if ((draw_list->Flags & ImDrawListFlags_TextNoPixelSnap) == 0)
{
x = IM_TRUNC(x);
y = IM_TRUNC(y);
}
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.
@@ -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, by));
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.
@@ -6151,8 +6217,8 @@ void ImGui::RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p
flags = ImDrawFlags_RoundCornersDefault_;
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_bg2 = GetColorU32(ImAlphaBlendColors(IM_COL32(128, 128, 128, 255), col));
ImU32 col_bg1 = 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);
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);
if (y2 <= y1)
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);
if (x2 <= x1)
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 (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
//-----------------------------------------------------------------------------
#ifndef IMGUI_DISABLE_DEFAULT_FONT
#if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_BITMAP)
// File: 'ProggyClean.ttf' (41208 bytes)
// 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;
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)
@@ -6496,6 +6563,8 @@ static const char* GetDefaultCompressedFontDataProggyClean(int* out_size)
// 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)
// 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;
@@ -6750,7 +6819,6 @@ static const char* GetDefaultCompressedFontDataProggyForever(int* out_size)
*out_size = proggy_forever_minimal_ttf_compressed_size;
return (const char*)proggy_forever_minimal_ttf_compressed_data;
}
#endif // #ifndef IMGUI_DISABLE_DEFAULT_FONT
#endif // #if !defined(IMGUI_DISABLE_DEFAULT_FONT) && !defined(IMGUI_DISABLE_DEFAULT_FONT_VECTOR)
#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)
// 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_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
#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 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 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> 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; }
@@ -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 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 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 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); }
@@ -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; }
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 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 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; }
@@ -877,7 +884,7 @@ struct IMGUI_API ImDrawListSharedData
float FontSize; // Current font size (used for for simplified AddText overload)
float FontScale; // Current font scale (== FontSize / Font->FontSize)
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
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()
@@ -1010,6 +1017,7 @@ enum ImGuiItemStatusFlags_
ImGuiItemStatusFlags_HasClipRect = 1 << 9, // g.LastItemData.ClipRect is valid.
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_EditedInternal = 1 << 11, // Similar to ImGuiItemStatusFlags_Edited but bypassing ImGuiItemFlags_NoMarkEdited.
// Additional status + semantic for ImGuiTestEngine
#ifdef IMGUI_ENABLE_TEST_ENGINE
@@ -1215,6 +1223,7 @@ struct IMGUI_API ImGuiMenuColumns
};
// Internal temporary state for deactivating InputText() instances.
// Store as part of ImGuiDeactivatedItemData?
struct IMGUI_API ImGuiInputTextDeactivatedState
{
ImGuiID ID; // widget id owning the text state (which just got deactivated)
@@ -1468,6 +1477,7 @@ struct ImGuiPtrOrIndex
};
// Data used by IsItemDeactivated()/IsItemDeactivatedAfterEdit() functions
// Also see ImGuiInputTextDeactivatedState which is an extension for this for InputText()
struct ImGuiDeactivatedItemData
{
ImGuiID ID;
@@ -1715,6 +1725,7 @@ enum ImGuiActivateFlags_
};
// Early work-in-progress API for ScrollToItem()
// FIXME: Missing flags to request making both edges visible when possible.
enum ImGuiScrollFlags_
{
ImGuiScrollFlags_None = 0,
@@ -1912,6 +1923,7 @@ struct ImGuiBoxSelectState
// Temporary/Transient data
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 UnclipRects[2]; // Per-axis versions.
ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos)
ImRect BoxSelectRectCurr;
@@ -1934,7 +1946,8 @@ struct IMGUI_API ImGuiMultiSelectTempData
ImGuiMultiSelectFlags Flags;
ImVec2 ScopeRectMin;
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;
ImGuiKeyChord KeyMods;
ImS8 LoopRequestSetAll; // -1: no operation, 0: clear all, 1: select all.
@@ -2022,7 +2035,7 @@ enum ImGuiDockNodeState
ImGuiDockNodeState_HostWindowVisible,
};
// sizeof() 156~192
// sizeof() 176~216
struct IMGUI_API ImGuiDockNode
{
ImGuiID ID;
@@ -2039,8 +2052,8 @@ struct IMGUI_API ImGuiDockNode
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.
ImGuiAxis SplitAxis; // [Split node only] Split axis (X or Y)
ImGuiWindowClass WindowClass; // [Root node only]
ImU32 LastBgColor;
ImGuiWindowClass WindowClass; // [Root node only]
ImGuiWindow* HostWindow;
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
ImDrawListSharedData DrawListSharedData;
ImGuiID WithinEndChildID; // Set within EndChild()
ImGuiID WithinEndPopupID; // Set within EndPopup()
void* TestEngine; // Test engine user data
// Inputs
@@ -3439,7 +3453,7 @@ namespace ImGui
IMGUI_API void SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags);
// 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 RegisterFontAtlas(ImFontAtlas* atlas);
IMGUI_API void UnregisterFontAtlas(ImFontAtlas* atlas);
@@ -3557,6 +3571,7 @@ namespace ImGui
// Childs
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
IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags);
@@ -3662,7 +3677,7 @@ namespace ImGui
IMGUI_API ImGuiID GetKeyOwner(ImGuiKey key);
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 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'
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.
IMGUI_API void PushFocusScope(ImGuiID id);
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()
// Drag and Drop
@@ -3774,7 +3790,7 @@ namespace ImGui
IMGUI_API void ClearDragDrop();
IMGUI_API bool IsDragDropPayloadBeingAccepted();
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
// (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 TableUpdateBorders(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 TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display);
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 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 const char* ImTextureDataGetStatusName(ImTextureStatus status);
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)
/*
@@ -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 "-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 "-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
#endif
@@ -1315,14 +1316,32 @@ void ImGui::TableUpdateLayout(ImGuiTable* table)
table->InnerWindow->DecoInnerSizeY1 = table_instance->LastFrozenHeight;
table_instance->LastFrozenHeight = 0.0f;
// Initial state
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)
table->DrawSplitter->SetCurrentChannel(inner_window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP);
else
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()
// - Set table->HoveredColumnBorder with a short delay/timer to reduce visual feedback noise.
void ImGui::TableUpdateBorders(ImGuiTable* table)
@@ -1436,12 +1455,12 @@ void ImGui::EndTable()
if (table->Flags & ImGuiTableFlags_ScrollX)
{
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)
max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - outer_padding_for_border);
if (table->ResizedColumn != -1)
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
@@ -1550,7 +1569,7 @@ void ImGui::EndTable()
}
else
{
table->InnerWindow->DC.TreeDepth--;
inner_window->DC.TreeDepth--;
ItemSize(table->OuterRect.GetSize());
ItemAdd(table->OuterRect, 0);
}
@@ -1565,13 +1584,12 @@ void ImGui::EndTable()
}
else if (temp_data->UserOuterSize.x <= 0.0f)
{
// Some references for this: #7651 + tests "table_reported_size", "table_reported_size_outer" equivalent Y block
// - 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.
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 + ((table->Flags & ImGuiTableFlags_ScrollY) ? inner_window->ScrollbarSizes.x : 0.0f);
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, inner_content_max_x + decoration_size));
// Some references for this: #7651 + tests "table_reported_size", "table_reported_size_outer" equivalent Y block, #9352
// - 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'
const float outer_content_max_x = table->OuterRect.Min.x + table->ColumnsAutoFitWidth;
const float decoration_size = table->TempData->AngledHeadersExtraWidth + ((inner_window != outer_window) ? 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.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, ImMin(table->OuterRect.Max.x, outer_content_max_x + decoration_size));
}
else
{
@@ -1579,9 +1597,12 @@ void ImGui::EndTable()
}
if (temp_data->UserOuterSize.y <= 0.0f)
{
const float decoration_size = (table->Flags & ImGuiTableFlags_ScrollX) ? inner_window->ScrollbarSizes.y : 0.0f;
outer_window->DC.IdealMaxPos.y = ImMax(outer_window->DC.IdealMaxPos.y, inner_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, inner_content_max_y + decoration_size));
// (same comment as above)
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);
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
{
@@ -1641,7 +1662,7 @@ void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, flo
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->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()");
ImGuiTableColumn* column = &table->Columns[table->DeclColumnsCount];
@@ -2053,11 +2074,11 @@ void ImGui::TableEndRow(ImGuiTable* table)
// Draw top border
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)
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)
@@ -2834,7 +2855,7 @@ void ImGui::TableDrawBorders(ImGuiTable* table)
else if ((table->Flags & (ImGuiTableFlags_NoBordersInBodyUntilResize | ImGuiTableFlags_NoBordersInBody)) == 0)
draw_y2 = draw_y2_body;
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;
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)
{
inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Min.x, 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.Min.x, outer_border.Min.y, outer_border.Max.y, 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)
{
inner_drawlist->AddLine(outer_border.Min, ImVec2(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.Min.y, 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)
@@ -2869,7 +2890,7 @@ void ImGui::TableDrawBorders(ImGuiTable* table)
// Draw bottom-most row border between it is above outer border.
const float border_y = table->RowPosY2;
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();
@@ -3174,7 +3195,7 @@ void ImGui::TableHeader(const char* label)
if (label == NULL)
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;
// 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
if (IsPopupOpenRequestForItem(ImGuiPopupFlags_None, id))
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.
@@ -4595,7 +4618,7 @@ void ImGui::EndColumns()
// Draw column
const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
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.
+263 -113
View File
@@ -1,4 +1,4 @@
// dear imgui, v1.92.7
// dear imgui, v1.92.9 WIP
// (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 "-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 "-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
#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)
{
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;
ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
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 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
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)
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, ...)
@@ -788,7 +792,8 @@ bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
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;
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)
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
//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)
IM_ASSERT(scrollbar_size >= 0.0f);
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)
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
@@ -1155,7 +1160,7 @@ void ImGui::ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const Im
else
window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
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)
@@ -1250,7 +1255,8 @@ bool ImGui::Checkbox(const char* label, bool* v)
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
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 ImVec2 pos = window->DC.CursorPos;
@@ -1291,8 +1297,9 @@ bool ImGui::Checkbox(const char* label, bool* v)
if (is_visible)
{
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);
RenderFrame(check_bb.Min, check_bb.Max, bg_col, true, style.FrameRounding);
if (mixed_value)
{
// Undocumented tristate/mixed/indeterminate checkbox (#2644)
@@ -1310,7 +1317,7 @@ bool ImGui::Checkbox(const char* label, bool* v)
if (g.LogEnabled)
LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
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));
return pressed;
@@ -1372,7 +1379,8 @@ bool ImGui::RadioButton(const char* label, bool active)
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
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 ImVec2 pos = window->DC.CursorPos;
@@ -1411,7 +1419,7 @@ bool ImGui::RadioButton(const char* label, bool active)
if (g.LogEnabled)
LogRenderedText(&label_pos, active ? "(x)" : "( )");
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);
return pressed;
@@ -1527,7 +1535,7 @@ bool ImGui::TextLink(const char* label)
const char* label_end = FindRenderedTextEnd(label);
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);
ItemSize(size, 0.0f);
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);
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));
RenderText(bb.Min, label, label_end);
RenderText(bb.Min, label, label_end, false);
PopStyleColor();
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
@@ -1731,7 +1739,7 @@ void ImGui::Separator()
if (window->DC.CurrentColumns)
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)
@@ -1747,14 +1755,14 @@ void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end
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 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);
if (!ItemAdd(bb, id))
return;
const float sep1_x1 = pos.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 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 sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
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)
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)
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);
@@ -1780,7 +1788,7 @@ void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end
if (g.LogEnabled)
LogText("---");
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);
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
const ImVec2 label_size = CalcTextSize(label, NULL, true);
const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f;
const char* label_end = FindRenderedTextEnd(label);
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 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));
@@ -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);
}
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)
return false;
@@ -2376,12 +2385,17 @@ bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void
// 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
// - 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];
if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
{
format = type_info->ScanFmt;
}
else
{
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()
int v32 = 0;
@@ -2716,7 +2730,8 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data,
const float w = CalcItemWidth();
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 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));
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));
return value_changed;
@@ -3317,7 +3332,8 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat
const float w = CalcItemWidth();
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 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));
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));
return value_changed;
@@ -3494,7 +3510,8 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
const ImGuiStyle& style = g.Style;
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 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);
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)
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;
}
@@ -3563,7 +3581,8 @@ bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min,
// - ImParseFormatSanitizeForPrinting() [Internal]
// - ImParseFormatSanitizeForScanning() [Internal]
// - ImParseFormatPrecision() [Internal]
// - TempInputTextScalar() [Internal]
// - TempInputText() [Internal]
// - TempInputScalar() [Internal]
// - InputScalar()
// - InputScalarN()
// - InputFloat()
@@ -3792,7 +3811,7 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
ImGuiContext& g = *GImGui;
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)
format = DataTypeGetInfo(data_type)->PrintFmt;
@@ -3829,7 +3848,8 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
}
// 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
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)))
{
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);
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);
value_changed = true;
value_changed = ret = true;
}
PopItemFlag();
if (flags & ImGuiInputTextFlags_ReadOnly)
@@ -3871,6 +3891,8 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
if (value_changed)
MarkItemEdited(g.LastItemData.ID);
if (flags & ImGuiInputTextFlags_EnterReturnsTrue)
return ret;
return value_changed;
}
@@ -4520,6 +4542,9 @@ static bool InputTextFilterCharacter(ImGuiContext* ctx, ImGuiInputTextState* sta
callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
callback_data.EventChar = (ImWchar)c;
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;
if (callback(&callback_data) != 0)
return false;
@@ -4568,6 +4593,7 @@ void ImGui::InputTextDeactivateHook(ImGuiID id)
ImGuiInputTextState* state = &g.InputTextState;
if (id == 0 || state->ID != id)
return;
//IMGUI_DEBUG_LOG_ACTIVEID("InputTextDeactivateHook() id = 0x%08X\n", id);
g.InputTextDeactivatedState.ID = state->ID;
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)
BeginGroup();
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 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;
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();
return false;
@@ -4742,7 +4770,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
g.NavActivateId = backup_activate_id;
PopStyleVar(3);
PopStyleColor();
if (!child_visible)
if (!child_visible && !no_clip)
{
EndChild();
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;
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);
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->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.
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
// 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);
if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0)))
bool recycle_state = (state->ID == id && !init_changed_specs_multiline);
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;
// 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;
if (g.ActiveId != id && init_make_active)
if (init_make_active && g.ActiveId != id)
{
IM_ASSERT(state && state->ID == id);
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)
PushPasswordFont();
// Word-wrapping: attempt to keep cursor in view while resizing frame/parent
// FIXME-WORDWRAP: It would be better to preserve same relative offset.
if (is_wordwrap && state != NULL && state->ID == id && state->WrapWidth != wrap_width)
if (state != NULL && state->ID == id)
{
state->CursorCenterY = true;
state->WrapWidth = wrap_width;
render_cursor = true;
state->Flags = flags;
// 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
@@ -4966,7 +4999,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
IM_ASSERT(state != NULL);
state->EditedThisFrame = false;
state->BufCapacity = buf_size;
state->Flags = flags;
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.
@@ -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);
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))
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.)
// 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)
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)
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 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->PathStroke(col_white, 0, wheel_thickness);
draw_list->PathStroke(col_white, wheel_thickness);
const int vert_end_idx = draw_list->VtxBuffer.Size;
// 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)
RenderFrameBorder(bb.Min, bb.Max, rounding);
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
@@ -7149,11 +7181,11 @@ void ImGui::TreeNodeDrawLineToChildNode(const ImVec2& target_pos)
window->DrawList->PathArcToFast(ImVec2(x1, y - rounding), rounding, 6, 3);
if (x1 < x2)
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
{
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);
if (data->DrawLinesTableColumn != -1)
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)
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.
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 pos = window->DC.CursorPos;
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.
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
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;
// 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 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already
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->BoxSelectRectCurr.Min = ImMin(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)
// Storing an extra rect used by widgets supporting box-select.
if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
// Box-select 2D mode detects change of the rectangle.
// Storing unclip rects which will be tested by widgets supporting box-select. Always update rectangles when active (even if we don't use them).
// To facilitate understanding this: enable IMGUI_DEBUG_BOXSELECT and visualize all geometry.
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;
bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis.
bs->UnclipRect.Add(bs->BoxSelectRectCurr);
ImVec2 d_min = (side == 0) ? ImMin(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMin(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max);
ImVec2 d_max = (side == 0) ? ImMax(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMax(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max);
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->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;
}
@@ -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
ImRect box_select_r = bs->BoxSelectRectCurr;
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
window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling
ImGuiWindow* draw_window = FindFrontMostVisibleChildWindow(window);
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
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)
{
ImGuiContext& g = *GImGui;
if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
{
// 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));
}
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;
if (g.CurrentTable != NULL)
scope_rect = g.CurrentTable->HostClipRect;
//if (g.CurrentTable != NULL)
// scope_rect = g.CurrentTable->HostClipRect;
// 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);
@@ -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)
// They should perhaps be stacked properly?
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.
}
// FIXME: BeginFocusScope()
const ImGuiID id = window->IDStack.back();
ms->Clear();
ms->FocusScopeId = id;
ms->Flags = flags;
ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
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);
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.
window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
@@ -8030,7 +8123,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel
storage->LastSelectionSize = 0;
}
ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid;
//ms->PrevSubmittedItem = ImGuiSelectionUserData_Invalid;
if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO);
@@ -8076,7 +8169,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect()
// Clear selection when clicking void?
// We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
// 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))
scope_hovered &= scope_rect.Contains(g.IO.MousePos);
if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
@@ -8102,10 +8195,13 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect()
if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
{
IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
NavMoveRequestTryWrapping(GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
}
// Unwind
if (ImGuiTable* table = g.CurrentTable)
if (table->IsInsideRow)
TableEndRow(table);
window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos);
PopFocusScope();
@@ -8133,6 +8229,8 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
if (ms->IO.RangeSrcItem == selection_user_data)
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
{
@@ -8281,8 +8379,31 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
if (ms->BoxSelectId != 0)
if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId))
{
const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect);
const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect);
ImRect item_rect = 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 (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
@@ -8294,6 +8415,9 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
{
selected = !selected;
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);
}
@@ -8407,7 +8531,6 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
}
if (storage->NavIdItem == item_data)
ms->NavIdPassedBy = true;
ms->LastSubmittedItem = item_data;
*p_selected = selected;
*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)
{
// 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)
{
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;
return;
}
}
#endif
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
@@ -8669,7 +8797,8 @@ bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
const ImGuiStyle& style = g.Style;
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.
// 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)
{
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);
AlignTextToFramePadding();
}
@@ -8773,9 +8902,7 @@ bool ImGui::ListBox(const char* label, int* current_item, const char* (*getter)(
// - PlotHistogram()
//-------------------------------------------------------------------------
// Plot/Graph widgets are not very good.
// Consider writing your own, or using a third-party one, see:
// - ImPlot https://github.com/epezent/implot
// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
// Consider using ImPlot (https://github.com/epezent/implot) which is much better!
//-------------------------------------------------------------------------
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 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 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
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);
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));
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.
// 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;
}
@@ -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);
}
// 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)
{
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
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)
// 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;
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoAutoClosePopups | (ImGuiSelectableFlags)ImGuiSelectableFlags_SelectOnClick;
ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
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);
pressed = Selectable("", menu_is_open, selectable_flags, label_size);
LogSetNextTextDecoration("[", "]");
RenderText(text_pos, label);
RenderText(text_pos, label, label_end, false);
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);
@@ -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);
pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
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)
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);
@@ -9315,6 +9445,14 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
if (!enabled)
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;
if (menuset_is_open)
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));
PopID();
if (g.ActiveId == id && want_open)
g.ActiveIdNoClearOnFocusLoss = true;
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.
@@ -9471,7 +9612,8 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut
ImGuiContext& g = *GImGui;
ImGuiStyle& style = g.Style;
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.
const bool menuset_is_open = IsRootOfOpenMenuSet();
@@ -9486,7 +9628,7 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut
BeginDisabled();
// 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;
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));
PopStyleVar();
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().
}
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));
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)
RenderText(text_pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
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);
}
}
// 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));
if (!enabled)
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)
const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
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;
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;
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->PathStroke(overline_col, 0, style.TabBarOverlineSize);
display_draw_list->PathStroke(overline_col, style.TabBarOverlineSize);
}
else
{
@@ -10845,20 +10998,16 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI
IM_UNUSED(flags);
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 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;
draw_list->PathLineTo(ImVec2(bb.Min.x, y2));
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);
draw_list->AddRectFilled(bb.Min, ImVec2(bb.Max.x, y2), col, rounding, ImDrawFlags_RoundCornersTop);
if (g.Style.TabBorderSize > 0.0f)
{
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.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
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)
{
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)
*out_just_closed = false;
@@ -10952,7 +11102,7 @@ void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb,
}
}
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 (!is_contents_visible)
+1
View File
@@ -39,6 +39,7 @@
#define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <functional>
+6 -5
View File
@@ -38,11 +38,12 @@ std::vector<Analog> &games::bc::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Stick X",
"Stick Y"
);
using GameAPI::Analogs::AnalogType;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Stick X", AnalogType::LinearCentered },
{ "Stick Y", AnalogType::LinearCentered }
});
}
return analogs;
-1
View File
@@ -7,7 +7,6 @@
#include "util/utils.h"
#include "util/detour.h"
#include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h"
#include "bi2x_hook.h"
#include "trackball.h"
+8 -7
View File
@@ -45,13 +45,14 @@ std::vector<Analog> &games::ccj::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Joystick X",
"Joystick Y",
"Trackball DX",
"Trackball DY"
);
using GameAPI::Analogs::AnalogType;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Joystick X", AnalogType::LinearCentered },
{ "Joystick Y", AnalogType::LinearCentered },
{ "Trackball DX", AnalogType::LinearCentered },
{ "Trackball DY", AnalogType::LinearCentered }
});
}
return analogs;
+165 -102
View File
@@ -6,6 +6,7 @@
#include "util/logging.h"
#include "rawinput/rawinput.h"
#include "games/io.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "io.h"
@@ -32,13 +33,14 @@ namespace games::ccj {
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
UINT cbSize) {
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
if (result == 0xFFFFFFFF)
if (result == 0xFFFFFFFF) {
return result;
}
if (pRawInputDeviceList == NULL) {
(*puiNumDevices)++;
} else if (result < *puiNumDevices) {
pRawInputDeviceList[result] = {fakeHandle, 0};
pRawInputDeviceList[result] = { fakeHandle, 0 };
result++;
}
@@ -46,8 +48,9 @@ namespace games::ccj {
}
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);
}
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
@@ -67,23 +70,157 @@ namespace games::ccj {
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
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);
}
hWnd = _hWnd;
wndProc = (WNDPROC)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) {
if (hRawInput != fakeHandle)
if (hRawInput != fakeHandle) {
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
}
if (pData == NULL) {
if (uiCommand == RID_HEADER)
if (uiCommand == RID_HEADER) {
*pcbSize = sizeof(RAWINPUTHEADER);
else
} else {
*pcbSize = sizeof(RAWINPUT);
}
return 0;
}
@@ -106,92 +243,9 @@ namespace games::ccj {
RAWMOUSE rawMouse {};
if (MOUSE_TRACKBALL) {
static bool active = false;
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() & 0x8000;
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;
}
trackball_mouse_input(rawMouse);
} else {
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;
trackball_mapped_input(rawMouse);
}
*((RAWINPUT*)pData) = { header, { rawMouse } };
@@ -202,10 +256,19 @@ namespace games::ccj {
}
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;
}
@@ -214,9 +277,8 @@ namespace games::ccj {
static bool initialized = false;
if (initialized) {
return;
} else {
initialized = true;
}
initialized = true;
// announce
log_info("trackball", "init");
@@ -236,30 +298,31 @@ namespace games::ccj {
}
void trackball_thread_start() {
using namespace std::chrono_literals;
tbThreadRunning = true;
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
tbThread = new std::thread([&] {
timeutils::PreciseSleepTimer timer;
while (tbThreadRunning) {
if (hWnd && wndProc) {
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
}
if (!tbThreadRunning)
if (!tbThreadRunning) {
break;
}
std::this_thread::sleep_for(10ms);
timer.sleep(10);
}
});
}
void trackball_thread_stop() {
tbThreadRunning = false;
if (tbThread)
if (tbThread) {
tbThread->join();
}
log_info("trackball", "thread stop");
+58 -11
View File
@@ -23,6 +23,7 @@
#include "p3io/usbmem.h"
#include "p4io/p4io.h"
#include <cstring>
using namespace acioemu;
@@ -53,9 +54,9 @@ namespace games::ddr {
return SendMessage_real(hWnd, Msg, wParam, lParam);
}
bool contains_only_ascii(const std::string& str) {
bool contains_only_ascii(const std::wstring& str) {
for (auto c: str) {
if (static_cast<unsigned char>(c) > 127) {
if (c > 127) {
return false;
}
}
@@ -80,24 +81,24 @@ namespace games::ddr {
}
if (fileutils::dir_exists(dir)) {
log_info("ddr", "looking for codecs in this directory: {}", dir.string());
log_info("ddr", "looking for codecs in this directory: {}", dir);
} else {
log_info("ddr", "codecs directory not found: {}", dir.string());
log_info("ddr", "codecs directory not found: {}", dir);
return;
}
for (const auto &file : std::filesystem::directory_iterator(dir)) {
const auto &filename = file.path().filename();
const auto extension = strtolower(filename.extension().string());
const auto extension = filename.extension().wstring();
if (extension != ".dll") {
if (wcsicmp(extension.c_str(), L".dll") != 0) {
continue;
}
log_info("ddr", "found DLL: {}, size: {} bytes", filename.string(), file.file_size());
if (filename == "k-clvsd.dll" || filename.string().find("xactengine") == 0) {
log_info("ddr", "found DLL: {}, size: {} bytes", filename, file.file_size());
if (filename == "k-clvsd.dll" || filename.wstring().find(L"xactengine") == 0) {
const std::wstring wcmd = L"regsvr32.exe /s \"" + file.path().wstring() + L"\"";
const std::string cmd = "regsvr32.exe /s \"" + file.path().string() + "\"";
const std::string cmd = fmt::format("regsvr32.exe /s \"{}\"", file.path());
int result = 0;
std::thread t([wcmd, &result]() {
@@ -111,12 +112,12 @@ namespace games::ddr {
log_warning("ddr", "`{}` failed, returned {}", cmd, result);
deferredlogs::defer_error_messages({
"DDR codec registration failure",
fmt::format(" {} failed to register with error {}", filename.string(), result)});
fmt::format(" {} failed to register with error {}", filename, result)});
}
static std::once_flag printed;
std::call_once(printed, [file]() {
if (!contains_only_ascii(file.path().string())) {
if (!contains_only_ascii(file.path().wstring())) {
log_warning(
"ddr",
"BAD PATH ERROR\n\n"
@@ -290,4 +291,50 @@ namespace games::ddr {
// dispose device hook
devicehook_dispose();
}
static void get_analog_xy_axis(Analog &analog, bool &less, bool &more) {
if (!analog.isSet()) {
return;
}
const auto value = GameAPI::Analogs::getState(RI_MGR, analog);
// stepmania linux source:
// https://github.com/stepmania/stepmania/blob/d55acb1ba26f1c5b5e3048d6d6c0bd116625216f/src/arch/InputHandler/InputHandler_Linux_Event.cpp#L410
// for example, value cap with range [0, 255]:
// 127 = 0.498, neutral
// 128 = 0.502, both
// apparently some adapters report values like above; this obviously makes a lot of
// assumptions about bit width of the HID value cap and how the adapter reports
// neutral/both values, but this is good enough for parity with stepmania and its many forks
if (0.5001f < value && value < 0.75f) {
less |= true;
more |= true;
} else if (value <= 0.25f) {
less |= true;
} else if (value >= 0.75f) {
more |= true;
}
}
void get_analog_x_axis(int player, bool &left, bool &right) {
if (player != 1 && player != 2) {
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
return;
}
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_LEFT_RIGHT : Analogs::P2_LEFT_RIGHT);
get_analog_xy_axis(analog, left, right);
}
void get_analog_y_axis(int player, bool &up, bool &down) {
if (player != 1 && player != 2) {
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
return;
}
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_UP_DOWN : Analogs::P2_UP_DOWN);
get_analog_xy_axis(analog, up, down);
}
}
+3
View File
@@ -23,4 +23,7 @@ namespace games::ddr {
private:
void register_codecs();
};
void get_analog_x_axis(int player, bool &left, bool &right);
void get_analog_y_axis(int player, bool &up, bool &down);
}
+44
View File
@@ -35,6 +35,50 @@ std::vector<Button> &games::ddr::get_buttons() {
return analogs;
}
std::string games::ddr::get_buttons_help() {
// keep to max 100 characters wide
return
"For DDR pad arrows, double check that simultaneous Left+Right and Up+Down can be detected.\n"
"You should boot the game, enter test menu, and use Foot Panel Check.\n\n"
"When mapping arrows, try the following in order:\n\n"
" 1. If your controller uses face buttons (A/B/X/Y), bind them here.\n"
" 2. If your controller supports XInput, use that.\n"
" 3. If your controller uses analog axis for arrows, try Analogs tab.\n"
" 4. Otherwise, you will need to use remapping software or get a better adapter."
;
}
std::vector<Analog> &games::ddr::get_analogs() {
static std::vector<Analog> analogs;
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Dance Dance Revolution");
using namespace GameAPI::Analogs;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "P1 Left-Right (Axis Fix)", AnalogType::LinearCentered },
{ "P1 Up-Down (Axis Fix)", AnalogType::LinearCentered },
{ "P2 Left-Right (Axis Fix)", AnalogType::LinearCentered },
{ "P2 Up-Down (Axis Fix)", AnalogType::LinearCentered }
});
}
return analogs;
}
std::string games::ddr::get_analogs_help() {
// keep to max 100 characters wide
return
"Only use this if your DDR pad outputs analog axis for arrows.\n\n"
"If the pad uses face buttons (A/B/X/Y), use Buttons tab instead.\n\n"
"Spice will treat values <=25% as Left, >=75% as Right, ~=50% as neutral,\n"
"and value between 50% and 75% as both arrows.\n\n"
"This is the classic Stepmania \"Axis Fix\" which may or may not work with\n"
"your dance pad or your adapter."
;
}
std::vector<Light> &games::ddr::get_lights() {
static std::vector<Light> lights;
+13
View File
@@ -33,6 +33,16 @@ namespace games::ddr {
};
}
// all analogs in correct order
namespace Analogs {
enum {
P1_LEFT_RIGHT,
P1_UP_DOWN,
P2_LEFT_RIGHT,
P2_UP_DOWN
};
}
// all lights in correct order
namespace Lights {
enum {
@@ -177,5 +187,8 @@ namespace games::ddr {
// getters
std::vector<Button> &get_buttons();
std::string get_buttons_help();
std::string get_analogs_help();
std::vector<Analog> &get_analogs();
std::vector<Light> &get_lights();
}
+52 -2
View File
@@ -3,6 +3,7 @@
#include "cfg/api.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "../ddr.h"
@@ -459,7 +460,8 @@ int games::ddr::DDRP3IOHandle::device_io(
if (nOutBufferSize >= 4) {
// cool down
Sleep(1);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// get controls as single variable (4 bytes)
auto &controls = *(uint32_t*) lpOutBuffer;
@@ -498,32 +500,80 @@ int games::ddr::DDRP3IOHandle::device_io(
// shift table
static size_t shift_table[] = {
30, 28, 29, 8, 9, 10, 11, 12, 24, 25, 14, 15, 16, 17, 18, 19, 20, 26, 27, 22, 23
30, 28, 29, // service, test, coin
8, // p1 start
9, 10, 11, 12, // p1 panel
24, 25, 14, 15, // p1 menu
16, // p2 start
17, 18, 19, 20, // p2 panel
26, 27, 22, 23 // p2 menu
};
static size_t button_table[] = {
Buttons::SERVICE,
Buttons::TEST,
Buttons::COIN_MECH,
Buttons::P1_START,
Buttons::P1_PANEL_UP,
Buttons::P1_PANEL_DOWN,
Buttons::P1_PANEL_LEFT,
Buttons::P1_PANEL_RIGHT,
Buttons::P1_MENU_UP,
Buttons::P1_MENU_DOWN,
Buttons::P1_MENU_LEFT,
Buttons::P1_MENU_RIGHT,
Buttons::P2_START,
Buttons::P2_PANEL_UP,
Buttons::P2_PANEL_DOWN,
Buttons::P2_PANEL_LEFT,
Buttons::P2_PANEL_RIGHT,
Buttons::P2_MENU_UP,
Buttons::P2_MENU_DOWN,
Buttons::P2_MENU_LEFT,
Buttons::P2_MENU_RIGHT,
};
// get analogs
struct {
bool up;
bool down;
bool left;
bool right;
} analog_values[2] = {};
games::ddr::get_analog_x_axis(1, analog_values[0].left, analog_values[0].right);
games::ddr::get_analog_y_axis(1, analog_values[0].up, analog_values[0].down);
games::ddr::get_analog_x_axis(2, analog_values[1].left, analog_values[1].right);
games::ddr::get_analog_y_axis(2, analog_values[1].up, analog_values[1].down);
if (analog_values[0].up) {
controls |= 1 << 9;
}
if (analog_values[0].down) {
controls |= 1 << 10;
}
if (analog_values[0].left) {
controls |= 1 << 11;
}
if (analog_values[0].right) {
controls |= 1 << 12;
}
if (analog_values[1].up) {
controls |= 1 << 17;
}
if (analog_values[1].down) {
controls |= 1 << 18;
}
if (analog_values[1].left) {
controls |= 1 << 19;
}
if (analog_values[1].right) {
controls |= 1 << 20;
}
// update states
auto &buttons = get_buttons();
size_t count = 0;
+3 -1
View File
@@ -2,6 +2,7 @@
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "../io.h"
@@ -255,7 +256,8 @@ namespace games::ddr {
case P4IO_IOCTL_GET_INPUTS: {
// Prevents this function from being called at its normal 2000-2500 kHz cadence and overloading the CPU, instead reduces it to 250 Hz
Sleep(4);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(4);
memset(lpOutBuffer, 0, 16);
auto controls = (uint32_t*) lpOutBuffer;
+3 -1
View File
@@ -7,6 +7,7 @@
#include "games/game.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/memutils.h"
#include "rgb_cam.h"
@@ -221,10 +222,11 @@ namespace games::drs {
void start_touch() {
std::thread t([] {
log_info("drs", "starting touch input thread");
timeutils::PreciseSleepTimer timer;
// main loop
while (TRUE) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
timer.sleep(1);
TOUCH_EVENTS.clear();
touch_get_events(TOUCH_EVENTS);
+8 -7
View File
@@ -35,13 +35,14 @@ std::vector<Analog> &games::ftt::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("FutureTomTom");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Pad 1",
"Pad 2",
"Pad 3",
"Pad 4"
);
using GameAPI::Analogs::AnalogType;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Pad 1", AnalogType::LinearPositive },
{ "Pad 2", AnalogType::LinearPositive },
{ "Pad 3", AnalogType::LinearPositive },
{ "Pad 4", AnalogType::LinearPositive }
});
}
return analogs;
+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();
}
+411 -30
View File
@@ -1,18 +1,27 @@
#include "gitadora.h"
#include "asio.h"
#include "handle.h"
#include "bi2x_hook.h"
#include <unordered_map>
#include <ks.h>
#include <ksmedia.h>
#include "cfg/configurator.h"
#include "hooks/audio/audio.h"
#include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "touch/native/nativetouchhook.h"
#include "util/cpuutils.h"
#include "util/deferlog.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/sigscan.h"
#include "util/socd_cleaner.h"
#include "util/sysutils.h"
#include "util/utils.h"
#include "hooks/setupapihook.h"
namespace games::gitadora {
@@ -20,11 +29,13 @@ namespace games::gitadora {
// settings
bool TWOCHANNEL = false;
std::optional<unsigned int> CAB_TYPE = std::nullopt;
bool ARENA_SINGLE_WINDOW = false;
bool P1_LEFTY = false;
bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
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
@@ -221,13 +232,6 @@ namespace games::gitadora {
}
#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() &&
GRAPHICS_WINDOWED && !cfg::CONFIGURATOR_STANDALONE) {
overlay::UI_SCALE_PERCENT = 250;
}
// for guitar wail SOCD cleaning
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
@@ -237,12 +241,334 @@ namespace games::gitadora {
} else {
log_info("gitadora", "guitar pick SOCD algorithm: legacy");
}
#if SPICE64 && !SPICE_XP
if (is_arena_model()) {
// in full screen, if single-adapter option is checked, it's functionally
// the same as forcing a single monitor
if (!GRAPHICS_WINDOWED && GRAPHICS_FORCE_SINGLE_ADAPTER) {
ARENA_WINDOW_COUNT = 1;
}
// 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);
}
}
#endif
}
}
#if SPICE64 && !SPICE_XP
static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr;
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
// cached primary real monitor: its path + source/target mode entries.
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
// fake monitors appended after the real ones. the game classifies monitors
// by outputTechnology + connectorInstance:
// HDMI -> main 4k monitor (real primary)
// DP connInstance 0 -> left
// DP connInstance 1 -> right
// DP connInstance 2 -> small (sub/touch)
// ids are negated on the fake monitor headers so they can be distinguished
// from real ones in DisplayConfigGetDeviceInfo.
struct FakeMonitor {
LONG id;
int width;
int height;
int offset_x;
int offset_y;
UINT32 connector_instance;
};
// ORDERING MATTERS: the d3d9 wrapper (FAKE_SUBSCREEN_ADAPTER) hands out
// adapters as "\\.\DISPLAY_SPICE_FAKE_{N}" for N=1,2,3. The game maps each
// adapter to a swap chain role via its DisplayConfig connector instance,
// so the entries here must be listed in the same order the wrapper enumerates
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
static constexpr FakeMonitor FAKE_MONITORS[] = {
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
};
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
// call QueryDisplayConfig once, keep only the primary monitor's path and its
// two referenced modes (source + target). modeInfoIdx values are rewritten to
// 0 and 1 so the cache is self-consistent.
static void cache_primary_monitor_info() {
UINT32 path_count = 0;
UINT32 mode_count = 0;
if (GetDisplayConfigBufferSizes_orig(
QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: GetDisplayConfigBufferSizes failed");
}
std::vector<DISPLAYCONFIG_PATH_INFO> all_paths(path_count);
std::vector<DISPLAYCONFIG_MODE_INFO> all_modes(mode_count);
if (QueryDisplayConfig_orig(
QDC_ONLY_ACTIVE_PATHS,
&path_count, all_paths.data(),
&mode_count, all_modes.data(),
nullptr) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: QueryDisplayConfig failed");
}
all_paths.resize(path_count);
all_modes.resize(mode_count);
// pick the primary monitor: source mode at (0, 0)
auto primary = std::find_if(all_paths.begin(), all_paths.end(),
[&](const auto &p) {
const auto idx = p.sourceInfo.modeInfoIdx;
return idx < all_modes.size() &&
all_modes[idx].infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE &&
all_modes[idx].sourceMode.position.x == 0 &&
all_modes[idx].sourceMode.position.y == 0;
});
if (primary == all_paths.end()) {
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
}
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
real_primary_path = *primary;
real_primary_path.sourceInfo.modeInfoIdx = 0;
real_primary_path.targetInfo.modeInfoIdx = 1;
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
}
static
LONG
WINAPI
GetDisplayConfigBufferSizes_hook(
UINT32 Flags,
UINT32 *pNumPathArrayElements,
UINT32 *pNumModeInfoArrayElements)
{
// populate cached primary real monitor on the first call
static std::once_flag populate_once;
std::call_once(populate_once, cache_primary_monitor_info);
// always report exactly 1 real + N fake monitors
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
log_info(
"gitadora",
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
FAKE_MONITOR_COUNT);
return ERROR_SUCCESS;
}
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
// assumption): index 0 in both arrays holds the cached primary real monitor, so
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
static void insert_fake_monitor(
DISPLAYCONFIG_PATH_INFO *paths,
DISPLAYCONFIG_MODE_INFO *modes,
UINT32 i)
{
const FakeMonitor &m = FAKE_MONITORS[i];
const UINT32 src_idx = 2 + i * 2;
const UINT32 tgt_idx = src_idx + 1;
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
const UINT32 uid = static_cast<UINT32>(-m.id);
paths[1 + i] = {
.sourceInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = src_idx,
.statusFlags = DISPLAYCONFIG_SOURCE_IN_USE,
},
.targetInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = tgt_idx,
.outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL,
.rotation = DISPLAYCONFIG_ROTATION_IDENTITY,
.scaling = DISPLAYCONFIG_SCALING_IDENTITY,
.refreshRate = { .Numerator = 60000, .Denominator = 1000 },
.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
.targetAvailable = TRUE,
.statusFlags = DISPLAYCONFIG_TARGET_IN_USE,
},
.flags = DISPLAYCONFIG_PATH_ACTIVE,
};
modes[src_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
.id = uid,
.adapterId = adapter_id,
.sourceMode = {
.width = static_cast<UINT32>(m.width),
.height = static_cast<UINT32>(m.height),
.pixelFormat = DISPLAYCONFIG_PIXELFORMAT_32BPP,
.position = { .x = m.offset_x, .y = m.offset_y },
},
};
modes[tgt_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_TARGET,
.id = uid,
.adapterId = adapter_id,
.targetMode = {},
};
log_misc(
"gitadora",
"inserted fake monitor: id={}, width={}, height={}, offset_x={}, offset_y={}",
m.id, m.width, m.height, m.offset_x, m.offset_y);
}
static
LONG
WINAPI
QueryDisplayConfig_hook(
UINT32 flags,
UINT32* numPathArrayElements,
DISPLAYCONFIG_PATH_INFO* pathArray,
UINT32* numModeInfoArrayElements,
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
{
// copy cached primary real monitor into caller buffers at index 0
pathArray[0] = real_primary_path;
modeInfoArray[0] = real_primary_modes[0];
modeInfoArray[1] = real_primary_modes[1];
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
if (currentTopologyId != nullptr) {
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
}
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
// append fake monitors after the real one
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
insert_fake_monitor(pathArray, modeInfoArray, i);
}
return ERROR_SUCCESS;
}
static
LONG
WINAPI
DisplayConfigGetDeviceInfo_hook(DISPLAYCONFIG_DEVICE_INFO_HEADER* requestPacket)
{
if (requestPacket == nullptr) {
return DisplayConfigGetDeviceInfo_orig(requestPacket);
}
// handle fake monitors (negative id) directly without calling orig
const auto id = static_cast<int>(requestPacket->id);
if (id < 0) {
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto sourceName = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// name must match WrappedIDirect3D9::GetAdapterIdentifier
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", -id);
wcscpy(sourceName->viewGdiDeviceName, s2ws(adapter_name).c_str());
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake source id={} name={}", id, adapter_name);
return ERROR_SUCCESS;
}
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const LONG fake_id = -id;
UINT32 conn_inst = 0xff;
for (const auto& f : FAKE_MONITORS) {
if (f.id == fake_id) {
conn_inst = f.connector_instance;
break;
}
}
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
targetName->connectorInstance = conn_inst;
wcscpy(targetName->monitorFriendlyDeviceName, L"Spice Fake Monitor");
wcscpy(targetName->monitorDevicePath, L"\\\\?\\SpiceFakeMonitor");
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake target id={} -> DP connInst {}",
id, targetName->connectorInstance);
return ERROR_SUCCESS;
}
}
const auto ret = DisplayConfigGetDeviceInfo_orig(requestPacket);
if (ret != ERROR_SUCCESS ||
requestPacket->type != DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
return ret;
}
// override the cached primary real monitor target info to look like HDMI/0
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const auto& target = real_primary_path.targetInfo;
if (target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
{
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
targetName->connectorInstance = 0;
log_info("gitadora",
"overriding primary monitor (id={}) to pretend to be HDMI",
targetName->header.id);
}
return ret;
}
#endif
void GitaDoraGame::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
HMODULE sharepj_module = libutils::try_module("libshare-pj.dll");
HMODULE bmsd2_module = libutils::try_module("libbmsd2.dll");
@@ -269,6 +595,12 @@ namespace games::gitadora {
#ifdef SPICE64
// gitadora arena model
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) {
SETUPAPI_SETTINGS settings{};
settings.class_guid[0] = 0x86E0D1E0;
@@ -294,39 +626,74 @@ namespace games::gitadora {
detour::iat_try("GetDriveTypeA", GetDriveTypeA_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
hooks::audio::mme::init(avs::game::DLL_INSTANCE);
// touch hook
if (ARENA_SINGLE_WINDOW) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", 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;
}
return;
}
HMODULE gdme_module = libutils::try_module("libgdme.dll");
// monitor/touch hooks (windowed or full screen)
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay
nativetouch::hook(avs::game::DLL_INSTANCE);
#if !SPICE_XP
if (!GRAPHICS_WINDOWED) {
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
QueryDisplayConfig_orig =
detour::iat_try("QueryDisplayConfig",
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
DisplayConfigGetDeviceInfo_orig =
detour::iat_try("DisplayConfigGetDeviceInfo",
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
}
#endif
}
}
// window patch
if (GRAPHICS_WINDOWED && !replace_pattern(
gdme_module,
"754185ED753D8B4118BF0000CB02",
"9090????9090??????????????12", 0, 0))
{
log_warning("gitadora", "windowed mode failed");
if (!is_arena_model()) {
HMODULE gdme_module = libutils::try_module("libgdme.dll");
if (GRAPHICS_WINDOWED && !replace_pattern(
gdme_module,
"754185ED753D8B4118BF0000CB02",
"9090????9090??????????????12", 0, 0)) {
log_warning("gitadora", "windowed mode failed");
}
}
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
// two channel mod
if (TWOCHANNEL) {
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
if (TWOCHANNEL && !is_arena_model()) {
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
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)))
{
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
log_warning("gitadora", "two channel mode failed");
}
}
@@ -334,4 +701,18 @@ namespace games::gitadora {
#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 -1
View File
@@ -2,6 +2,9 @@
#include <optional>
#include <windows.h>
#include <mmreg.h>
#include "avs/game.h"
#include "games/game.h"
#include "util/socd_cleaner.h"
@@ -11,11 +14,13 @@ namespace games::gitadora {
// settings
extern bool TWOCHANNEL;
extern std::optional<unsigned int> CAB_TYPE;
extern bool ARENA_SINGLE_WINDOW;
extern bool P1_LEFTY;
extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
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 {
public:
@@ -24,6 +29,8 @@ namespace games::gitadora {
virtual void attach() override;
};
void fix_audio_channel_mask(WAVEFORMATEX *format);
static inline bool is_drum() {
return (
avs::game::is_model({ "J32", "K32", "L32" }) ||
+12 -10
View File
@@ -86,19 +86,21 @@ std::vector<Button> &games::gitadora::get_buttons() {
std::vector<Analog> &games::gitadora::get_analogs() {
static std::vector<Analog> analogs;
using namespace GameAPI::Analogs;
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("GitaDora");
GameAPI::Analogs::sortAnalogs(&analogs,
"Guitar P1 Wail X",
"Guitar P1 Wail Y",
"Guitar P1 Wail Z",
"Guitar P1 Knob",
"Guitar P2 Wail X",
"Guitar P2 Wail Y",
"Guitar P2 Wail Z",
"Guitar P2 Knob"
);
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{"Guitar P1 Wail X", AnalogType::LinearCentered},
{"Guitar P1 Wail Y", AnalogType::LinearCentered},
{"Guitar P1 Wail Z", AnalogType::LinearCentered},
{"Guitar P1 Knob", AnalogType::Circular},
{"Guitar P2 Wail X", AnalogType::LinearCentered},
{"Guitar P2 Wail Y", AnalogType::LinearCentered},
{"Guitar P2 Wail Z", AnalogType::LinearCentered},
{"Guitar P2 Knob", AnalogType::Circular}
});
}
return analogs;
+3 -1
View File
@@ -2,6 +2,7 @@
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "iidx.h"
@@ -82,7 +83,8 @@ bool games::iidx::IIDXFMSerialHandle::FMSerialDevice::parse_msg(
}
// sleep - otherwise the IO thread will go too hard on the CPU
Sleep(1);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// generate message
auto msg = this->create_msg(msg_in, 0x2E);
+3 -1
View File
@@ -2,6 +2,7 @@
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "iidx.h"
@@ -82,7 +83,8 @@ bool games::iidx::BI2XSerialHandle::BI2XDevice::parse_msg(
}
// sleep - otherwise the IO thread will go too hard on the CPU
Sleep(1);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// generate message
auto msg = this->create_msg(msg_in, 0x2E);
+10 -48
View File
@@ -20,8 +20,7 @@
#include "hooks/sleephook.h"
#include "launcher/options.h"
#include "touch/touch.h"
#include "misc/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
#include "misc/eamuse.h"
#include "util/detour.h"
#include "util/deferlog.h"
@@ -63,10 +62,11 @@ namespace games::iidx {
bool TDJ_MODE = false;
bool FORCE_720P = false;
bool DISABLE_ESPEC_IO = false;
bool NATIVE_TOUCH = false;
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
uint32_t TT_DELAY_P1 = 0;
uint32_t TT_DELAY_P2 = 0;
uint8_t DIGITAL_TT_SENS = 4;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE = std::nullopt;
std::optional<std::string> SCREEN_MODE = std::nullopt;
@@ -348,13 +348,7 @@ namespace games::iidx {
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
devicehook_init(avs::core::DLL_INSTANCE);
if (NATIVE_TOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE);
} else {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
}
nativetouch::hook(avs::game::DLL_INSTANCE);
}
// insert BI2X hooks
@@ -541,11 +535,7 @@ namespace games::iidx {
// 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
if (SOUND_OUTPUT_DEVICE.has_value() && SOUND_OUTPUT_DEVICE.value() != "auto") {
log_info(
"iidx",
"using user-supplied \"{}\" for SOUND_OUTPUT_DEVICE",
SOUND_OUTPUT_DEVICE.value());
SetEnvironmentVariable("SOUND_OUTPUT_DEVICE", SOUND_OUTPUT_DEVICE.value().c_str());
// the environemnt variable was already set in launcher.cpp
SOUND_OUTPUT_DEVICE_IN_EFFECT = SOUND_OUTPUT_DEVICE;
return;
}
@@ -781,7 +771,7 @@ namespace games::iidx {
}
// return higher 8 bit
return (uint8_t) (ret_value >> 2);
return (uint8_t)(ret_value >> 2);
}
unsigned char get_slider(uint8_t slider) {
@@ -891,38 +881,10 @@ namespace games::iidx {
}
}
// patch iidx32+ for asio compatibility
// only do this if NOT wasapi (as opposed to checking if it's asio)
// the patch is only really needed for (some) non-XONAR devices but since people sometimes disguise
// other devices as a XONAR, don't check for the exact string (common ASIO workaround for INF)
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);
}
}
// note: the iidx32+ ASIO refcount bug (a duplicate AddRef on the ASIO instance with
// no matching Release, which leaks the driver and can hang non-XONAR devices) is now
// handled transparently by the WrappedAsio proxy (see hooks/audio/asio_proxy.cpp),
// so no game-DLL signature patch is needed here anymore
#endif
-1
View File
@@ -28,7 +28,6 @@ namespace games::iidx {
extern bool TDJ_MODE;
extern bool FORCE_720P;
extern bool DISABLE_ESPEC_IO;
extern bool NATIVE_TOUCH;
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
extern std::optional<std::string> ASIO_DRIVER;
extern uint8_t DIGITAL_TT_SENS;
+11 -10
View File
@@ -63,16 +63,17 @@ std::vector<Analog> &games::iidx::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Turntable P1",
"Turntable P2",
"VEFX",
"Low-EQ",
"Hi-EQ",
"Filter",
"Play Volume"
);
using namespace GameAPI::Analogs;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Turntable P1", AnalogType::Circular },
{ "Turntable P2", AnalogType::Circular },
{ "VEFX", AnalogType::LinearPositive },
{ "Low-EQ", AnalogType::LinearPositive },
{ "Hi-EQ", AnalogType::LinearPositive },
{ "Filter", AnalogType::LinearPositive },
{ "Play Volume", AnalogType::LinearPositive }
});
}
return analogs;
}
+3 -1
View File
@@ -3,6 +3,7 @@
#if SPICE64 && !SPICE_XP
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "mf_wrappers.h"
@@ -399,6 +400,7 @@ namespace games::iidx {
void IIDXLocalCamera::CreateThread() {
// Create thread
m_drawThread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
double accumulator = 0.0;
@@ -412,7 +414,7 @@ namespace games::iidx {
accumulator -= 1.0;
floorFrameTimeMicroSec += 1;
}
std::this_thread::sleep_for(std::chrono::microseconds(floorFrameTimeMicroSec));
timer.sleep(std::chrono::microseconds(floorFrameTimeMicroSec));
}
});
}
+22 -145
View File
@@ -3,28 +3,14 @@
#include <thread>
#include "windows.h"
#include "cfg/screen_resize.h"
#include "games/io.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/graphics.h"
#include "launcher/shutdown.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "rawinput/rawinput.h"
#include "touch/native/inject.h"
#include "touch/touch.h"
#include "util/libutils.h"
#include "util/logging.h"
#define POKE_NATIVE_TOUCH 0
#if POKE_NATIVE_TOUCH
static HINSTANCE USER32_INSTANCE = nullptr;
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
static InjectTouchInput_t pInjectTouchInput = nullptr;
#endif
#include "util/precise_timer.h"
namespace games::iidx::poke {
@@ -115,82 +101,15 @@ namespace games::iidx::poke {
{"1/D", 50},
};
#if POKE_NATIVE_TOUCH
void initialize_native_touch_library() {
if (USER32_INSTANCE == nullptr) {
USER32_INSTANCE = libutils::load_library("user32.dll");
}
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
USER32_INSTANCE, "InitializeTouchInjection");
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
USER32_INSTANCE, "InjectTouchInput");
}
void emulate_native_touch(TouchPoint tp, bool is_down) {
if (pInjectTouchInput == nullptr) {
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
if (touch_points.empty()) {
return;
}
POINTER_TOUCH_INFO contact;
BOOL bRet = TRUE;
const int contact_offset = 2;
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
contact.pointerInfo.pointerType = PT_TOUCH;
contact.pointerInfo.pointerId = 0;
contact.pointerInfo.ptPixelLocation.x = tp.x;
contact.pointerInfo.ptPixelLocation.y = tp.y;
if (is_down) {
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
} else {
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
}
contact.pointerInfo.dwTime = 0;
contact.pointerInfo.PerformanceCount = 0;
contact.touchFlags = TOUCH_FLAG_NONE;
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
contact.orientation = 0;
contact.pressure = 1024;
contact.rcContact.top = tp.y - contact_offset;
contact.rcContact.bottom = tp.y + contact_offset;
contact.rcContact.left = tp.x - contact_offset;
contact.rcContact.right = tp.x + contact_offset;
bRet = InjectTouchInput(1, &contact);
if (!bRet) {
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
}
const auto &touch = touch_points.front();
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
}
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
int i = 0;
for (auto &touch : *touch_points) {
emulate_native_touch(touch, true);
}
}
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
for (auto &touch : *touch_points) {
emulate_native_touch(touch, false);
}
touch_points->clear();
}
#endif
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
std::vector<DWORD> touch_ids;
for (auto &touch : *touch_points) {
touch_ids.emplace_back(touch.id);
}
touch_remove_points(&touch_ids);
touch_points->clear();
}
void enable() {
// check if already running
@@ -200,6 +119,7 @@ namespace games::iidx::poke {
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
// log
log_info("poke", "enabled");
@@ -208,35 +128,13 @@ namespace games::iidx::poke {
std::vector<TouchPoint> touch_points;
std::vector<uint16_t> last_keypad_state = {0, 0};
#if POKE_NATIVE_TOUCH
bool use_native = games::iidx::NATIVE_TOUCH;
if (use_native) {
initialize_native_touch_library();
if (pInitializeTouchInjection != nullptr) {
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
}
}
#else
bool use_native = false;
#endif
// set variable to false to stop
while (THREAD_RUNNING) {
// clean up touch from last frame
if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) {
clear_native_touch_points(&touch_points);
} else {
clear_touch_points(&touch_points);
}
#else
clear_touch_points(&touch_points);
#endif
inject_native_touch_points(touch_points, false);
touch_points.clear();
}
for (int unit = 0; unit < 2; unit++) {
@@ -261,32 +159,15 @@ namespace games::iidx::poke {
float x = x_iter->second / 1920.0;
float y = y_iter->second / 1080.0;
if (use_native) {
x *= rawinput::TOUCHSCREEN_RANGE_X;
y *= rawinput::TOUCHSCREEN_RANGE_Y;
} else if (GRAPHICS_IIDX_WSUB) {
// Scale to windowed subscreen
x *= GRAPHICS_WSUB_WIDTH;
y *= GRAPHICS_WSUB_HEIGHT;
} else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
// Overlay is not present, scale to main screen
if (GRAPHICS_WINDOWED) {
x *= SPICETOUCH_TOUCH_WIDTH;
y *= SPICETOUCH_TOUCH_HEIGHT;
} else {
x *= ImGui::GetIO().DisplaySize.x;
y *= ImGui::GetIO().DisplaySize.y;
if (GRAPHICS_WINDOWED) {
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
continue;
}
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
} else {
// Overlay subscreen
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
// Scale to window size ratio
if (GRAPHICS_WINDOWED) {
x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
}
x = x_iter->second;
y = y_iter->second;
}
TouchPoint tp {
@@ -307,19 +188,15 @@ namespace games::iidx::poke {
} // for all units
if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) {
emulate_native_touch_points(&touch_points);
} else {
touch_write_points(&touch_points);
}
#else
touch_write_points(&touch_points);
#endif
inject_native_touch_points(touch_points, true);
}
// slow down
Sleep(50);
timer.sleep(50);
}
if (!touch_points.empty()) {
inject_native_touch_points(touch_points, false);
}
return nullptr;
+40 -1
View File
@@ -44,6 +44,7 @@ namespace games {
static bool IO_INITIALIZED = false;
static std::vector<std::string> games;
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_modifiers;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
static robin_hood::unordered_map<std::string, std::string> buttons_help;
@@ -92,6 +93,9 @@ namespace games {
games.push_back(ddr);
buttons.insert({ ddr, ddr::get_buttons() });
lights.insert({ ddr, ddr::get_lights() });
buttons_help.insert({ ddr, ddr::get_buttons_help() });
analogs_help.insert({ ddr, ddr::get_analogs_help() });
analogs.insert({ ddr, ddr::get_analogs() });
file_hints[ddr].push_back({"ddr.dll"});
file_hints[ddr].push_back({"mdxja_945.dll"});
file_hints[ddr].push_back({"arkmdxp3.dll"});
@@ -362,6 +366,37 @@ namespace games {
return it->second;
}
static std::vector<Button> gen_buttons_modifiers(const std::string &game) {
auto modifier_buttons = GameAPI::Buttons::getButtons(game);
const std::vector<std::string> names {
"Modifier 1",
"Modifier 2",
"Modifier 3",
"Modifier 4",
};
modifier_buttons = GameAPI::Buttons::sortButtons(modifier_buttons, names);
for (auto &modifier : modifier_buttons) {
modifier.setModifierMask(0);
for (auto &alternative : modifier.getAlternatives()) {
alternative.setModifierMask(0);
}
}
return modifier_buttons;
}
std::vector<Button> *get_buttons_modifiers(const std::string &game) {
initialize();
auto it = buttons_modifiers.find(game);
if (it == buttons_modifiers.end()) {
if (game.empty()) {
return nullptr;
}
buttons_modifiers[game] = gen_buttons_modifiers(game);
return &buttons_modifiers[game];
}
return &it->second;
}
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
auto buttons = GameAPI::Buttons::getButtons(game);
std::vector<std::string> names;
@@ -441,6 +476,8 @@ namespace games {
// overlay button definitions
names.emplace_back("Screenshot");
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");
vkey_defaults.push_back(VK_ESCAPE);
names.emplace_back("Toggle Sub Screen");
@@ -465,10 +502,12 @@ namespace games {
vkey_defaults.push_back(VK_F10);
names.emplace_back("Toggle Screen Resize");
vkey_defaults.push_back(VK_F11);
names.emplace_back("Toggle Overlay");
names.emplace_back("Toggle FPS");
vkey_defaults.push_back(VK_F12);
names.emplace_back("Toggle Camera Control");
vkey_defaults.push_back(0xFF);
names.emplace_back("Toggle OBS Control");
vkey_defaults.push_back(0xFF);
names.emplace_back("Player 1 PIN Macro");
vkey_defaults.push_back(0xFF);
names.emplace_back("Player 2 PIN Macro");
+14 -1
View File
@@ -5,9 +5,20 @@
namespace games {
namespace ModifierButtons {
enum {
Modifier1,
Modifier2,
Modifier3,
Modifier4,
Size,
};
}
namespace OverlayButtons {
enum {
Screenshot,
ToggleAllWindows,
ToggleMainMenu,
ToggleSubScreen,
InsertCoin,
@@ -20,8 +31,9 @@ namespace games {
ToggleControl,
TogglePatchManager,
ToggleScreenResize,
ToggleOverlay,
ToggleFps,
ToggleCameraControl,
ToggleOBSControl,
TriggerPinMacroP1,
TriggerPinMacroP2,
ScreenResize,
@@ -57,6 +69,7 @@ namespace games {
const std::vector<std::string> &get_games();
std::vector<Button> *get_buttons(const std::string &game);
std::vector<Button> *get_buttons_modifiers(const std::string &game);
std::string get_buttons_help(const std::string &game);
std::string get_analogs_help(const std::string &game);
std::vector<Button> *get_buttons_keypads(const std::string &game);
+7 -139
View File
@@ -3,154 +3,24 @@
#include <windows.h>
#include <filesystem>
#include "avs/game.h"
#include "cfg/configurator.h"
#include "hooks/graphics/graphics.h"
#include "touch/touch.h"
#include "util/logging.h"
#include "util/utils.h"
#include "util/detour.h"
#include "util/libutils.h"
#define JB_BUTTON_SIZE 160
#define JB_BUTTON_GAP 37
#define JB_BUTTON_HITBOX (JB_BUTTON_SIZE + JB_BUTTON_GAP)
namespace games::jb {
// touch stuff
bool TOUCH_LEGACY_BOX = false;
static bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true;
static std::vector<TouchPoint> TOUCH_POINTS;
bool TOUCH_STATE[16];
void touch_update() {
// check if touch enabled
if (!TOUCH_ENABLE) {
return;
}
// attach touch module
if (!TOUCH_ATTACHED) {
/*
* Find the game window.
* We check the foreground window first, then fall back to searching for the window title
* All game versions seem to have their model first in the window title
*/
HWND wnd = GetForegroundWindow();
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
wnd = FindWindowBeginsWith(avs::game::MODEL);
}
// check if we have a window handle
if (!wnd) {
log_warning("jubeat", "could not find window handle for touch");
TOUCH_ENABLE = false;
return;
}
// attach touch hook
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
touch_create_wnd(wnd, true);
// show cursor
if (GRAPHICS_SHOW_CURSOR) {
ShowCursor(TRUE);
}
// earlier games use a different screen orientation
if (!avs::game::is_model("L44")) {
IS_PORTRAIT = false;
}
// set attached
TOUCH_ATTACHED = true;
}
// reset touch state
memset(TOUCH_STATE, 0, sizeof(TOUCH_STATE));
// check touch points
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
TOUCH_POINTS.clear();
touch_get_points(TOUCH_POINTS);
if (TOUCH_LEGACY_BOX) {
auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : TOUCH_POINTS) {
// get grid coordinates
int x = tp.x * 4 / 768;
int y = (tp.y - offset) * 4 / (1360 - 580);
// set the corresponding state
int index = y * 4 + x;
if (index >= 0 && index < 16) {
TOUCH_STATE[index] = true;
}
}
} else {
for (auto &tp : TOUCH_POINTS) {
int x_relative = tp.x;
int y_relative = tp.y;
// x_relative and y_relative are relative to the top-left pixel of the first button
if (IS_PORTRAIT) {
// which is at (8, 602) in portrait:
// X: [8...759] (752 pixels wide)
// Y: [602...1353] (752 pixels high)
x_relative -= 8;
y_relative -= 602;
} else {
// and at (8, 8) in landscape
x_relative -= 8;
y_relative -= 8;
}
if (x_relative < 0 || y_relative < 0) {
continue;
}
// x_hitbox and y_hitbox is relative to top-left pixel of each button
int x_index = x_relative / JB_BUTTON_HITBOX;
int x_hitbox = x_relative % JB_BUTTON_HITBOX;
int y_index = y_relative / JB_BUTTON_HITBOX;
int y_hitbox = y_relative % JB_BUTTON_HITBOX;
// check if the gap was touched
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
continue;
}
// set the corresponding state
int index = y_index * 4 + x_index;
if (0 <= index && index < 16) {
TOUCH_STATE[index] = true;
}
}
}
}
/*
* to fix "IP ADDR CHANGE" errors on boot and in-game when using weird network setups such as a VPN
*/
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
static BOOL __stdcall network_addr_is_changed() {
return 0;
}
/*
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
*/
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
static BOOL __stdcall network_get_network_check_info() {
return 0;
}
/*
* to fix network error on non DHCP interfaces
*/
// fixes network errors on non-DHCP interfaces
static BOOL __cdecl network_get_dhcp_result() {
return 1;
}
@@ -167,7 +37,7 @@ namespace games::jb {
void JBGame::pre_attach() {
if (!cfg::CONFIGURATOR_STANDALONE) {
const auto current_path = std::filesystem::current_path();
log_misc("jubeat", "current working directory: {}", current_path.string());
log_misc("jubeat", "current working directory: {}", current_path);
if (current_path.parent_path() == current_path.root_path()) {
log_warning(
"jubeat",
@@ -178,7 +48,7 @@ namespace games::jb {
" c:\\jubeat\\contents\\spice.exe <- OK\n\n"
"To fix this, create a new directory and move ALL game files there.\n\n"
"Your current working directory: {}\n",
current_path.string());
current_path);
log_fatal(
"jubeat",
@@ -190,8 +60,7 @@ namespace games::jb {
void JBGame::attach() {
Game::attach();
// enable touch
TOUCH_ENABLE = true;
touch_attach();
// enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll");
@@ -211,7 +80,6 @@ namespace games::jb {
void JBGame::detach() {
Game::detach();
// disable touch
TOUCH_ENABLE = false;
touch_detach();
}
}
+1 -5
View File
@@ -1,14 +1,10 @@
#pragma once
#include "games/game.h"
#include "games/jb/jb_touch.h"
namespace games::jb {
// touch stuff
extern bool TOUCH_LEGACY_BOX;
extern bool TOUCH_STATE[16];
void touch_update();
class JBGame : public games::Game {
public:
JBGame();
+471
View File
@@ -0,0 +1,471 @@
#include "jb_touch.h"
#include <windows.h>
#include <atomic>
#include <cmath>
#include <limits>
#include <vector>
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "touch/touch.h"
#include "rawinput/touch.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
// touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps
// (752px across). the first button's top-left is at (8, 602) in portrait and
// (6, 8) in landscape.
#define JB_BUTTON_SIZE 160
#define JB_MAX_BUTTON_GAP 38
// improved and plus modes use this reach around each button. must be >= the
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
#define JB_TOUCH_RADIUS 38
namespace games::jb {
static_assert(std::atomic_bool::is_always_lock_free);
static_assert(std::atomic_uint16_t::is_always_lock_free);
static_assert(std::atomic<POINT>::is_always_lock_free);
static constexpr int JB_MAX_GAP_DISTANCE = (JB_MAX_BUTTON_GAP + 1) / 2;
static_assert(JB_TOUCH_RADIUS * JB_TOUCH_RADIUS >=
2 * JB_MAX_GAP_DISTANCE * JB_MAX_GAP_DISTANCE);
// touch state
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto;
uint32_t TOUCH_DEBOUNCE_MS = 0;
static std::atomic_bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true;
static std::atomic_uint16_t TOUCH_STATE = 0;
// fixed-size contact view used by the debug overlay
static const size_t JB_MAX_TOUCH_POINTS = 16;
static constexpr LONG JB_INVALID_TOUCH_COORD = std::numeric_limits<LONG>::max();
static constexpr POINT JB_INVALID_TOUCH_POINT {
JB_INVALID_TOUCH_COORD,
JB_INVALID_TOUCH_COORD
};
static std::atomic<POINT> TOUCH_POINTS[JB_MAX_TOUCH_POINTS] {};
static void clear_touch_points() {
for (auto &point : TOUCH_POINTS) {
point.store(JB_INVALID_TOUCH_POINT, std::memory_order_release);
}
}
// false when a contact is younger than the debounce window and should be ignored
static bool touch_matured(const TouchPoint &tp, double now_ms, double threshold_ms) {
return now_ms - tp.down_ms >= threshold_ms;
}
// snapshot for the debug overlay: each slot atomically holds one matured contact or
// the invalid sentinel. immature and absent contacts are published as invalid, so the
// overlay never shows a suppressed tap and a single atomic per slot cannot tear
static void publish_touch_points(const std::vector<TouchPoint> &touch_points,
double now_ms, double threshold_ms) {
size_t count = touch_points.size();
if (count > JB_MAX_TOUCH_POINTS) {
count = JB_MAX_TOUCH_POINTS;
}
for (size_t i = 0; i < JB_MAX_TOUCH_POINTS; i++) {
POINT point = JB_INVALID_TOUCH_POINT;
if (i < count && touch_matured(touch_points[i], now_ms, threshold_ms)) {
point = { touch_points[i].x, touch_points[i].y };
}
TOUCH_POINTS[i].store(point, std::memory_order_release);
}
}
// --- touch geometry ------------------------------------------------------
// gaps between the four buttons along one axis (the middle gap is 1px wider)
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
struct AxisGeometry {
int button[4]; // left/top edge of each button
};
// left/top edges of the four buttons along one axis, starting at `first`
static AxisGeometry axis_geometry(int first) {
AxisGeometry g {};
g.button[0] = first;
for (int i = 1; i < 4; i++) {
g.button[i] = g.button[i - 1] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1];
}
return g;
}
// button edges for the current orientation
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
if (IS_PORTRAIT) {
gx = axis_geometry(8);
gy = axis_geometry(602);
} else {
gx = axis_geometry(6);
gy = axis_geometry(8);
}
}
// distance from `p` to a button along one axis (0 when inside)
static int axis_distance(int p, int button) {
int end = button + JB_BUTTON_SIZE - 1;
if (p < button) {
return button - p;
}
if (p > end) {
return p - end;
}
return 0;
}
// index (0..15) of the nearest button to (px, py) within `radius`, or -1 if none;
// shared by detection and the debug overlay so both agree which button a touch hits
static int nearest_button(
int px, int py, const AxisGeometry &gx, const AxisGeometry &gy, int radius) {
int best_index = -1;
int best_dist = 0;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c]);
int dy = axis_distance(py, gy.button[r]);
int dist = dx * dx + dy * dy;
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
best_dist = dist;
best_index = r * 4 + c;
}
}
}
return best_index;
}
// detection reach for the current algorithm (0 = register only inside a button)
static int touch_radius() {
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS;
}
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
// every button within `radius` when `multi` (edge/gap presses trigger several)
static void mark_buttons(uint16_t &state, int px, int py,
const AxisGeometry &gx, const AxisGeometry &gy,
int radius, bool multi) {
if (!multi) {
int index = nearest_button(px, py, gx, gy, radius);
if (index >= 0) {
state |= uint16_t(1) << index;
}
return;
}
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c]);
int dy = axis_distance(py, gy.button[r]);
if (dx * dx + dy * dy <= radius * radius) {
state |= uint16_t(1) << (r * 4 + c);
}
}
}
}
std::bitset<16> touch_state() {
return std::bitset<16>(TOUCH_STATE.load(std::memory_order_acquire));
}
void touch_update() {
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
return;
}
// one-time touch window attach
if (!TOUCH_ATTACHED) {
// find the game window: prefer the foreground window, else search by
// title (the model name prefixes the window title in every version)
HWND wnd = GetForegroundWindow();
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
wnd = FindWindowBeginsWith(avs::game::MODEL);
}
if (!wnd) {
log_warning("jubeat", "could not find window handle for touch");
TOUCH_ENABLE.store(false, std::memory_order_release);
TOUCH_STATE.store(0, std::memory_order_release);
return;
}
// only the L44 model runs in portrait; set this before starting the
// touch-window thread so the renderer only observes the final value
IS_PORTRAIT = avs::game::is_model("L44");
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
// let the rawinput stack correct the aspect ratio
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
touch_create_wnd(wnd, true);
if (GRAPHICS_SHOW_CURSOR) {
ShowCursor(TRUE);
}
TOUCH_ATTACHED = true;
}
// calculate the next state locally and publish it after processing every touch
uint16_t next_state = 0;
std::vector<TouchPoint> touch_points;
touch_get_points(touch_points);
// debounce: the landing time is stamped by the touch layer, so this is
// independent of how often the game polls for input (0 = off)
double now_ms = get_performance_milliseconds();
double threshold_ms = TOUCH_DEBOUNCE_MS;
// publish every raw contact (with its maturity) for the debug overlay, then drop
// the ones still inside the debounce window so no algorithm sees them
publish_touch_points(touch_points, now_ms, threshold_ms);
std::erase_if(touch_points, [&](const TouchPoint &tp) {
return !touch_matured(tp, now_ms, threshold_ms);
});
if (TOUCH_ALGORITHM == Legacy) {
// legacy: evenly divide the play area into a 4x4 grid
auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : touch_points) {
int x = tp.x * 4 / 768;
int y = (tp.y - offset) * 4 / (1360 - 580);
int index = y * 4 + x;
if (index >= 0 && index < 16) {
next_state |= uint16_t(1) << index;
}
}
} else {
// accurate registers only a touch inside a button; improved snaps each touch
// to the single nearest button within reach (so a gap or centre touch still
// triggers exactly one button); plus marks every button within the same reach,
// so an edge or gap touch can trigger several at once (like the mobile game)
AxisGeometry gx, gy;
touch_geometry(gx, gy);
int radius = touch_radius();
bool multi = (TOUCH_ALGORITHM == Plus);
for (auto &tp : touch_points) {
mark_buttons(next_state, tp.x, tp.y, gx, gy, radius, multi);
}
}
TOUCH_STATE.store(next_state, std::memory_order_release);
}
// true when any supported touch handler detects a touchscreen (checked once)
static bool touchscreen_detected() {
static const bool detected = is_touch_available("jubeat touch debug overlay");
return detected;
}
// auto draws the boxes only when a touch screen is present
static bool debug_show_boxes() {
switch (TOUCH_DEBUG_OVERLAY) {
case JbTouchDebugBox:
case JbTouchDebugAll:
return true;
case JbTouchDebugAuto:
return touchscreen_detected();
default:
return false;
}
}
static bool debug_show_taps() {
return TOUCH_DEBUG_OVERLAY == JbTouchDebugAll;
}
bool touch_debug_overlay_enabled() {
return TOUCH_ENABLE.load(std::memory_order_acquire) &&
(debug_show_boxes() || debug_show_taps());
}
// the 16 boundary rects: legacy divides the area evenly, others use button squares
static void debug_cells(
bool legacy, const AxisGeometry &gx, const AxisGeometry &gy, RECT cells[16]) {
if (legacy) {
int offset = IS_PORTRAIT ? 580 : 0;
int x_edges[5];
int y_edges[5];
for (int i = 0; i <= 4; i++) {
x_edges[i] = i * 768 / 4;
y_edges[i] = offset + i * (1360 - 580) / 4;
}
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
cells[r * 4 + c] = { x_edges[c], y_edges[r], x_edges[c + 1], y_edges[r + 1] };
}
}
} else {
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
cells[r * 4 + c] = {
gx.button[c], gy.button[r],
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE
};
}
}
}
}
// hollow box outlines: grey idle, thick green when pressed (PS_INSIDEFRAME stays inside)
static void draw_debug_boxes(
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
RECT cells[16];
debug_cells(legacy, gx, gy, cells);
HPEN pen_idle = CreatePen(PS_SOLID, 1, RGB(160, 160, 160));
HPEN pen_active = CreatePen(PS_INSIDEFRAME, 4, RGB(0, 200, 0));
auto state = touch_state();
for (int i = 0; i < 16; i++) {
HGDIOBJ old_pen = SelectObject(hdc, state[i] ? pen_active : pen_idle);
Rectangle(hdc, cells[i].left, cells[i].top, cells[i].right, cells[i].bottom);
SelectObject(hdc, old_pen);
}
DeleteObject(pen_idle);
DeleteObject(pen_active);
}
// whether a touch at (px, py) presses at least one button: legacy has no gaps, plus
// reaches across gaps to nearby buttons, and the others only fire inside a button square
static bool touch_presses_button(int px, int py, const AxisGeometry &gx, const AxisGeometry &gy,
bool legacy, int radius) {
if (legacy) {
return true;
}
int reach = (TOUCH_ALGORITHM == Plus) ? radius : 0;
return nearest_button(px, py, gx, gy, reach) >= 0;
}
// 90-degree arc on the circle rim facing the centre of button `index`
static void draw_tap_arc(HDC hdc, int px, int py, int index,
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
int c = index % 4;
int r = index / 4;
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
const double quarter = 3.14159265358979323846 / 2.0;
const int segments = 16;
POINT arc[segments + 1];
for (int i = 0; i <= segments; i++) {
double a = mid - quarter / 2.0 + quarter * i / segments;
arc[i].x = px + static_cast<LONG>(std::lround(arc_radius * std::cos(a)));
arc[i].y = py + static_cast<LONG>(std::lround(arc_radius * std::sin(a)));
}
Polyline(hdc, arc, segments + 1);
}
// circle at each touch: white when it presses a button, grey otherwise; in single-button
// modes a gap touch also gets a white arc facing the button it snapped to (skipped in
// plus, where a touch can trigger several buttons at once)
static void draw_debug_taps(
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
// PS_INSIDEFRAME keeps the stroke inside the circle radius
const int stroke = 4;
HPEN pen_white = CreatePen(PS_INSIDEFRAME, stroke, RGB(255, 255, 255));
HPEN pen_gray = CreatePen(PS_INSIDEFRAME, stroke, RGB(128, 128, 128));
HGDIOBJ old_pen = SelectObject(hdc, pen_white);
// accurate has no reach, so fall back to a visible marker size when drawing the circle
int radius = touch_radius();
int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS;
// each slot holds a matured contact or the invalid sentinel; immature and absent
// contacts were already filtered out by publish_touch_points
for (auto &touch_point : TOUCH_POINTS) {
POINT point = touch_point.load(std::memory_order_acquire);
if (point.x == JB_INVALID_TOUCH_COORD) {
continue;
}
bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius);
SelectObject(hdc, pressed ? pen_white : pen_gray);
Ellipse(hdc, point.x - draw_radius, point.y - draw_radius,
point.x + draw_radius, point.y + draw_radius);
// the arc points at a single snapped-to button; plus can trigger several at
// once, so skip it there
if (pressed || TOUCH_ALGORITHM == Plus) {
continue;
}
int index = nearest_button(point.x, point.y, gx, gy, radius);
if (index >= 0) {
SelectObject(hdc, pen_white);
draw_tap_arc(hdc, point.x, point.y, index, gx, gy, draw_radius - stroke / 2);
}
}
SelectObject(hdc, old_pen);
DeleteObject(pen_white);
DeleteObject(pen_gray);
}
void touch_draw_debug_overlay(HDC hdc) {
// only draw while touch is active
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
return;
}
bool show_boxes = debug_show_boxes();
bool show_taps = debug_show_taps();
if (!show_boxes && !show_taps) {
return;
}
// legacy divides the field evenly; the other algorithms use button squares
bool legacy = (TOUCH_ALGORITHM == Legacy);
AxisGeometry gx {}, gy {};
if (!legacy) {
touch_geometry(gx, gy);
}
HGDIOBJ old_brush = SelectObject(hdc, GetStockObject(NULL_BRUSH));
if (show_boxes) {
draw_debug_boxes(hdc, legacy, gx, gy);
}
if (show_taps) {
draw_debug_taps(hdc, legacy, gx, gy);
}
SelectObject(hdc, old_brush);
}
void touch_attach() {
clear_touch_points();
TOUCH_ENABLE.store(true, std::memory_order_release);
switch (TOUCH_ALGORITHM) {
case Legacy:
log_info("jubeat", "using 'legacy' touch targets");
break;
case Improved:
log_info("jubeat", "using 'improved' touch targets");
break;
case Plus:
log_info("jubeat", "using 'plus' touch targets");
break;
case AcAccurate:
log_info("jubeat", "using 'ac accurate' touch targets");
break;
default:
log_fatal("jubeat", "unknown touch algo, this is a bug");
break;
}
}
void touch_detach() {
TOUCH_ENABLE.store(false, std::memory_order_release);
TOUCH_STATE.store(0, std::memory_order_release);
}
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <bitset>
#include <cstdint>
#include <windows.h>
namespace games::jb {
// touch detection algorithm
enum JubeatTouchAlgorithm {
Legacy,
Improved,
Plus,
AcAccurate
};
// debug touch overlay mode
enum JubeatTouchDebugMode {
JbTouchDebugAuto, // boundary boxes when a touch screen is detected, else nothing
JbTouchDebugNone, // draw nothing
JbTouchDebugBox, // boundary boxes
JbTouchDebugAll, // boundary boxes and touch circles
};
// selected algorithm and debug mode (set from the launcher options)
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY;
// minimum contact age in milliseconds before a touch registers (0 = off);
// filters momentary phantom touches from noisy panels
extern uint32_t TOUCH_DEBOUNCE_MS;
// atomically published panel state, read by the I/O layer
std::bitset<16> touch_state();
// read the current touch points and refresh the panel state
void touch_update();
// enable/disable touch handling on game attach/detach
void touch_attach();
void touch_detach();
// whether the debug overlay should currently draw anything
bool touch_debug_overlay_enabled();
// draw the debug touch overlay (boundary boxes and/or touch circles per the
// selected mode) onto the spice touch overlay window's device context
void touch_draw_debug_overlay(HDC hdc);
}
-1
View File
@@ -6,7 +6,6 @@
#include "util/utils.h"
#include "util/execexe.h"
#include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h"
#include "bi2a_hook.h"
+5 -5
View File
@@ -36,11 +36,11 @@ std::vector<Analog> &games::mga::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Metal Gear");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Joy X",
"Joy Y"
);
using namespace GameAPI::Analogs;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Joy X", AnalogType::LinearCentered },
{ "Joy Y", AnalogType::LinearCentered }
});
}
return analogs;
+32 -31
View File
@@ -165,37 +165,38 @@ std::vector<Analog> &games::nost::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Nostalgia");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Key 1",
"Key 2",
"Key 3",
"Key 4",
"Key 5",
"Key 6",
"Key 7",
"Key 8",
"Key 9",
"Key 10",
"Key 11",
"Key 12",
"Key 13",
"Key 14",
"Key 15",
"Key 16",
"Key 17",
"Key 18",
"Key 19",
"Key 20",
"Key 21",
"Key 22",
"Key 23",
"Key 24",
"Key 25",
"Key 26",
"Key 27",
"Key 28"
);
using namespace GameAPI::Analogs;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Key 1", AnalogType::LinearPositive },
{ "Key 2", AnalogType::LinearPositive },
{ "Key 3", AnalogType::LinearPositive },
{ "Key 4", AnalogType::LinearPositive },
{ "Key 5", AnalogType::LinearPositive },
{ "Key 6", AnalogType::LinearPositive },
{ "Key 7", AnalogType::LinearPositive },
{ "Key 8", AnalogType::LinearPositive },
{ "Key 9", AnalogType::LinearPositive },
{ "Key 10", AnalogType::LinearPositive },
{ "Key 11", AnalogType::LinearPositive },
{ "Key 12", AnalogType::LinearPositive },
{ "Key 13", AnalogType::LinearPositive },
{ "Key 14", AnalogType::LinearPositive },
{ "Key 15", AnalogType::LinearPositive },
{ "Key 16", AnalogType::LinearPositive },
{ "Key 17", AnalogType::LinearPositive },
{ "Key 18", AnalogType::LinearPositive },
{ "Key 19", AnalogType::LinearPositive },
{ "Key 20", AnalogType::LinearPositive },
{ "Key 21", AnalogType::LinearPositive },
{ "Key 22", AnalogType::LinearPositive },
{ "Key 23", AnalogType::LinearPositive },
{ "Key 24", AnalogType::LinearPositive },
{ "Key 25", AnalogType::LinearPositive },
{ "Key 26", AnalogType::LinearPositive },
{ "Key 27", AnalogType::LinearPositive },
{ "Key 28", AnalogType::LinearPositive }
});
}
return analogs;
}
+10 -8
View File
@@ -1,16 +1,14 @@
// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include "nost.h"
#include "poke.h"
#include "touch_mode.h"
#include "hooks/setupapihook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
#include "avs/game.h"
namespace games::nost {
bool ENABLE_POKE = false;
bool ENABLE_TOUCH_MODE = false;
NostGame::NostGame() : Game("Nostalgia") {
}
@@ -43,9 +41,10 @@ namespace games::nost {
setupapihook_init(avs::game::DLL_INSTANCE);
setupapihook_add(touch_settings);
// custom touch
// nostalgia crashes if you inject touch events too early
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
nativetouch::hook(avs::game::DLL_INSTANCE);
if (ENABLE_TOUCH_MODE) {
touch_mode::enable();
}
}
void NostGame::post_attach() {
@@ -55,6 +54,9 @@ namespace games::nost {
}
void NostGame::detach() {
if (ENABLE_TOUCH_MODE) {
touch_mode::disable();
}
if (ENABLE_POKE) {
poke::disable();
}
+1
View File
@@ -5,6 +5,7 @@
namespace games::nost {
extern bool ENABLE_POKE;
extern bool ENABLE_TOUCH_MODE;
class NostGame : public games::Game {
public:
+91 -77
View File
@@ -1,21 +1,24 @@
#include "poke.h"
#include <thread>
#include <atomic>
#include <optional>
#include <thread>
#include "games/nost/io.h"
#include "cfg/api.h"
#include "rawinput/rawinput.h"
#include "misc/eamuse.h"
#include "touch/touch.h"
#include "touch/native/inject.h"
#include "util/logging.h"
#include "util/precise_timer.h"
namespace games::nost::poke {
static std::thread *THREAD = nullptr;
static volatile bool THREAD_RUNNING = false;
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
static std::atomic<bool> THREAD_RUNNING { false };
// positions use game-client pixels, matching the legacy client-sized touch window
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
{EAM_IO_KEYPAD_0, {760, 440}},
{EAM_IO_KEYPAD_1, {760, 385}},
{EAM_IO_KEYPAD_2, {820, 385}},
@@ -30,7 +33,8 @@ namespace games::nost::poke {
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
};
static const std::unordered_map<games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE {
static const std::unordered_map<
games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE_BUTTONS {
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
{games::nost::Buttons::Button::PokeBack, {340, 100}},
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
@@ -45,13 +49,8 @@ namespace games::nost::poke {
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
};
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
std::vector<DWORD> touch_ids;
for (auto &touch : *touch_points) {
touch_ids.emplace_back(touch.id);
}
touch_remove_points(&touch_ids);
touch_points->clear();
static bool inject_poke_position(POINT position, bool down) {
return nativetouch::inject::inject_synthetic_touch(position, down);
}
void enable() {
@@ -63,122 +62,137 @@ namespace games::nost::poke {
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
timeutils::PreciseSleepTimer timer;
const int swipe_anim_total_frames = 6;
const int swipe_anim_y = 300;
const int swipe_next_page_x_begin = 1120;
const int swipe_next_page_x_end = 1120-120;
const int swipe_next_page_x_end = 1120 - 120;
const int swipe_prev_page_x_begin = 280-120;
const int swipe_prev_page_x_begin = 280 - 120;
const int swipe_prev_page_x_end = 280;
int next_page_anim_index = -1;
int prev_page_anim_index = -1;
std::vector<TouchPoint> touch_points;
bool touch_release = false;
bool contact_active = false;
bool release_pending = false;
POINT last_position {};
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
uint16_t last_keypad_state = 0;
// log
log_info("poke", "enabled");
// set variable to false to stop
while (THREAD_RUNNING) {
// clean up touch from last frame
if (!touch_points.empty()) {
if (touch_release) {
clear_touch_points(&touch_points);
touch_release = false;
} else {
touch_points.clear();
if (release_pending) {
if (contact_active) {
inject_poke_position(last_position, false);
}
contact_active = false;
release_pending = false;
}
auto &buttons = games::nost::get_buttons();
std::optional<POINT> touch_position;
bool release_after_touch = false;
const auto button_triggered = [&](games::nost::Buttons::Button button) {
const auto pressed =
GameAPI::Buttons::getState(RI_MGR, buttons.at(button));
const auto triggered = pressed && !button_states[button];
button_states[button] = pressed;
return triggered;
};
std::optional<POINT> triggered_touch_position;
for (const auto& it : NOST_POKE_BUTTONS) {
if (button_triggered(it.first) && !triggered_touch_position.has_value()) {
triggered_touch_position = POINT {
it.second.first,
it.second.second,
};
}
}
const auto next_triggered =
button_triggered(games::nost::Buttons::PokeNextPage);
const auto prev_triggered =
button_triggered(games::nost::Buttons::PokePrevPage);
const auto keypad_state = eamuse_get_keypad_state(0);
if (!triggered_touch_position.has_value()) {
for (const auto& it : NOST_POKE_KEYPAD) {
const auto mask = static_cast<uint16_t>(1 << it.first);
if ((keypad_state & mask) && !(last_keypad_state & mask)) {
triggered_touch_position = POINT {
it.second.first,
it.second.second,
};
break;
}
}
}
last_keypad_state = keypad_state;
if (0 <= next_page_anim_index) {
const auto delta =
(swipe_next_page_x_end - swipe_next_page_x_begin)
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
TouchPoint tp {
.id = touch_id,
.x = (LONG)swipe_next_page_x_begin + delta,
.y = (LONG)swipe_anim_y,
.mouse = true,
touch_position = POINT {
swipe_next_page_x_begin + delta,
swipe_anim_y,
};
touch_points.emplace_back(tp);
next_page_anim_index--;
if (next_page_anim_index < 0) {
touch_release = true;
release_after_touch = true;
}
} else if (0 <= prev_page_anim_index) {
const auto delta =
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
TouchPoint tp {
.id = touch_id,
.x = (LONG)swipe_prev_page_x_begin + delta,
.y = (LONG)swipe_anim_y,
.mouse = true,
touch_position = POINT {
swipe_prev_page_x_begin + delta,
swipe_anim_y,
};
touch_points.emplace_back(tp);
prev_page_anim_index--;
if (prev_page_anim_index < 0) {
touch_release = true;
release_after_touch = true;
}
} else {
for (const auto& it : NOST_POKE) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) {
TouchPoint tp {
.id = touch_id,
.x = it.second.first,
.y = it.second.second,
.mouse = true,
};
touch_points.emplace_back(tp);
touch_release = true;
break;
}
}
if (!touch_release) {
const auto state = eamuse_get_keypad_state(0);
if (state) {
for (const auto& it : NOST_POKE_NUM) {
if (state & (1 << it.first)) {
TouchPoint tp {
.id = touch_id,
.x = it.second.first,
.y = it.second.second,
.mouse = true,
};
touch_points.emplace_back(tp);
touch_release = true;
break;
}
}
}
}
touch_position = triggered_touch_position;
release_after_touch = touch_position.has_value();
// start animations for next frame
if (!touch_release) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
if (!touch_position.has_value()) {
if (next_triggered) {
next_page_anim_index = swipe_anim_total_frames;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) {
if (prev_triggered) {
prev_page_anim_index = swipe_anim_total_frames;
}
}
}
if (!touch_points.empty()) {
touch_write_points(&touch_points);
if (touch_position.has_value() &&
inject_poke_position(*touch_position, true)) {
contact_active = true;
last_position = *touch_position;
}
if (release_after_touch && contact_active) {
release_pending = true;
}
// slow down
Sleep(30);
timer.sleep(30);
}
if (contact_active) {
inject_poke_position(last_position, false);
}
return nullptr;
+233
View File
@@ -0,0 +1,233 @@
#include "touch_mode.h"
#include <atomic>
#include <mutex>
#include <unordered_map>
#include "touch/native/nativetouchhook.h"
#include "util/logging.h"
namespace games::nost::touch_mode {
// native contact positions feed piano input directly. native events reach the game
// in nav mode and are suppressed in piano mode. mode-button contacts are always
// suppressed and change that routing after release.
static constexpr LONG PIANO_LEFT_GAP = 11;
static constexpr LONG PIANO_RIGHT_GAP = 10;
static constexpr uint32_t PIANO_KEY_COUNT = 28;
struct TouchGeometry {
HWND window = nullptr;
RECT mode_button {};
LONG client_width = 0;
LONG client_height = 0;
bool valid() const {
return window != nullptr && client_width > 0 && client_height > 0;
}
};
struct NativeContact {
POINT position {};
// position contains game-client coordinates
bool client_position_valid = false;
// down began on the mode switch button; remains true through up
bool mode_button = false;
};
static std::atomic_bool accept_events { false };
static std::atomic<Mode> current_mode_state { Mode::Nav };
static std::mutex state_mutex;
static TouchGeometry touch_geometry;
// native contacts are kept by ID so each contact contributes exactly one position
static std::unordered_map<DWORD, NativeContact> active_contacts;
// a hardware button release requests one change after all contacts are released
static bool mode_change_pending = false;
static void reset_state_locked() {
current_mode_state.store(Mode::Nav, std::memory_order_release);
touch_geometry = {};
active_contacts.clear();
mode_change_pending = false;
}
// hardware contacts arrive in screen coordinates
static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) {
if (!touch_geometry.valid()) {
return false;
}
POINT position { event.x, event.y };
if (!ScreenToClient(touch_geometry.window, &position)) {
return false;
}
return PtInRect(&touch_geometry.mode_button, position) != FALSE;
}
static bool update_touch_state(const nativetouch::NativeTouchEvent &event) {
std::lock_guard<std::mutex> lock(state_mutex);
// first, process down / move events
if (event.down || event.move) {
auto contact = active_contacts.try_emplace(event.id).first;
// keep track of IDs that began as a down on the mode switch button
if (event.down) {
contact->second.mode_button = native_touch_in_button(event);
}
// check for valid position
POINT position { event.x, event.y };
if (touch_geometry.window != nullptr &&
ScreenToClient(touch_geometry.window, &position)) {
contact->second.position = position;
contact->second.client_position_valid = true;
}
}
const auto contact = active_contacts.find(event.id);
const bool mode_button_contact = contact != active_contacts.end() &&
contact->second.mode_button;
// process up events
if (event.up) {
active_contacts.erase(event.id);
// if a contact that began down event on the mode switch button has
// been released, a mode switch is now pending
if (mode_button_contact) {
mode_change_pending = true;
}
// apply the change on the final hardware up. switching earlier would
// split another contact's down and up events across different modes
if (mode_change_pending && active_contacts.empty()) {
mode_change_pending = false;
const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav;
current_mode_state.store(next_mode, std::memory_order_release);
}
}
return mode_button_contact;
}
// install the Nostalgia-specific native touch interception
void enable() {
if (accept_events.exchange(true, std::memory_order_acq_rel)) {
return;
}
{
std::lock_guard<std::mutex> lock(state_mutex);
reset_state_locked();
}
nativetouch::set_input_filter(filter_native_touch);
log_info("nost::touch", "enabled");
}
void disable() {
if (!accept_events.exchange(false, std::memory_order_acq_rel)) {
return;
}
nativetouch::set_input_filter(nullptr);
std::lock_guard<std::mutex> lock(state_mutex);
reset_state_locked();
}
bool enabled() {
return accept_events.load(std::memory_order_acquire);
}
Mode current_mode() {
return current_mode_state.load(std::memory_order_acquire);
}
// publish the rendered overlay button rectangle in game-client pixels
void publish_button_bounds(HWND window, const RECT &client_bounds) {
TouchGeometry next {};
RECT client_rect {};
if (window != nullptr && GetClientRect(window, &client_rect) &&
client_rect.right > 0 && client_rect.bottom > 0) {
next.window = window;
next.mode_button = client_bounds;
next.client_width = client_rect.right;
next.client_height = client_rect.bottom;
}
std::lock_guard<std::mutex> lock(state_mutex);
touch_geometry = next;
}
// return the active 28-key piano bitfield for the PANB input update
uint32_t piano_key_state() {
if (!enabled() || current_mode() != Mode::Piano) {
return 0;
}
std::lock_guard<std::mutex> lock(state_mutex);
if (current_mode() != Mode::Piano || !touch_geometry.valid()) {
return 0;
}
uint32_t state = 0;
for (const auto &contact : active_contacts) {
// invalid position or mode-button contact; ignore these contacts
if (!contact.second.client_position_valid || contact.second.mode_button) {
continue;
}
const auto &position = contact.second.position;
// outside the client area or on the mode button; ignore these contacts
if (position.x < 0 || position.x >= touch_geometry.client_width ||
position.y < 0 || position.y >= touch_geometry.client_height ||
PtInRect(&touch_geometry.mode_button, position)) {
continue;
}
// divide the inset width evenly; touches in either side gap clamp to
// the nearest outer key so the physical screen edges remain playable
const auto piano_width =
touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP;
uint32_t key = 0;
if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) {
key = PIANO_KEY_COUNT - 1;
} else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) {
key = static_cast<uint32_t>(
(position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width);
}
state |= UINT32_C(1) << key;
}
return state;
}
// update native contacts and report whether this event should be hidden from the game
bool filter_native_touch(const nativetouch::NativeTouchEvent &event) {
// synthetic events are outside this hardware-only feature
if (!enabled() || event.synthetic) {
// false leaves the event visible to the game
return false;
}
// snapshot routing before an up event can commit a pending mode switch
const bool piano_mode_before_update = current_mode() == Mode::Piano;
// update the contact lifetime and commit any pending switch when safe
const bool mode_button_contact = update_touch_state(event);
// hide every event in a contact that began on the mode switch button
if (mode_button_contact) {
return true;
}
// piano mode consumes hardware events; nav mode forwards them to the game
return piano_mode_before_update;
}
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <windows.h>
#include "touch/native/nativetouchhook.h"
namespace games::nost::touch_mode {
// nav mode forwards contacts to the game; piano mode converts them into piano keys
enum class Mode {
Nav,
Piano,
};
void enable();
void disable();
bool enabled();
Mode current_mode();
void publish_button_bounds(HWND window, const RECT &client_bounds);
uint32_t piano_key_state();
bool filter_native_touch(const nativetouch::NativeTouchEvent &event);
}

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