mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-09-30 14:04:55 -07:00
Compare commits
277 Commits
26-03-31
...
4009bb8a84
| Author | SHA1 | Date | |
|---|---|---|---|
| 4009bb8a84 | |||
| 8fb6d94000 | |||
| 8d3685415d | |||
| 50b38b856f | |||
| dad88caf01 | |||
| 699659d4bf | |||
| 21e7d24ed3 | |||
| 1cdb7be810 | |||
| 09314f1fd3 | |||
| e7b90271ca | |||
| 95f10ed733 | |||
| d51de976b1 | |||
| 8acd433ec6 | |||
| b9c8afbbc1 | |||
| 7d5f528e1a | |||
| 2dc3c0cbe4 | |||
| 4c2a70bab6 | |||
| 46f76597fc | |||
| b23640222c | |||
| 3f6862908e | |||
| 0934cce225 | |||
| 7c50fcc79e | |||
| 0f4ab63101 | |||
| 8b2f38307b | |||
| 3863d5a4ed | |||
| 94574c485a | |||
| f857926ec3 | |||
| adf4cccd4a | |||
| bf8e194685 | |||
| a2e508208c | |||
| f5888609a8 | |||
| c6cd72c528 | |||
| 82e0c053d0 | |||
| 3b29227dbc | |||
| ce2f93d234 | |||
| 13a171f199 | |||
| 3da352488e | |||
| 2386047c2f | |||
| baa550037a | |||
| b53447bed5 | |||
| 4959a58de3 | |||
| 9141ff453b | |||
| f955ab984c | |||
| 435b9ce9aa | |||
| 1ec7528dac | |||
| e7822aad8e | |||
| 8140e83e46 | |||
| cc0899290e | |||
| 3012139028 | |||
| c590b87cff | |||
| 5cabd026ae | |||
| 7ccd938f9b | |||
| 88a210e82e | |||
| 74e619df37 | |||
| 10a97b9c63 | |||
| 159043803c | |||
| bd2fbfcb67 | |||
| 64600826b7 | |||
| 80deb4bbbd | |||
| 00c8bb8ce6 | |||
| 09ed8b948c | |||
| e2daefbb6a | |||
| 39bf17aafe | |||
| 4836cd94ab | |||
| 9d41ca2515 | |||
| 18421ffbfe | |||
| 3f0921d983 | |||
| ed7318c270 | |||
| 623e1e3998 | |||
| 558e3d0cb3 | |||
| d0db547dd0 | |||
| be4bb00390 | |||
| a8cc85531c | |||
| 2dea0d27bf | |||
| 61a0220c1a | |||
| c080bbe301 | |||
| e1d1b39567 | |||
| 2aae8ad9ca | |||
| 1b6d1af951 | |||
| 5c3617c05f | |||
| a9a943fbfe | |||
| b094a9e352 | |||
| 4bd18afb12 | |||
| 0b24e703a1 | |||
| 1a1be7a393 | |||
| a2d8f60d12 | |||
| 5d25fdb035 | |||
| c255e3a1db | |||
| d7c144646f | |||
| 27077aa21f | |||
| 632dcf7b41 | |||
| a25bbdc9c9 | |||
| 47d886306e | |||
| edda658fa4 | |||
| 1ab20f1b56 | |||
| 0e9093a74b | |||
| f52eaea76c | |||
| c3fcf2f417 | |||
| 8d04eba21d | |||
| 8d42c0ce77 | |||
| 829503568e | |||
| 746fdb5409 | |||
| 67da892fe9 | |||
| dc191487e0 | |||
| 5c0c405800 | |||
| 60166e6ba1 | |||
| cb0b59600a | |||
| b9f3be4df2 | |||
| cf86fbd238 | |||
| 3fdb369128 | |||
| db7defff5a | |||
| 451cbec0b9 | |||
| 5c69e295ab | |||
| 5065a92d55 | |||
| 5483e8e2c0 | |||
| e8949a2612 | |||
| db71a0b24d | |||
| d3d5422768 | |||
| 5185f753e5 | |||
| eb897d0f1b | |||
| 50fc80a1c3 | |||
| 301e15c44d | |||
| b6f886a7c5 | |||
| 0b085286d3 | |||
| 29d3d9bb52 | |||
| bf05a3ef30 | |||
| 6280ac3b0a | |||
| cc9df27569 | |||
| ac51acdafe | |||
| 6fec169347 | |||
| 4af006e869 | |||
| 256ef4d341 | |||
| 698f2ffc2c | |||
| 4879a35eb8 | |||
| fd7e850789 | |||
| 0cf07c38da | |||
| 1f073b2d75 | |||
| 3037214932 | |||
| 18d6d9783a | |||
| 267add3227 | |||
| a170627f52 | |||
| 5c244eaca7 | |||
| 6bcadccf09 | |||
| ccb3bac48c | |||
| 0a7ecf82a9 | |||
| 1c6929dd84 | |||
| 1d3d9040f2 | |||
| 8f1069628e | |||
| f8c585a9d1 | |||
| 9651051990 | |||
| a54a62d0d7 | |||
| 100caa0f5c | |||
| bb87aa2944 | |||
| 2e2968d1bc | |||
| 0457262472 | |||
| f23c359114 | |||
| 81e74dcb97 | |||
| 8ead941c6c | |||
| ece03cabba | |||
| 4b7f68f920 | |||
| 2dae86a6f2 | |||
| 85058c2156 | |||
| ea2f4c5572 | |||
| 48033816a8 | |||
| b517ef3182 | |||
| 6fef16353e | |||
| 84ea3f9e8e | |||
| 6bb6b0a301 | |||
| 465d6c6c18 | |||
| ef384e85dd | |||
| 7ee04879e2 | |||
| 4c73200f58 | |||
| 31aabe9786 | |||
| 4ea55a61b8 | |||
| 0209b80a22 | |||
| 0511dfb6ca | |||
| b558df3340 | |||
| 89602cfcab | |||
| b4557993c9 | |||
| f119d7c988 | |||
| b3d8d0aea9 | |||
| a977cba772 | |||
| 489d40d87d | |||
| 3a468a9a3b | |||
| a2e220d3f4 | |||
| febc02b50b | |||
| 311404f56b | |||
| d929cdf7c8 | |||
| 2b930ad95f | |||
| 3c60f3966b | |||
| c59d399ab8 | |||
| f69e40fa26 | |||
| e9dcc5717e | |||
| 5bec3d5db7 | |||
| 6ec2b47345 | |||
| ebda16beae | |||
| c29a26f183 | |||
| 2014e9cf75 | |||
| 33f2e1d4b7 | |||
| eb037542b4 | |||
| 039b42ad53 | |||
| 33b4718744 | |||
| 71ba9b6b47 | |||
| 1957917270 | |||
| b9ccddf47d | |||
| cd0ba51b5a | |||
| 1f23d88c7a | |||
| 46e43ab09c | |||
| 96950b6b4e | |||
| 5f94ef4478 | |||
| c4193eddc5 | |||
| c8962a0e77 | |||
| b6a749e5db | |||
| 7dbbe9809e | |||
| 79a3e32d5d | |||
| c5a4e954f9 | |||
| 7b862768b0 | |||
| af8d8dae9f | |||
| d9d5823fdb | |||
| 82dda33800 | |||
| 3876e38636 | |||
| 6bc1357d6a | |||
| 37218e7fe0 | |||
| 678e11eade | |||
| f72313fe45 | |||
| 06374ef78a | |||
| bdde1ec6b8 | |||
| d2176fe4f0 | |||
| fb9eef904a | |||
| 48903998a5 | |||
| f2dca0265f | |||
| 1147ed9858 | |||
| 19d93f4ffd | |||
| 15bfffa1d3 | |||
| 18a9a47c33 | |||
| 6838e69ebe | |||
| 26b3b64794 | |||
| aadd2bdd59 | |||
| d0e24e31ca | |||
| 4fb7f20c7b | |||
| 6ddae70c5a | |||
| b38160d6c2 | |||
| 231c998cc0 | |||
| 127d31a85c | |||
| a8419bbaa0 | |||
| 0fc430ceb3 | |||
| e59a50b64c | |||
| 2042b629e9 | |||
| 440b9ed749 | |||
| 9936088286 | |||
| 4d8a6c1952 | |||
| af69cf9251 | |||
| 1d5550b1e8 | |||
| ec3f6639b7 | |||
| 727b99effd | |||
| 8afff3aaca | |||
| b281517429 | |||
| 54ec9f272e | |||
| a159691097 | |||
| ca70f7b19c | |||
| 7b4227d2a9 | |||
| f89efa2558 | |||
| 6f4824b648 | |||
| 45debeb5c9 | |||
| a01f452eba | |||
| e452734bc1 | |||
| d1779b93fa | |||
| fe9aac7e29 | |||
| f3d887ebcf | |||
| 336500c8ae | |||
| ee3fa58bfa | |||
| a4c3eddc2f | |||
| e0f6cb2cbd | |||
| 1ad6a9412e | |||
| 5af3e0d494 | |||
| b1ee828457 | |||
| 0d60649715 |
@@ -0,0 +1,46 @@
|
||||
# Normalize every text file to LF in the repository.
|
||||
# The build runs under Linux/MinGW containers, so LF is also used in the
|
||||
# working tree; Windows editors and toolchains handle LF fine.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Windows-only files that must keep CRLF in the working tree.
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.sln text eol=crlf
|
||||
*.vcproj text eol=crlf
|
||||
*.vcxproj text eol=crlf
|
||||
*.props text eol=crlf
|
||||
*.filters text eol=crlf
|
||||
|
||||
# Files that must keep LF even if a Windows editor rewrites them.
|
||||
*.sh text eol=lf
|
||||
*.in text eol=lf
|
||||
*.cmake text eol=lf
|
||||
*.mk text eol=lf
|
||||
Makefile text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
|
||||
# Binary files - never touch the contents.
|
||||
*.bin binary
|
||||
*.ico binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.bmp binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.gz binary
|
||||
*.dll binary
|
||||
*.exe binary
|
||||
*.lib binary
|
||||
*.a binary
|
||||
*.o binary
|
||||
*.obj binary
|
||||
*.pdb binary
|
||||
|
||||
# Vendored code is stored and checked out byte-for-byte as upstream ships it,
|
||||
# so re-importing a library never produces line-ending-only diffs.
|
||||
src/spice2x/external/** -text
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+8
-3
@@ -11,13 +11,18 @@ 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.
|
||||
* Bundling third party DLL binary; e.g., Discord Social SDK, PC light control integration, and so on. These should be built as hook DLLs instead, optionally with [Spice SDK](https://github.com/spice2x/spice2x.github.io/wiki/Spice-SDK).
|
||||
|
||||
### Avoiding regressions
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ We explicitly do **NOT** have a Discord server for dicussions - we try to do eve
|
||||
|
||||
Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code.
|
||||
|
||||
### AI-Use Policy
|
||||
|
||||
Use AI tools at your discretion, but remember that a human will review every issue and pull request. Please ensure submissions are accurate, relevant, and worth the reviewer’s time.
|
||||
|
||||
## Additional information
|
||||
|
||||
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
|
||||
|
||||
+235
-25
@@ -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")
|
||||
|
||||
@@ -247,8 +254,60 @@ add_subdirectory(external/discord-rpc EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/hash-library EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/wslay EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL)
|
||||
|
||||
# libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own
|
||||
# sysroot and cannot see it, so those targets build without JPEG support.
|
||||
add_library(spice_jpeg INTERFACE)
|
||||
if(NOT SPICE_XP)
|
||||
# search static archives only: the mingw package also ships an import library,
|
||||
# and linking that one would pull in a libjpeg DLL at runtime
|
||||
set(SPICE_JPEG_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX})
|
||||
find_package(JPEG)
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_JPEG_SUFFIXES})
|
||||
|
||||
if(JPEG_FOUND)
|
||||
target_link_libraries(spice_jpeg INTERFACE JPEG::JPEG)
|
||||
target_compile_definitions(spice_jpeg INTERFACE SPICE_JPEG=1)
|
||||
else()
|
||||
message(WARNING
|
||||
"libjpeg-turbo not found: screen capture over the API is disabled")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# x264 for the API H.264 video stream, installed into the mingw sysroots by the
|
||||
# deps image. The WinXP toolchains deliberately go without it and serve MJPEG only.
|
||||
add_library(spice_x264 INTERFACE)
|
||||
if(NOT SPICE_XP)
|
||||
# search static archives only: the mingw package also ships an import library,
|
||||
# and linking that one would pull in a libx264 DLL at runtime
|
||||
set(SPICE_X264_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX})
|
||||
find_library(X264_LIBRARY NAMES x264 libx264)
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_X264_SUFFIXES})
|
||||
|
||||
find_path(X264_INCLUDE_DIR NAMES x264.h)
|
||||
if(X264_LIBRARY AND X264_INCLUDE_DIR)
|
||||
target_include_directories(spice_x264 INTERFACE "${X264_INCLUDE_DIR}")
|
||||
target_link_libraries(spice_x264 INTERFACE "${X264_LIBRARY}")
|
||||
target_compile_definitions(spice_x264 INTERFACE SPICE_H264=1)
|
||||
else()
|
||||
message(WARNING
|
||||
"x264 not found: the api video stream will only offer MJPEG")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# fpng's SIMD needs the whole unit built for SSE4.1, which its runtime CPU check
|
||||
# cannot undo, so keep it scalar rather than raising the CPU baseline
|
||||
set_source_files_properties(external/fpng/fpng.cpp PROPERTIES
|
||||
COMPILE_DEFINITIONS "FPNG_NO_SSE=1")
|
||||
if(NOT MSVC)
|
||||
set_source_files_properties(external/fpng/fpng.cpp PROPERTIES
|
||||
COMPILE_OPTIONS "-fno-strict-aliasing")
|
||||
endif()
|
||||
|
||||
# set link time optimizations (disabled for Debug builds for speed, disabled
|
||||
# for RelWithDebInfo builds due to "lto1: error: two or more sections for"
|
||||
# errors)
|
||||
@@ -310,6 +369,10 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
# api
|
||||
api/controller.cpp
|
||||
api/websocket.cpp
|
||||
api/capture_pump.cpp
|
||||
api/h264_stream.cpp
|
||||
api/stream_format.cpp
|
||||
api/stream_server.cpp
|
||||
api/request.cpp
|
||||
api/response.cpp
|
||||
api/module.cpp
|
||||
@@ -325,6 +388,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
api/modules/control.cpp
|
||||
api/modules/touch.cpp
|
||||
api/modules/iidx.cpp
|
||||
api/modules/sdvx.cpp
|
||||
api/serial.cpp
|
||||
api/modules/drs.cpp
|
||||
api/modules/lcd.cpp
|
||||
@@ -372,7 +436,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
external/tinyxml2/tinyxml2.cpp
|
||||
external/http-parser/http_parser.c
|
||||
external/usbhidusage/usb-hid-usage.c
|
||||
external/toojpeg/toojpeg.cpp
|
||||
external/fpng/fpng.cpp
|
||||
external/scard/scard.cpp
|
||||
|
||||
# games
|
||||
@@ -383,6 +447,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/shared/twtouch.cpp
|
||||
games/popn/popn.cpp
|
||||
games/popn/io.cpp
|
||||
games/popn/bi3a_hook.cpp
|
||||
games/bbc/bbc.cpp
|
||||
games/bbc/io.cpp
|
||||
games/hpm/hpm.cpp
|
||||
@@ -400,14 +465,19 @@ 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/bi2x_hook.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
|
||||
@@ -421,6 +491,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
|
||||
@@ -460,6 +531,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/otoca/p4io.cpp
|
||||
games/silentscope/silentscope.cpp
|
||||
games/silentscope/io.cpp
|
||||
games/silentscope/projector.cpp
|
||||
games/pcm/pcm.cpp
|
||||
games/pcm/io.cpp
|
||||
games/onpara/onpara.cpp
|
||||
@@ -485,16 +557,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
|
||||
@@ -509,19 +589,32 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/devicehook.cpp
|
||||
hooks/graphics/graphics.cpp
|
||||
hooks/graphics/graphics_windowed.cpp
|
||||
hooks/graphics/jpeg_encoder.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_readback.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_gfdm.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
|
||||
@@ -544,7 +637,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
misc/device.cpp
|
||||
misc/eamuse.cpp
|
||||
misc/extdev.cpp
|
||||
misc/nativetouchhook.cpp
|
||||
misc/hotkeys.cpp
|
||||
misc/sciunit.cpp
|
||||
misc/sde.cpp
|
||||
misc/wintouchemu.cpp
|
||||
@@ -555,6 +648,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
|
||||
@@ -581,11 +675,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
|
||||
@@ -596,6 +706,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
rawinput/smxstage.h
|
||||
rawinput/smxdedicab.cpp
|
||||
rawinput/smxdedicab.h
|
||||
rawinput/xinput.cpp
|
||||
rawinput/xinput.h
|
||||
|
||||
# reader
|
||||
reader/reader.cpp
|
||||
@@ -603,12 +715,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
|
||||
|
||||
@@ -629,6 +750,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
|
||||
@@ -640,37 +762,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 wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||
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
|
||||
@@ -680,7 +839,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W
|
||||
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
target_link_libraries(spicetools_spice_linux
|
||||
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)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux")
|
||||
target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
|
||||
@@ -698,11 +857,15 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||
target_link_libraries(spicetools_spice64
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||
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")
|
||||
@@ -717,11 +880,14 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||
target_link_libraries(spicetools_spice64_linux
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||
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")
|
||||
@@ -733,9 +899,10 @@ endif()
|
||||
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})
|
||||
# the configurator serves neither the API nor the video stream, so it needs no codecs
|
||||
target_link_libraries(spicetools_cfg
|
||||
PUBLIC 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)
|
||||
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 wslay imm32 dwmapi CpuFeatures::cpu_features)
|
||||
target_link_libraries(spicetools_cfg PUBLIC winscard)
|
||||
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg")
|
||||
@@ -752,8 +919,8 @@ 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
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
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 wslay imm32 dwmapi CpuFeatures::cpu_features)
|
||||
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
|
||||
target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
|
||||
@@ -856,6 +1023,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
|
||||
####################
|
||||
|
||||
@@ -865,15 +1062,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()
|
||||
|
||||
@@ -127,7 +127,30 @@ doesn't matter since the TCP protocol doesn't allow for out of order data,
|
||||
however this may change when/if support for UDP is being introduced. The only
|
||||
restriction is that the ID has to be a valid 64-bit unsigned integer.
|
||||
|
||||
#### Capture
|
||||
- get_screens()
|
||||
- returns the screen numbers the game has registered for capture
|
||||
- get_jpg(screen: uint, quality: uint, divide: uint)
|
||||
- returns the timestamp, width, height and base64 encoded JPEG of one screen
|
||||
- all parameters are optional and default to screen 0, quality 70, divide 1
|
||||
- divide shrinks the image by that factor before encoding
|
||||
- get_streams()
|
||||
- returns a dict describing the HTTP video stream, or no data at all when
|
||||
`-apistream` is not enabled and there is nothing to describe
|
||||
- `port` is the stream server port
|
||||
- `formats` lists the wire formats this build serves, each with a `name`
|
||||
(`h264` or `mjpeg`) and the `path` to request them on
|
||||
- `screens` lists every capturable screen with its `width`, `height`, and
|
||||
`busy`
|
||||
|
||||
#### Card
|
||||
- get_cards()
|
||||
- returns the current card ID and source for each active card reader
|
||||
- each entry contains `index`, `card_id`, and `source`
|
||||
- `source` is `file` or `override`
|
||||
- file entries also contain `file_name`, without the full configured path
|
||||
- `-card0` and `-card1` overrides do not contain `file_name`
|
||||
- this function only works when an API password is configured
|
||||
- insert(index: uint, card_id: hex)
|
||||
- inserts a card which gets read by the emulated card readers for the game
|
||||
- index has to be either 0 (for P1) or 1 (for P2)
|
||||
@@ -267,6 +290,20 @@ which also means that your hex edits are applicable directly.
|
||||
- `Side Panel Right Inner`
|
||||
- `Side Panel Right`
|
||||
|
||||
#### SDVX
|
||||
- tapeled_get(name: str, ...)
|
||||
- returns a list containing a dict of the current tape LED states. The dict keys are:
|
||||
- `Title`
|
||||
- `Upper Left Speaker`
|
||||
- `Upper Right Speaker`
|
||||
- `Left Wing`
|
||||
- `Right Wing`
|
||||
- `Control Panel`
|
||||
- `Lower Left Speaker`
|
||||
- `Lower Right Speaker`
|
||||
- `Woofer`
|
||||
- `V Unit`
|
||||
|
||||
#### LCD
|
||||
- info()
|
||||
- returns information about the serial LCD controller some games use
|
||||
@@ -277,12 +314,63 @@ which also means that your hex edits are applicable directly.
|
||||
- image_resize_set_scene(scene: int)
|
||||
- sets the active scene for image resize state; set to 0 to disable resize
|
||||
|
||||
## Video Stream
|
||||
|
||||
Separate from the JSON API, spice can serve the mirrored screen as a video
|
||||
stream over plain HTTP. Enable it with `-apistream`. It listens on the API port
|
||||
plus two, in the same way the WebSocket server uses the API port plus one, so
|
||||
`-api 1337` puts the stream on 1339. This means `-api` has to be enabled too.
|
||||
|
||||
Rather than working the port out, clients should ask the JSON API for it with
|
||||
`capture.get_streams()`, which also reports which of the formats below this
|
||||
build serves, the size of each screen and whether one is already taken.
|
||||
|
||||
Two formats are served:
|
||||
|
||||
http://host:1339/stream.mjpg JPEG frames, multipart/x-mixed-replace
|
||||
http://host:1339/stream.h264 H.264 annex-b, no container
|
||||
|
||||
All accept the same optional query parameters:
|
||||
|
||||
- `screen` - which screen to mirror, 0-3. Defaults to the subscreen when the
|
||||
game has one, otherwise the main screen.
|
||||
- `fps` - frames per second, 1-60. Default 30.
|
||||
- `q` - quality, 1-100. Default 70. This is the JPEG quality for `stream.mjpg`
|
||||
and is mapped onto the H.264 rate factor for `stream.h264`, so the same number
|
||||
does not mean the same thing for both.
|
||||
|
||||
For example:
|
||||
|
||||
http://host:1339/stream.h264?screen=1&fps=30&q=70
|
||||
|
||||
See the wiki for format tradeoffs, latency tuning, testing commands and client
|
||||
notes.
|
||||
|
||||
The stream is view only. Touch and other input still go through the JSON API,
|
||||
so a companion app needs both. There is no authentication on the stream port -
|
||||
anyone who can reach it can watch the screen.
|
||||
|
||||
WinXP builds have no video stream. Neither encoder is compiled in, so nothing
|
||||
listens on the stream port even with `-apistream`, `capture.get_streams()`
|
||||
returns no data, and the JSON API's JPEG screen capture is unavailable for the
|
||||
same reason.
|
||||
|
||||
## Native wrapper libraries
|
||||
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "bmpu.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "acio/icca/icca.h"
|
||||
#include "avs/game.h"
|
||||
#include "cfg/api.h"
|
||||
@@ -9,6 +11,7 @@
|
||||
#include "games/ftt/io.h"
|
||||
#include "games/museca/io.h"
|
||||
#include "games/silentscope/io.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "misc/eamuse.h"
|
||||
|
||||
@@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() {
|
||||
STATUS_BUFFER[4] |= 0x20;
|
||||
}
|
||||
|
||||
// joy stick raw input
|
||||
// gun position, sent big endian; the game keeps the top 12 bits of each pair
|
||||
auto &analogs = games::silentscope::get_analogs();
|
||||
auto &gun_x = analogs.at(games::silentscope::Analogs::GUN_X);
|
||||
auto &gun_y = analogs.at(games::silentscope::Analogs::GUN_Y);
|
||||
unsigned short joy_x = 0x7FFF;
|
||||
unsigned short joy_y = 0x7FFF;
|
||||
if (analogs.at(games::silentscope::Analogs::GUN_X).isSet()) {
|
||||
joy_x = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_X)) * USHRT_MAX);
|
||||
}
|
||||
if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) {
|
||||
joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * USHRT_MAX);
|
||||
|
||||
if (gun_x.isSet() || gun_y.isSet()) {
|
||||
if (gun_x.isSet()) {
|
||||
joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX);
|
||||
}
|
||||
if (gun_y.isSet()) {
|
||||
joy_y = (unsigned short) (Analogs::getState(RI_MGR, gun_y) * USHRT_MAX);
|
||||
}
|
||||
} else {
|
||||
|
||||
// the gun reports where it is aimed, which a relative pointer cannot express - the
|
||||
// calibration screen asks for the screen centre and two corners, so read the cursor
|
||||
POINT cursor {};
|
||||
RECT client {};
|
||||
POINT origin {};
|
||||
if (NDD_MAIN_WINDOW != nullptr &&
|
||||
GetCursorPos(&cursor) &&
|
||||
GetClientRect(NDD_MAIN_WINDOW, &client) &&
|
||||
ClientToScreen(NDD_MAIN_WINDOW, &origin) &&
|
||||
client.right > 1 && client.bottom > 1)
|
||||
{
|
||||
const LONG x = std::clamp(cursor.x - origin.x, 0L, client.right - 1);
|
||||
const LONG y = std::clamp(cursor.y - origin.y, 0L, client.bottom - 1);
|
||||
joy_x = (unsigned short) (x * USHRT_MAX / (client.right - 1));
|
||||
joy_y = (unsigned short) (y * USHRT_MAX / (client.bottom - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// invert X axis
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+122
-20
@@ -35,24 +35,33 @@ struct ICCA_STATUS_LA9 {
|
||||
static_assert(sizeof(struct ICCA_STATUS) == 24, "ICCA_STATUS must be 24 bytes");
|
||||
|
||||
enum ICCA_WORKFLOW {
|
||||
STEP,
|
||||
SLEEP,
|
||||
START,
|
||||
INIT,
|
||||
READY,
|
||||
GET_USERID,
|
||||
ACTIVE,
|
||||
EJECT,
|
||||
EJECT_CHECK,
|
||||
END,
|
||||
CLOSE_EJECT,
|
||||
CLOSE_E_CHK,
|
||||
CLOSE_END,
|
||||
ERR_GETUID = -2
|
||||
STEP = 0,
|
||||
SLEEP = 1,
|
||||
START = 2,
|
||||
INIT = 3,
|
||||
READY = 4,
|
||||
GET_USERID = 5,
|
||||
ACTIVE = 6,
|
||||
EJECT = 7,
|
||||
EJECT_CHECK = 8,
|
||||
END = 9,
|
||||
CLOSE_EJECT = 10,
|
||||
CLOSE_E_CHK = 11,
|
||||
CLOSE_END = 12,
|
||||
ERR_GETUID = -2,
|
||||
ERR_REMOVED = -11
|
||||
};
|
||||
|
||||
// used by RA as the status_code
|
||||
enum ICCA_STATUS_CODE {
|
||||
EMPTY = 1,
|
||||
ENGAGED = 2
|
||||
};
|
||||
|
||||
struct ICCA_UNIT {
|
||||
struct ICCA_STATUS status {};
|
||||
enum ICCA_WORKFLOW state = STEP;
|
||||
enum ICCA_WORKFLOW state_jdz = SLEEP;
|
||||
bool card_cmd_pressed = false;
|
||||
bool card_in = false;
|
||||
double card_in_time = 0.0;
|
||||
@@ -75,6 +84,11 @@ static inline int icca_get_active_count() {
|
||||
}
|
||||
|
||||
static inline int icca_get_unit_id(int unit_id) {
|
||||
// on RA the unit_id is zero-indexed
|
||||
if (avs::game::is_model("JDZ")) {
|
||||
return unit_id;
|
||||
}
|
||||
|
||||
if (icca_get_active_count() < 2)
|
||||
return 1;
|
||||
else {
|
||||
@@ -122,9 +136,15 @@ static inline void update_card(int unit_id) {
|
||||
IS_LAST_CARD_FELICA = is_card_uid_felica(unit->status.uid);
|
||||
|
||||
unit->state = acio::ICCA_COMPAT_ACTIVE ? START : ACTIVE;
|
||||
unit->state_jdz = ACTIVE;
|
||||
unit->status.error = 0;
|
||||
unit->status.front_sensor = 1;
|
||||
unit->status.rear_sensor = 1;
|
||||
} else {
|
||||
unit->state = ERR_GETUID;
|
||||
unit->state_jdz = ERR_GETUID;
|
||||
unit->status.front_sensor = 1;
|
||||
unit->status.rear_sensor = 1;
|
||||
memset(unit->status.uid, 0, 8);
|
||||
}
|
||||
unit->card_in = true;
|
||||
@@ -132,10 +152,25 @@ static inline void update_card(int unit_id) {
|
||||
} else if (unit->state == EJECT_CHECK) {
|
||||
unit->state = SLEEP;
|
||||
unit->card_in = false;
|
||||
unit->status.front_sensor = 0;
|
||||
unit->status.rear_sensor = 0;
|
||||
} else if (unit->state_jdz == READY) {
|
||||
unit->state = GET_USERID;
|
||||
unit->state_jdz = GET_USERID;
|
||||
unit->status.error = 0;
|
||||
unit->status.front_sensor = 1;
|
||||
unit->status.rear_sensor = 1;
|
||||
unit->card_in = true;
|
||||
unit->card_in_time = get_performance_seconds();
|
||||
}
|
||||
} else {
|
||||
unit->state = acio::ICCA_COMPAT_ACTIVE ? START : ACTIVE;
|
||||
if (unit->state_jdz == READY) {
|
||||
unit->state_jdz = GET_USERID;
|
||||
}
|
||||
unit->status.error = 0;
|
||||
unit->status.front_sensor = 1;
|
||||
unit->status.rear_sensor = 1;
|
||||
unit->card_in = true;
|
||||
unit->card_in_time = get_performance_seconds();
|
||||
}
|
||||
@@ -144,6 +179,12 @@ static inline void update_card(int unit_id) {
|
||||
unit->state = CLOSE_EJECT;
|
||||
if (fabs(get_performance_seconds() - unit->card_in_time) > CARD_TIMEOUT) {
|
||||
unit->card_in = false;
|
||||
memset(unit->status.uid, 0, 8);
|
||||
if (unit->state_jdz == ACTIVE || unit->state_jdz == GET_USERID) {
|
||||
unit->state_jdz = ERR_REMOVED;
|
||||
}
|
||||
unit->status.front_sensor = 0;
|
||||
unit->status.rear_sensor = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +343,11 @@ static char __cdecl ac_io_icca_get_status(void *a1, void *a2) {
|
||||
|
||||
// copy state to output buffer
|
||||
ICCA_UNIT *unit = &ICCA_UNITS[unit_id];
|
||||
unit->status.status_code = unit->state;
|
||||
if (avs::game::is_model("JDZ")) {
|
||||
unit->status.status_code = unit->card_in ? ENGAGED : EMPTY;
|
||||
} else {
|
||||
unit->status.status_code = unit->state;
|
||||
}
|
||||
memcpy(status, &unit->status, sizeof(struct ICCA_STATUS));
|
||||
|
||||
// funny workaround
|
||||
@@ -387,6 +432,11 @@ static char __cdecl ac_io_icca_req_uid(int unit_id) {
|
||||
static int __cdecl ac_io_icca_req_uid_isfinished(int unit_id, DWORD *read_state) {
|
||||
unit_id = icca_get_unit_id(unit_id);
|
||||
ICCA_UNIT *unit = &ICCA_UNITS[unit_id];
|
||||
if (avs::game::is_model("JDZ")) {
|
||||
// hack for RA - only ever seen it as this
|
||||
*read_state = 8;
|
||||
return 1;
|
||||
}
|
||||
if (unit->card_in) {
|
||||
if (fabs(get_performance_seconds() - unit->card_in_time) < CARD_TIMEOUT) {
|
||||
unit->state = END;
|
||||
@@ -403,16 +453,61 @@ static int __cdecl ac_io_icca_send_keep_alive_packet(int a1, int a2, int a3) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int __cdecl ac_io_icca_workflow_jdz(int workflow, int unit_id) {
|
||||
|
||||
unit_id = icca_get_unit_id(unit_id);
|
||||
ICCA_UNIT *unit = &ICCA_UNITS[unit_id];
|
||||
switch (workflow) {
|
||||
// RA uses "STEP" as more of a polling thing it seems
|
||||
case STEP:
|
||||
switch (unit->state_jdz) {
|
||||
case CLOSE_EJECT:
|
||||
unit->state_jdz = CLOSE_E_CHK;
|
||||
break;
|
||||
case CLOSE_E_CHK:
|
||||
unit->state_jdz = CLOSE_END;
|
||||
break;
|
||||
case CLOSE_END:
|
||||
unit->state_jdz = SLEEP;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SLEEP:
|
||||
unit->state = SLEEP;
|
||||
break;
|
||||
case START:
|
||||
unit->state_jdz = INIT;
|
||||
break;
|
||||
case INIT:
|
||||
case READY:
|
||||
unit->state_jdz = READY;
|
||||
break;
|
||||
case CLOSE_EJECT:
|
||||
unit->state_jdz = CLOSE_EJECT;
|
||||
return CLOSE_E_CHK;
|
||||
case CLOSE_END:
|
||||
unit->state_jdz = SLEEP;
|
||||
break;
|
||||
default:
|
||||
// should probably log a warning here
|
||||
break;
|
||||
}
|
||||
return unit->state_jdz;
|
||||
}
|
||||
|
||||
static int __cdecl ac_io_icca_workflow(int workflow, int unit_id) {
|
||||
// RA uses a different workflow process for the slotted readers; treat it differently
|
||||
if (avs::game::is_model("JDZ")) {
|
||||
return ac_io_icca_workflow_jdz(workflow, unit_id);
|
||||
}
|
||||
|
||||
unit_id = icca_get_unit_id(unit_id);
|
||||
ICCA_UNIT *unit = &ICCA_UNITS[unit_id];
|
||||
switch (workflow) {
|
||||
case STEP:
|
||||
if (avs::game::is_model("JDZ"))
|
||||
unit->state = SLEEP;
|
||||
else
|
||||
unit->state = STEP;
|
||||
unit->state = STEP;
|
||||
break;
|
||||
case SLEEP:
|
||||
unit->state = SLEEP;
|
||||
@@ -441,7 +536,6 @@ static int __cdecl ac_io_icca_workflow(int workflow, int unit_id) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return unit->state;
|
||||
}
|
||||
|
||||
@@ -467,6 +561,14 @@ acio::ICCAModule::ICCAModule(HMODULE module, acio::HookMode hookMode) : ACIOModu
|
||||
void acio::ICCAModule::attach() {
|
||||
ACIOModule::attach();
|
||||
|
||||
// workaround for RA; ac_io_icca_cardunit_init is never called so treat like both are initialized
|
||||
if (avs::game::is_model("JDZ")) {
|
||||
ICCA_UNITS[0].initialized = true;
|
||||
ICCA_UNITS[1].initialized = true;
|
||||
|
||||
CARD_TIMEOUT = 10.0; // keep the card in the slot for longer
|
||||
}
|
||||
|
||||
// hooks
|
||||
ACIO_MODULE_HOOK(ac_io_icca_cardunit_init);
|
||||
ACIO_MODULE_HOOK(ac_io_icca_cardunit_init_isfinished);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "util/circular_buffer.h"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -237,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
|
||||
// SDVX Old cabinet mode
|
||||
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
|
||||
answer_type = 1;
|
||||
if (avs::game::is_model("L44"))
|
||||
if (avs::game::is_model({ "L44", "T44" }))
|
||||
answer_type = 2;
|
||||
|
||||
// check answer type
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "capture_pump.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#include "hooks/graphics/graphics.h"
|
||||
|
||||
namespace api::capture_pump {
|
||||
|
||||
namespace {
|
||||
|
||||
std::array<std::mutex, GRAPHICS_CAPTURE_SCREEN_NO> CONSUMER_M;
|
||||
|
||||
// read once per capture from the present thread, so it stays lock free
|
||||
std::atomic<bool> CLAIMED[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||
|
||||
bool valid_screen(int screen) {
|
||||
return 0 <= screen && screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO);
|
||||
}
|
||||
}
|
||||
|
||||
bool capture_direct(int screen, std::shared_ptr<uint8_t[]> &out, int divide,
|
||||
uint64_t *timestamp, int *width, int *height) {
|
||||
|
||||
if (!valid_screen(screen)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(CONSUMER_M[screen]);
|
||||
graphics_capture_trigger(screen);
|
||||
return graphics_capture_receive_raw(
|
||||
screen, out, divide, timestamp, width, height);
|
||||
}
|
||||
|
||||
bool claim_screen(int screen) {
|
||||
if (!valid_screen(screen)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !CLAIMED[screen].exchange(true);
|
||||
}
|
||||
|
||||
void release_screen(int screen) {
|
||||
if (!valid_screen(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CLAIMED[screen] = false;
|
||||
}
|
||||
|
||||
bool screen_claimed(int screen) {
|
||||
if (!valid_screen(screen)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CLAIMED[screen];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace api::capture_pump {
|
||||
|
||||
struct Frame {
|
||||
// packed 24bpp RGB, width * height * 3 bytes
|
||||
std::shared_ptr<uint8_t[]> pixels;
|
||||
uint64_t timestamp = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
// the graphics layer has one capture slot per screen, so concurrent waiters would steal
|
||||
// each other's frames; everything that captures goes through here to keep it serialized
|
||||
bool capture_direct(int screen, std::shared_ptr<uint8_t[]> &out, int divide,
|
||||
uint64_t *timestamp = nullptr, int *width = nullptr, int *height = nullptr);
|
||||
|
||||
// a screen carries one stream at a time; false when another connection already holds it
|
||||
bool claim_screen(int screen);
|
||||
void release_screen(int screen);
|
||||
|
||||
// true while a video stream client holds this screen
|
||||
bool screen_claimed(int screen);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace api {
|
||||
|
||||
extern std::atomic_uint32_t CLIENT_COUNT;
|
||||
|
||||
inline bool has_clients() {
|
||||
return CLIENT_COUNT.load(std::memory_order_relaxed) > 0;
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,15 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "client.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "overlay/notifications.h"
|
||||
|
||||
#include "module.h"
|
||||
#include "modules/analogs.h"
|
||||
#include "modules/buttons.h"
|
||||
@@ -26,6 +29,7 @@
|
||||
#include "modules/lcd.h"
|
||||
#include "modules/lights.h"
|
||||
#include "modules/memory.h"
|
||||
#include "modules/sdvx.h"
|
||||
#include "modules/touch.h"
|
||||
#include "modules/resize.h"
|
||||
#include "request.h"
|
||||
@@ -34,6 +38,8 @@
|
||||
using namespace rapidjson;
|
||||
using namespace api;
|
||||
|
||||
std::atomic_uint32_t api::CLIENT_COUNT = 0;
|
||||
|
||||
Controller::Controller(unsigned short port, std::string password, bool pretty)
|
||||
: port(port), password(std::move(password)), pretty(pretty)
|
||||
{
|
||||
@@ -197,6 +203,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 +286,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();
|
||||
@@ -325,9 +337,11 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
|
||||
if (module->name == request.module) {
|
||||
module_found = true;
|
||||
|
||||
// check password force
|
||||
if (module->password_force && this->password.empty() && request.function != "session_refresh") {
|
||||
Value err("Module requires the password to be set.");
|
||||
// check password requirement
|
||||
if (module->requires_password(request.function)
|
||||
&& this->password.empty()
|
||||
&& request.function != "session_refresh") {
|
||||
Value err("Function requires the password to be set.");
|
||||
response.add_error(err);
|
||||
break;
|
||||
}
|
||||
@@ -403,8 +417,11 @@ void Controller::init_state(api::ClientState *state) {
|
||||
state->modules.push_back(new modules::LCD());
|
||||
state->modules.push_back(new modules::Lights());
|
||||
state->modules.push_back(new modules::Memory());
|
||||
state->modules.push_back(new modules::SDVX());
|
||||
state->modules.push_back(new modules::Touch());
|
||||
state->modules.push_back(new modules::Resize());
|
||||
|
||||
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Controller::free_state(api::ClientState *state) {
|
||||
@@ -416,6 +433,8 @@ void Controller::free_state(api::ClientState *state) {
|
||||
|
||||
// free cipher
|
||||
delete state->cipher;
|
||||
|
||||
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Controller::free_socket() {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#include "h264_stream.h"
|
||||
|
||||
#ifdef SPICE_H264
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <x264.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
namespace {
|
||||
|
||||
// BT.601 limited range, the range every decoder assumes for H.264 without
|
||||
// explicit colour metadata
|
||||
inline uint8_t rgb_to_y(int r, int g, int b) {
|
||||
return static_cast<uint8_t>(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16);
|
||||
}
|
||||
|
||||
inline uint8_t rgb_to_u(int r, int g, int b) {
|
||||
return static_cast<uint8_t>(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128);
|
||||
}
|
||||
|
||||
inline uint8_t rgb_to_v(int r, int g, int b) {
|
||||
return static_cast<uint8_t>(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128);
|
||||
}
|
||||
|
||||
// a bare annex-b elementary stream, one encoder per connection so every client
|
||||
// starts on its own keyframe. no container, so nothing here keeps a media clock
|
||||
class H264Writer : public StreamWriter {
|
||||
public:
|
||||
|
||||
H264Writer(int quality, int fps) : quality(quality), fps(fps) {}
|
||||
|
||||
~H264Writer() override {
|
||||
this->close();
|
||||
}
|
||||
|
||||
std::string content_type() const override {
|
||||
return "video/h264";
|
||||
}
|
||||
|
||||
bool write(const StreamSend &send, const capture_pump::Frame &frame) override {
|
||||
|
||||
// I420 needs even dimensions
|
||||
const int width = frame.width & ~1;
|
||||
const int height = frame.height & ~1;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this->encoder == nullptr) {
|
||||
if (!this->open(width, height)) {
|
||||
return false;
|
||||
}
|
||||
} else if (width != this->width || height != this->height) {
|
||||
// the encoder is fixed at the size it opened with; let the client reconnect
|
||||
log_info("api::stream", "capture size changed, ending H.264 client");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->convert(frame.pixels.get(), frame.width);
|
||||
|
||||
this->picture.i_pts = this->frame_index;
|
||||
|
||||
x264_nal_t *nals = nullptr;
|
||||
int nal_count = 0;
|
||||
x264_picture_t picture_out;
|
||||
const int size = x264_encoder_encode(
|
||||
this->encoder, &nals, &nal_count, &this->picture, &picture_out);
|
||||
|
||||
if (size < 0) {
|
||||
log_warning("api::stream", "H.264 encode failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->frame_index++;
|
||||
|
||||
if (size == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// x264 lays every NAL of the frame out back to back. an SEI or delimiter
|
||||
// carries no picture, so only the parameter sets and the slice go through
|
||||
this->annexb.clear();
|
||||
for (int i = 0; i < nal_count; i++) {
|
||||
switch (nals[i].i_type) {
|
||||
case NAL_SEI:
|
||||
case NAL_AUD:
|
||||
case NAL_FILLER:
|
||||
continue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->annexb.insert(this->annexb.end(),
|
||||
nals[i].p_payload, nals[i].p_payload + nals[i].i_payload);
|
||||
}
|
||||
|
||||
if (this->annexb.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return send(this->annexb.data(), this->annexb.size());
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
bool open(int width, int height) {
|
||||
|
||||
x264_param_t param;
|
||||
if (x264_param_default_preset(¶m, "ultrafast", "zerolatency") < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
param.i_csp = X264_CSP_I420;
|
||||
param.i_width = width;
|
||||
param.i_height = height;
|
||||
param.i_fps_num = this->fps;
|
||||
param.i_fps_den = 1;
|
||||
|
||||
// sliced threading, which zerolatency already selected, so a frame is split
|
||||
// across workers rather than held back to be reordered. deliberately not the
|
||||
// automatic count: this shares a machine with the game it is capturing, and
|
||||
// taking every core to encode would win back frames at the game's expense
|
||||
param.i_threads = 4;
|
||||
|
||||
param.b_annexb = 1;
|
||||
// SPS/PPS ahead of every IDR, so a client can start decoding cold
|
||||
param.b_repeat_headers = 1;
|
||||
// a keyframe every two seconds bounds how long a new client waits
|
||||
param.i_keyint_max = this->fps * 2;
|
||||
param.i_log_level = X264_LOG_NONE;
|
||||
param.rc.i_rc_method = X264_RC_CRF;
|
||||
param.rc.f_rf_constant = 40.0f - (this->quality * 0.25f);
|
||||
|
||||
// baseline keeps hardware decode available on the widest range of phones
|
||||
if (x264_param_apply_profile(¶m, "baseline") < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->encoder = x264_encoder_open(¶m);
|
||||
if (this->encoder == nullptr) {
|
||||
log_warning("api::stream", "could not open the H.264 encoder");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x264_picture_alloc(&this->picture, X264_CSP_I420, width, height) < 0) {
|
||||
this->close();
|
||||
return false;
|
||||
}
|
||||
this->picture_ready = true;
|
||||
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (this->picture_ready) {
|
||||
x264_picture_clean(&this->picture);
|
||||
this->picture_ready = false;
|
||||
}
|
||||
if (this->encoder != nullptr) {
|
||||
x264_encoder_close(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// packed 24bpp RGB to I420, averaging each 2x2 block for the chroma planes
|
||||
void convert(const uint8_t *rgb, int source_width) {
|
||||
|
||||
uint8_t *plane_y = this->picture.img.plane[0];
|
||||
uint8_t *plane_u = this->picture.img.plane[1];
|
||||
uint8_t *plane_v = this->picture.img.plane[2];
|
||||
const int stride_y = this->picture.img.i_stride[0];
|
||||
const int stride_u = this->picture.img.i_stride[1];
|
||||
const int stride_v = this->picture.img.i_stride[2];
|
||||
|
||||
for (int y = 0; y < this->height; y++) {
|
||||
const uint8_t *row = rgb + static_cast<size_t>(y) * source_width * 3;
|
||||
uint8_t *out_y = plane_y + static_cast<size_t>(y) * stride_y;
|
||||
|
||||
for (int x = 0; x < this->width; x++) {
|
||||
const uint8_t *pixel = row + x * 3;
|
||||
out_y[x] = rgb_to_y(pixel[0], pixel[1], pixel[2]);
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < this->height / 2; y++) {
|
||||
const uint8_t *row0 = rgb + static_cast<size_t>(y * 2) * source_width * 3;
|
||||
const uint8_t *row1 = row0 + static_cast<size_t>(source_width) * 3;
|
||||
uint8_t *out_u = plane_u + static_cast<size_t>(y) * stride_u;
|
||||
uint8_t *out_v = plane_v + static_cast<size_t>(y) * stride_v;
|
||||
|
||||
for (int x = 0; x < this->width / 2; x++) {
|
||||
const uint8_t *p00 = row0 + (x * 2) * 3;
|
||||
const uint8_t *p01 = p00 + 3;
|
||||
const uint8_t *p10 = row1 + (x * 2) * 3;
|
||||
const uint8_t *p11 = p10 + 3;
|
||||
|
||||
const int r = (p00[0] + p01[0] + p10[0] + p11[0] + 2) / 4;
|
||||
const int g = (p00[1] + p01[1] + p10[1] + p11[1] + 2) / 4;
|
||||
const int b = (p00[2] + p01[2] + p10[2] + p11[2] + 2) / 4;
|
||||
|
||||
out_u[x] = rgb_to_u(r, g, b);
|
||||
out_v[x] = rgb_to_v(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int quality;
|
||||
int fps;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int64_t frame_index = 0;
|
||||
std::vector<uint8_t> annexb;
|
||||
|
||||
x264_t *encoder = nullptr;
|
||||
x264_picture_t picture {};
|
||||
bool picture_ready = false;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<StreamWriter> make_h264_writer(int quality, int fps) {
|
||||
return std::make_unique<H264Writer>(quality, fps);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // SPICE_H264
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "stream_format.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
// bare annex-b H.264; null when the build has no encoder
|
||||
std::unique_ptr<StreamWriter> make_h264_writer(int quality, int fps);
|
||||
}
|
||||
@@ -16,6 +16,14 @@ namespace api {
|
||||
this->password_force = password_force;
|
||||
}
|
||||
|
||||
void Module::require_password(const std::string &function) {
|
||||
this->password_force_functions.emplace(function);
|
||||
}
|
||||
|
||||
bool Module::requires_password(const std::string &function) const {
|
||||
return this->password_force || this->password_force_functions.contains(function);
|
||||
}
|
||||
|
||||
void Module::handle(Request &req, Response &res) {
|
||||
|
||||
// log module access
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include <external/robin_hood.h>
|
||||
|
||||
#include "response.h"
|
||||
@@ -26,6 +27,13 @@ namespace api {
|
||||
// default constructor
|
||||
explicit Module(std::string name, bool password_force=false);
|
||||
|
||||
void require_password(const std::string &function);
|
||||
|
||||
private:
|
||||
|
||||
// functions which expose sensitive data or actions
|
||||
std::unordered_set<std::string> password_force_functions;
|
||||
|
||||
public:
|
||||
|
||||
// virtual deconstructor
|
||||
@@ -35,6 +43,8 @@ namespace api {
|
||||
std::string name;
|
||||
bool password_force;
|
||||
|
||||
bool requires_password(const std::string &function) const;
|
||||
|
||||
// the magic
|
||||
void handle(Request &req, Response &res);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
#include "capture.h"
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include "api/capture_pump.h"
|
||||
#include "api/stream_format.h"
|
||||
#include "api/stream_server.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "hooks/graphics/jpeg_encoder.h"
|
||||
#include "util/crypt.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
@@ -14,9 +21,60 @@ 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);
|
||||
functions["get_streams"] = std::bind(&Capture::get_streams, this, _1, _2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +99,6 @@ namespace api::modules {
|
||||
* reduce: uint for dividing image size
|
||||
*/
|
||||
void Capture::get_jpg(Request &req, Response &res) {
|
||||
CAPTURE_BUFFER.reserve(1024 * 128);
|
||||
|
||||
// settings
|
||||
int screen = 0;
|
||||
@@ -67,28 +124,119 @@ namespace api::modules {
|
||||
uint64_t timestamp = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
graphics_capture_trigger(screen);
|
||||
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
|
||||
CAPTURE_BUFFER.push_back(byte);
|
||||
}, true, quality, true, divide, ×tamp, &width, &height);
|
||||
if (!success) {
|
||||
|
||||
std::shared_ptr<uint8_t[]> pixels;
|
||||
bool success = capture_pump::capture_direct(
|
||||
screen, pixels, divide, ×tamp, &width, &height);
|
||||
|
||||
if (success) {
|
||||
CAPTURE_BUFFER.clear();
|
||||
success = jpeg_encoder::encode(
|
||||
CAPTURE_BUFFER, pixels.get(), width, height, quality);
|
||||
}
|
||||
|
||||
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();
|
||||
try_cached_response(screen, res);
|
||||
}
|
||||
|
||||
// add data to response
|
||||
Value data;
|
||||
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
|
||||
res.add_data(timestamp);
|
||||
res.add_data(width);
|
||||
res.add_data(height);
|
||||
res.add_data(data);
|
||||
/**
|
||||
* get_streams()
|
||||
*/
|
||||
void Capture::get_streams(Request &req, Response &res) {
|
||||
|
||||
auto &alloc = res.doc()->GetAllocator();
|
||||
|
||||
// nothing is listening without -apistream, so there is no stream to describe
|
||||
const unsigned short port = stream_server_port();
|
||||
if (port == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Value formats(kArrayType);
|
||||
for (const auto &[name, path] : stream_formats()) {
|
||||
Value entry(kObjectType);
|
||||
entry.AddMember("name", Value(name.c_str(), alloc), alloc);
|
||||
entry.AddMember("path", Value(path.c_str(), alloc), alloc);
|
||||
formats.PushBack(entry, alloc);
|
||||
}
|
||||
|
||||
std::vector<int> screen_numbers;
|
||||
graphics_screens_get(screen_numbers);
|
||||
|
||||
// measuring a screen nobody has captured yet waits for the game to present, which can
|
||||
// take as long as the whole request is allowed, so only one screen is measured per
|
||||
// call and the rest are reported null until a later one settles them. which screen
|
||||
// gets the attempt rotates, otherwise one that never presents would take every
|
||||
// request and the screens behind it would stay unmeasured forever
|
||||
int probe_screen = -1;
|
||||
{
|
||||
std::vector<int> unmeasured;
|
||||
for (const auto screen : screen_numbers) {
|
||||
if (screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
|
||||
&& !graphics_capture_last_size(screen, nullptr, nullptr)
|
||||
&& !capture_pump::screen_claimed(screen)) {
|
||||
unmeasured.push_back(screen);
|
||||
}
|
||||
}
|
||||
|
||||
if (!unmeasured.empty()) {
|
||||
static std::atomic<unsigned> probe_cursor { 0 };
|
||||
probe_screen = unmeasured[probe_cursor.fetch_add(1) % unmeasured.size()];
|
||||
}
|
||||
}
|
||||
|
||||
Value screens(kArrayType);
|
||||
for (const auto screen : screen_numbers) {
|
||||
if (screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool known = graphics_capture_last_size(screen, &width, &height);
|
||||
|
||||
// a probe holds the screen for as long as it waits, so a second caller arriving
|
||||
// during one would queue behind it and then take a wait of its own; let it report
|
||||
// the screen as unmeasured instead and pick the size up once the first is done
|
||||
static std::atomic<bool> probe_running { false };
|
||||
if (!known && screen == probe_screen && !probe_running.exchange(true)) {
|
||||
std::shared_ptr<uint8_t[]> pixels;
|
||||
known = capture_pump::capture_direct(
|
||||
screen, pixels, 1, nullptr, &width, &height);
|
||||
probe_running = false;
|
||||
}
|
||||
|
||||
// a screen of unknown size cannot be described, and a client told about it could
|
||||
// not size its decoder anyway; leaving it out until it has been measured beats
|
||||
// handing over an entry that has to be treated as absent
|
||||
if (!known) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Value entry(kObjectType);
|
||||
entry.AddMember("screen", screen, alloc);
|
||||
entry.AddMember("width", width, alloc);
|
||||
entry.AddMember("height", height, alloc);
|
||||
|
||||
// a screen carries one viewer at a time, so this is what decides whether a client
|
||||
// can connect at all; still racy by the time it does, only more honest than not
|
||||
entry.AddMember("busy", capture_pump::screen_claimed(screen), alloc);
|
||||
|
||||
screens.PushBack(entry, alloc);
|
||||
}
|
||||
|
||||
Value info(kObjectType);
|
||||
info.AddMember("port", port, alloc);
|
||||
info.AddMember("formats", formats, alloc);
|
||||
info.AddMember("screens", screens, alloc);
|
||||
|
||||
res.add_data(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,6 @@ namespace api::modules {
|
||||
// function definitions
|
||||
void get_screens(Request &req, Response &res);
|
||||
void get_jpg(Request &req, Response &res);
|
||||
void get_streams(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "card.h"
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "util/logging.h"
|
||||
@@ -11,8 +12,71 @@ using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
static bool normalize_card_id(const std::string &value, std::string &card_id) {
|
||||
if (value.size() != 16) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t card_bin[8] {};
|
||||
if (!hex2bin(value.c_str(), card_bin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
card_id = bin2hex(card_bin, std::size(card_bin));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_card_id(const std::filesystem::path &path, std::string &card_id) {
|
||||
std::ifstream file(path);
|
||||
char buffer[16] {};
|
||||
if (!file.read(buffer, std::size(buffer))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalize_card_id(std::string(buffer, std::size(buffer)), card_id);
|
||||
}
|
||||
|
||||
Card::Card() : Module("card") {
|
||||
functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2);
|
||||
functions["insert"] = std::bind(&Card::insert, this, _1, _2);
|
||||
require_password("get_cards");
|
||||
}
|
||||
|
||||
/**
|
||||
* get_cards()
|
||||
*/
|
||||
void Card::get_cards(Request &req, Response &res) {
|
||||
auto &alloc = res.doc()->GetAllocator();
|
||||
|
||||
for (int index = 0; index < eamuse_get_game_keypads(); index++) {
|
||||
std::string card_id;
|
||||
std::string filename;
|
||||
const auto card_override = eamuse_get_card_override(index);
|
||||
const bool has_override = !card_override.empty();
|
||||
|
||||
if (has_override) {
|
||||
if (!normalize_card_id(card_override, card_id)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
const auto path = eamuse_get_card_path(index);
|
||||
if (!read_card_id(path, card_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto filename_u8 = path.filename().u8string();
|
||||
filename.assign(filename_u8.begin(), filename_u8.end());
|
||||
}
|
||||
|
||||
Value card(kObjectType);
|
||||
card.AddMember("index", index, alloc);
|
||||
card.AddMember("card_id", Value(card_id.c_str(), alloc), alloc);
|
||||
card.AddMember("source", Value(has_override ? "override" : "file", alloc), alloc);
|
||||
if (!has_override) {
|
||||
card.AddMember("file_name", Value(filename.c_str(), alloc), alloc);
|
||||
}
|
||||
res.add_data(card);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace api::modules {
|
||||
private:
|
||||
|
||||
// function definitions
|
||||
void get_cards(Request &req, Response &res);
|
||||
void insert(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,14 +106,16 @@ namespace api::modules {
|
||||
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
|
||||
// Create an array for the light state
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator());
|
||||
light_state.Reserve(
|
||||
static_cast<SizeType>(mapping.data.size() * 3),
|
||||
res.doc()->GetAllocator());
|
||||
for (const auto [r, g, b] : mapping.data) {
|
||||
light_state.PushBack(r, res.doc()->GetAllocator());
|
||||
light_state.PushBack(g, res.doc()->GetAllocator());
|
||||
light_state.PushBack(b, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
// Can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
// can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
|
||||
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "keypads.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
@@ -91,11 +93,11 @@ namespace api::modules {
|
||||
|
||||
// set
|
||||
eamuse_set_keypad_overrides(keypad, state);
|
||||
Sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
|
||||
// unset
|
||||
eamuse_set_keypad_overrides(keypad, 0);
|
||||
Sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "sdvx.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
SDVX::SDVX() : Module("sdvx") {
|
||||
functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2);
|
||||
|
||||
for (auto &light : games::sdvx::TAPELED_MAPPING) {
|
||||
lights_by_names.emplace(light.lightName, light);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tapeled_get()
|
||||
* tapeled_get(name: str, ...)
|
||||
*/
|
||||
void SDVX::tapeled_get(Request &req, Response &res) {
|
||||
Value response_object(kObjectType);
|
||||
|
||||
// all tape leds
|
||||
if (req.params.Size() == 0) {
|
||||
// iterate through each device and dump its lights data into the response
|
||||
for (const auto &mapping : games::sdvx::TAPELED_MAPPING) {
|
||||
copy_tapeled_data(res, response_object, mapping);
|
||||
}
|
||||
} else {
|
||||
// specified light names
|
||||
for (Value ¶m : req.params.GetArray()) {
|
||||
// check params
|
||||
if (!param.IsString()) {
|
||||
error_type(res, "name", "string");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto name = param.GetString();
|
||||
if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) {
|
||||
copy_tapeled_data(res, response_object, it->second.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.add_data(response_object);
|
||||
}
|
||||
|
||||
void SDVX::copy_tapeled_data(Response &res, Value &response_object,
|
||||
const tapeledutils::tape_led &mapping)
|
||||
{
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(
|
||||
static_cast<SizeType>(mapping.data.size() * 3),
|
||||
res.doc()->GetAllocator());
|
||||
for (const auto [r, g, b] : mapping.data) {
|
||||
light_state.PushBack(r, res.doc()->GetAllocator());
|
||||
light_state.PushBack(g, res.doc()->GetAllocator());
|
||||
light_state.PushBack(b, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
// can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
|
||||
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "external/robin_hood.h"
|
||||
#include "games/sdvx/sdvx.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
class SDVX : public Module {
|
||||
public:
|
||||
SDVX();
|
||||
|
||||
private:
|
||||
robin_hood::unordered_map<std::string, std::reference_wrapper<tapeledutils::tape_led>> lights_by_names;
|
||||
|
||||
void tapeled_get(Request &req, Response &res);
|
||||
void copy_tapeled_data(Response &res, rapidjson::Value &response_object,
|
||||
const tapeledutils::tape_led &mapping);
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "touch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include "external/rapidjson/document.h"
|
||||
@@ -8,7 +10,11 @@
|
||||
#include "misc/eamuse.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "touch/native/transform.h"
|
||||
#include "util/utils.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
@@ -17,6 +23,54 @@ 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);
|
||||
}
|
||||
|
||||
// map API coordinates onto the touch space SDVX reads, which depends on how it is displayed
|
||||
static void sdvx_touch_errata(
|
||||
int &x, int &y, bool use_native, int canvas_w, int canvas_h) {
|
||||
|
||||
// windowed coordinates already match the sub screen window they land on
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// landscape mode: native injection hands the game these coordinates
|
||||
// unchanged, so apply the rotation the touchscreen gets, while wintouchemu instead
|
||||
// rotates them later through the subscreen overlay
|
||||
if (GRAPHICS_FS_ORIENTATION_SWAP) {
|
||||
if (use_native) {
|
||||
POINT position { x, y };
|
||||
if (nativetouch::transform::sdvx_landscape_rotate(&position, canvas_w, canvas_h)) {
|
||||
x = position.x;
|
||||
y = position.y;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// rotate into the portrait touch space
|
||||
const int x_raw = x;
|
||||
x = canvas_w - y;
|
||||
y = x_raw;
|
||||
}
|
||||
|
||||
Touch::Touch() : Module("touch") {
|
||||
is_sdvx = avs::game::is_model("KFC");
|
||||
|
||||
@@ -26,6 +80,32 @@ 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) {
|
||||
// windowed API coordinates land on the sub screen window as-is; fullscreen
|
||||
// coordinates are rotated into the game's touch space by apply_touch_errata
|
||||
const bool landscape_coordinates =
|
||||
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
|
||||
native_canvas_w = landscape_coordinates ? 1920 : 1080;
|
||||
native_canvas_h = landscape_coordinates ? 1080 : 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;
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
// GITADORA arena SMALL subscreen, either in its own window or in the overlay
|
||||
native_canvas_w = games::gitadora::ARENA_SUBSCREEN_WIDTH;
|
||||
native_canvas_h = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
|
||||
}
|
||||
|
||||
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 +179,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,23 +207,54 @@ 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) {
|
||||
int x_raw = x;
|
||||
int y_raw = y;
|
||||
|
||||
if (is_tdj_fhd) {
|
||||
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p)
|
||||
// we don't know what screen is being shown on the companion and the API doesn't specify
|
||||
// the target of the touch events so just assume it's the sub screen
|
||||
x = x_raw * 1920 / 1280;
|
||||
y = y_raw * 1080 / 720;
|
||||
x = x * 1920 / 1280;
|
||||
y = y * 1080 / 720;
|
||||
} else if (is_sdvx) {
|
||||
// for exceed gear, they are both 1080p screens, but need to apply transformation
|
||||
x = 1080 - y_raw;
|
||||
y = x_raw;
|
||||
sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ part "src/exceptions.dart";
|
||||
part "src/rc4.dart";
|
||||
part "src/wrappers/analogs.dart";
|
||||
part "src/wrappers/buttons.dart";
|
||||
part "src/wrappers/capture.dart";
|
||||
part "src/wrappers/card.dart";
|
||||
part "src/wrappers/coin.dart";
|
||||
part "src/wrappers/control.dart";
|
||||
|
||||
@@ -4,8 +4,8 @@ part of spiceapi;
|
||||
class Connection {
|
||||
|
||||
// settings
|
||||
static const _TIMEOUT = Duration(seconds: 2);
|
||||
static const _BUFFER_SIZE = 1024 * 8;
|
||||
static const _TIMEOUT = Duration(seconds: 3);
|
||||
static const _BUFFER_SIZE = 1024 * 1024 * 8;
|
||||
|
||||
// state
|
||||
final String host, pass;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
part of spiceapi;
|
||||
|
||||
class CaptureData {
|
||||
int timestamp;
|
||||
int width, height;
|
||||
Uint8List data;
|
||||
}
|
||||
|
||||
var _base64DecoderInstance = Base64Decoder();
|
||||
|
||||
Future<List> captureGetScreens(Connection con) {
|
||||
var req = Request("capture", "get_screens");
|
||||
return con.request(req).then((res) {
|
||||
return res.getData();
|
||||
});
|
||||
}
|
||||
|
||||
Future<CaptureData> captureGetJPG(Connection con, {
|
||||
int screen = 0,
|
||||
int quality = 70,
|
||||
int divide = 1,
|
||||
}) {
|
||||
var req = Request("capture", "get_jpg");
|
||||
req.addParam(screen);
|
||||
req.addParam(quality);
|
||||
req.addParam(divide);
|
||||
return con.request(req).then((res) {
|
||||
var captureData = CaptureData();
|
||||
var data = res.getData();
|
||||
if (data.length > 0) captureData.timestamp = data[0];
|
||||
if (data.length > 1) captureData.width = data[1];
|
||||
if (data.length > 2) captureData.height = data[2];
|
||||
if (data.length > 3) {
|
||||
captureData.data = _base64DecoderInstance.convert(data[3]);
|
||||
}
|
||||
return captureData;
|
||||
});
|
||||
}
|
||||
|
||||
/// Describes the HTTP video stream, or null when this spice2x serves none.
|
||||
Future<Map> captureGetStreams(Connection con) {
|
||||
var req = Request("capture", "get_streams");
|
||||
return con.request(req).then((res) {
|
||||
var data = res.getData();
|
||||
return data.length > 0 ? data[0] : null;
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,32 @@
|
||||
part of spiceapi;
|
||||
|
||||
class CardInfo {
|
||||
final int index;
|
||||
final String cardID;
|
||||
final String source;
|
||||
final String? fileName;
|
||||
|
||||
CardInfo(this.index, this.cardID, this.source, this.fileName);
|
||||
}
|
||||
|
||||
Future<List<CardInfo>> cardGetCards(Connection con) {
|
||||
var req = Request("card", "get_cards");
|
||||
return con.request(req).then((res) {
|
||||
List<CardInfo> cards = [];
|
||||
for (var value in res.getData()) {
|
||||
cards.add(
|
||||
CardInfo(
|
||||
value["index"],
|
||||
value["card_id"],
|
||||
value["source"],
|
||||
value["file_name"],
|
||||
),
|
||||
);
|
||||
}
|
||||
return cards;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||
var req = Request("card", "insert");
|
||||
req.addParam(unit);
|
||||
|
||||
@@ -17,7 +17,7 @@ Future<List> captureGetScreens(Connection con) {
|
||||
|
||||
Future<CaptureData> captureGetJPG(Connection con, {
|
||||
int screen = 0,
|
||||
int quality = 60,
|
||||
int quality = 70,
|
||||
int divide = 1,
|
||||
}) {
|
||||
var req = Request("capture", "get_jpg");
|
||||
@@ -36,3 +36,12 @@ Future<CaptureData> captureGetJPG(Connection con, {
|
||||
return captureData;
|
||||
});
|
||||
}
|
||||
|
||||
/// Describes the HTTP video stream, or null when this spice2x serves none.
|
||||
Future<Map> captureGetStreams(Connection con) {
|
||||
var req = Request("capture", "get_streams");
|
||||
return con.request(req).then((res) {
|
||||
var data = res.getData();
|
||||
return data.length > 0 ? data[0] : null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
part of spiceapi;
|
||||
|
||||
class CardInfo {
|
||||
final int index;
|
||||
final String cardID;
|
||||
final String source;
|
||||
final String? fileName;
|
||||
|
||||
CardInfo(this.index, this.cardID, this.source, this.fileName);
|
||||
}
|
||||
|
||||
Future<List<CardInfo>> cardGetCards(Connection con) {
|
||||
var req = Request("card", "get_cards");
|
||||
return con.request(req).then((res) {
|
||||
List<CardInfo> cards = [];
|
||||
for (var value in res.getData()) {
|
||||
cards.add(
|
||||
CardInfo(
|
||||
value["index"],
|
||||
value["card_id"],
|
||||
value["source"],
|
||||
value["file_name"],
|
||||
),
|
||||
);
|
||||
}
|
||||
return cards;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||
var req = Request("card", "insert");
|
||||
req.addParam(unit);
|
||||
|
||||
@@ -2,11 +2,13 @@ from .connection import Connection
|
||||
from .request import Request
|
||||
from .analogs import *
|
||||
from .buttons import *
|
||||
from .capture import *
|
||||
from .card import *
|
||||
from .coin import *
|
||||
from .control import *
|
||||
from .exceptions import *
|
||||
from .iidx import *
|
||||
from .sdvx import *
|
||||
from .info import *
|
||||
from .keypads import *
|
||||
from .lights import *
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import base64
|
||||
|
||||
from .connection import Connection
|
||||
from .request import Request
|
||||
|
||||
|
||||
def capture_get_screens(con: Connection):
|
||||
res = con.request(Request("capture", "get_screens"))
|
||||
return res.get_data()
|
||||
|
||||
|
||||
def capture_get_jpg(con: Connection, screen: int = 0, quality: int = 70, divide: int = 1):
|
||||
req = Request("capture", "get_jpg")
|
||||
req.add_param(screen)
|
||||
req.add_param(quality)
|
||||
req.add_param(divide)
|
||||
data = con.request(req).get_data()
|
||||
|
||||
if len(data) < 4:
|
||||
return None
|
||||
|
||||
return {
|
||||
"timestamp": data[0],
|
||||
"width": data[1],
|
||||
"height": data[2],
|
||||
"data": base64.b64decode(data[3]),
|
||||
}
|
||||
|
||||
|
||||
def capture_get_streams(con: Connection):
|
||||
"""Describes the HTTP video stream, or None when this spice2x serves none."""
|
||||
data = con.request(Request("capture", "get_streams")).get_data()
|
||||
return data[0] if data else None
|
||||
@@ -2,6 +2,10 @@ from .connection import Connection
|
||||
from .request import Request
|
||||
|
||||
|
||||
def card_get_cards(con: Connection):
|
||||
return con.request(Request("card", "get_cards")).get_data()
|
||||
|
||||
|
||||
def card_insert(con: Connection, unit: int, card_id: str):
|
||||
req = Request("card", "insert")
|
||||
req.add_param(unit)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from .connection import Connection
|
||||
from .request import Request
|
||||
|
||||
|
||||
def sdvx_tapeled_get(con: Connection, *light_names):
|
||||
req = Request("sdvx", "tapeled_get")
|
||||
|
||||
for light_name in light_names:
|
||||
req.add_param(light_name)
|
||||
|
||||
res = con.request(req)
|
||||
return res.get_data()
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "controller.h"
|
||||
#include "serial.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "util/logging.h"
|
||||
@@ -160,7 +162,7 @@ namespace api {
|
||||
|
||||
// slow down on reconnect
|
||||
if (this->running) {
|
||||
Sleep(retry_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "stream_format.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "h264_stream.h"
|
||||
#include "hooks/graphics/jpeg_encoder.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef SPICE_JPEG
|
||||
constexpr const char *MJPEG_BOUNDARY = "spice2xframe";
|
||||
|
||||
// multipart/x-mixed-replace: every frame is a standalone JPEG, no inter-frame state
|
||||
class MjpegWriter : public StreamWriter {
|
||||
public:
|
||||
|
||||
explicit MjpegWriter(int quality) : quality(quality) {}
|
||||
|
||||
std::string content_type() const override {
|
||||
return std::string("multipart/x-mixed-replace; boundary=") + MJPEG_BOUNDARY;
|
||||
}
|
||||
|
||||
bool write(const StreamSend &send, const capture_pump::Frame &frame) override {
|
||||
this->jpeg.clear();
|
||||
if (!jpeg_encoder::encode(
|
||||
this->jpeg, frame.pixels.get(),
|
||||
frame.width, frame.height, this->quality)) {
|
||||
// a frame the encoder rejects is not worth dropping the client over
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string part =
|
||||
"--" + std::string(MJPEG_BOUNDARY) + "\r\n"
|
||||
"Content-Type: image/jpeg\r\n"
|
||||
"Content-Length: " + std::to_string(this->jpeg.size()) + "\r\n"
|
||||
"\r\n";
|
||||
|
||||
return send(part.data(), part.size())
|
||||
&& send(this->jpeg.data(), this->jpeg.size())
|
||||
&& send("\r\n", 2);
|
||||
}
|
||||
|
||||
private:
|
||||
int quality;
|
||||
std::vector<uint8_t> jpeg;
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
// both parameters go unused on toolchains that compile in neither format
|
||||
std::unique_ptr<StreamWriter> make_stream_writer(
|
||||
const std::string &path, [[maybe_unused]] int quality, [[maybe_unused]] int fps) {
|
||||
|
||||
#ifdef SPICE_JPEG
|
||||
if (path == "/stream.mjpg") {
|
||||
return std::make_unique<MjpegWriter>(quality);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SPICE_H264
|
||||
if (path == "/stream.h264") {
|
||||
return make_h264_writer(quality, fps);
|
||||
}
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> stream_formats() {
|
||||
std::vector<std::pair<std::string, std::string>> formats;
|
||||
|
||||
#ifdef SPICE_JPEG
|
||||
formats.emplace_back("mjpeg", "/stream.mjpg");
|
||||
#endif
|
||||
|
||||
#ifdef SPICE_H264
|
||||
formats.emplace_back("h264", "/stream.h264");
|
||||
#endif
|
||||
|
||||
return formats;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "capture_pump.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
// writes bytes to the client; false once the connection is gone
|
||||
using StreamSend = std::function<bool(const void *, size_t)>;
|
||||
|
||||
// one wire format, instantiated per connection so it can keep encoder state across frames
|
||||
class StreamWriter {
|
||||
public:
|
||||
virtual ~StreamWriter() = default;
|
||||
|
||||
StreamWriter(const StreamWriter &) = delete;
|
||||
StreamWriter &operator=(const StreamWriter &) = delete;
|
||||
|
||||
// value for the HTTP Content-Type response header
|
||||
virtual std::string content_type() const = 0;
|
||||
|
||||
// for formats that open with an init segment; runs once before any frame
|
||||
virtual bool begin(const StreamSend &send) { return true; }
|
||||
|
||||
virtual bool write(const StreamSend &send, const capture_pump::Frame &frame) = 0;
|
||||
|
||||
protected:
|
||||
StreamWriter() = default;
|
||||
};
|
||||
|
||||
// null when the path does not name a format this build supports
|
||||
std::unique_ptr<StreamWriter> make_stream_writer(
|
||||
const std::string &path, int quality, int fps);
|
||||
|
||||
// name and path of every format compiled into this build, for clients to pick from
|
||||
std::vector<std::pair<std::string, std::string>> stream_formats();
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include "stream_server.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "capture_pump.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "stream_format.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
namespace {
|
||||
|
||||
struct HttpRequest {
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::map<std::string, std::string> query;
|
||||
};
|
||||
|
||||
bool send_all(SOCKET socket, const void *data, size_t size) {
|
||||
auto cursor = reinterpret_cast<const char *>(data);
|
||||
size_t remaining = size;
|
||||
|
||||
while (remaining > 0) {
|
||||
const int sent = send(socket, cursor, static_cast<int>(remaining), 0);
|
||||
if (sent <= 0) {
|
||||
return false;
|
||||
}
|
||||
cursor += sent;
|
||||
remaining -= static_cast<size_t>(sent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool send_all(SOCKET socket, const std::string &text) {
|
||||
return send_all(socket, text.data(), text.size());
|
||||
}
|
||||
|
||||
// a viewer leaving is normally noticed by a failing send, so a stream with no frame
|
||||
// to push has to ask the socket instead
|
||||
bool client_gone(SOCKET socket) {
|
||||
fd_set read_set;
|
||||
FD_ZERO(&read_set);
|
||||
FD_SET(socket, &read_set);
|
||||
|
||||
// the socket is blocking with a receive timeout, so poll before touching it
|
||||
timeval immediately {};
|
||||
const int ready = select(0, &read_set, nullptr, nullptr, &immediately);
|
||||
if (ready == 0) {
|
||||
return false;
|
||||
}
|
||||
if (ready < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// consumed rather than peeked: a stray byte would otherwise sit in front of the
|
||||
// FIN and keep hiding it for as long as the stream runs
|
||||
char discard[256];
|
||||
return recv(socket, discard, sizeof(discard), 0) <= 0;
|
||||
}
|
||||
|
||||
std::string url_decode(const std::string &input) {
|
||||
std::string out;
|
||||
out.reserve(input.size());
|
||||
|
||||
for (size_t i = 0; i < input.size(); i++) {
|
||||
if (input[i] == '+') {
|
||||
out.push_back(' ');
|
||||
} else if (input[i] == '%' && i + 2 < input.size()
|
||||
&& isxdigit(static_cast<unsigned char>(input[i + 1]))
|
||||
&& isxdigit(static_cast<unsigned char>(input[i + 2]))) {
|
||||
out.push_back(static_cast<char>(
|
||||
std::stoi(input.substr(i + 1, 2), nullptr, 16)));
|
||||
i += 2;
|
||||
} else {
|
||||
out.push_back(input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void parse_query(const std::string &query, HttpRequest &request) {
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < query.size()) {
|
||||
auto end = query.find('&', pos);
|
||||
if (end == std::string::npos) {
|
||||
end = query.size();
|
||||
}
|
||||
|
||||
const auto pair = query.substr(pos, end - pos);
|
||||
const auto split = pair.find('=');
|
||||
if (split != std::string::npos && split > 0) {
|
||||
request.query[url_decode(pair.substr(0, split))] =
|
||||
url_decode(pair.substr(split + 1));
|
||||
}
|
||||
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// reads the request head only; anything oversized or malformed is refused
|
||||
bool read_request(SOCKET socket, size_t size_limit, HttpRequest &request) {
|
||||
std::string head;
|
||||
char buffer[1024];
|
||||
|
||||
while (head.find("\r\n\r\n") == std::string::npos) {
|
||||
if (head.size() >= size_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// read no further than the limit, so the head cannot overshoot it
|
||||
const size_t budget = std::min(sizeof(buffer), size_limit - head.size());
|
||||
const int received = recv(socket, buffer, static_cast<int>(budget), 0);
|
||||
if (received <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
head.append(buffer, static_cast<size_t>(received));
|
||||
}
|
||||
|
||||
const auto line_end = head.find("\r\n");
|
||||
const auto line = head.substr(0, line_end);
|
||||
|
||||
const auto method_end = line.find(' ');
|
||||
if (method_end == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto target_end = line.find(' ', method_end + 1);
|
||||
if (target_end == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
request.method = line.substr(0, method_end);
|
||||
auto target = line.substr(method_end + 1, target_end - method_end - 1);
|
||||
|
||||
const auto query_start = target.find('?');
|
||||
if (query_start != std::string::npos) {
|
||||
parse_query(target.substr(query_start + 1), request);
|
||||
target = target.substr(0, query_start);
|
||||
}
|
||||
|
||||
request.path = url_decode(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
int query_int(const HttpRequest &request, const std::string &name, int fallback,
|
||||
int min, int max) {
|
||||
|
||||
const auto pos = request.query.find(name);
|
||||
if (pos == request.query.end()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return std::clamp(std::stoi(pos->second), min, max);
|
||||
} catch (const std::exception &) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// an <img> can show a cross-origin stream without this, but a browser client that
|
||||
// decodes the bytes itself has to fetch() them, and fetch is subject to CORS. errors
|
||||
// carry it too, or the client sees an opaque failure instead of the status.
|
||||
constexpr const char *cors_header = "Access-Control-Allow-Origin: *\r\n";
|
||||
|
||||
// the port is unauthenticated, so a scanner hammering a busy/missing screen could
|
||||
// otherwise flood the overlay; throttle failure toasts per distinct cause. kept under
|
||||
// a second so it only swallows that, not a legitimate reconnect - substream itself
|
||||
// switches screens with a 300ms gap, and only backs off to a full second once a
|
||||
// retry has actually failed
|
||||
constexpr double notification_throttle_seconds = 0.5;
|
||||
|
||||
void send_error(SOCKET socket, const char *status) {
|
||||
const std::string response =
|
||||
std::string("HTTP/1.0 ") + status + "\r\n"
|
||||
+ cors_header +
|
||||
"Content-Length: 0\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
send_all(socket, response);
|
||||
}
|
||||
|
||||
std::atomic<unsigned short> LISTENING_PORT { 0 };
|
||||
}
|
||||
|
||||
unsigned short stream_server_port() {
|
||||
return LISTENING_PORT.load();
|
||||
}
|
||||
|
||||
StreamServer::StreamServer(unsigned short port)
|
||||
: port(port)
|
||||
{
|
||||
// WinXP builds compile in neither encoder, so there would be nothing to serve and
|
||||
// every request would 404; taking the port instead only invites confused clients
|
||||
if (stream_formats().empty()) {
|
||||
log_warning("api::stream",
|
||||
"this build has no video encoders, the video stream is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->open_listener()) {
|
||||
// the stream was asked for explicitly, so say plainly that it is not there
|
||||
log_warning("api::stream",
|
||||
"the video stream is not available on port {}", this->port);
|
||||
return;
|
||||
}
|
||||
|
||||
this->running = true;
|
||||
this->acceptor = std::thread([this] {
|
||||
this->accept_worker();
|
||||
});
|
||||
|
||||
LISTENING_PORT = this->port;
|
||||
|
||||
// deliberately not logging a full URL; local IPs would leak into shared logs
|
||||
log_info("api::stream", "video stream is listening on port: {}", this->port);
|
||||
log_warning("api::stream",
|
||||
"the video stream is unauthenticated - anyone who can reach port {} can watch "
|
||||
"the game screen", this->port);
|
||||
}
|
||||
|
||||
bool StreamServer::open_listener() {
|
||||
WSADATA wsa_data;
|
||||
const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
|
||||
if (error != 0) {
|
||||
log_warning("api::stream", "WSAStartup() returned {}", error);
|
||||
return false;
|
||||
}
|
||||
this->wsa_started = true;
|
||||
|
||||
this->listener = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (this->listener == INVALID_SOCKET) {
|
||||
log_warning("api::stream", "could not create listener socket: {}",
|
||||
get_last_error_string());
|
||||
return false;
|
||||
}
|
||||
|
||||
int opt_enable = 1;
|
||||
if (setsockopt(this->listener, SOL_SOCKET, SO_REUSEADDR,
|
||||
reinterpret_cast<const char *>(&opt_enable), sizeof(int)) == -1) {
|
||||
log_warning("api::stream", "could not set socket option SO_REUSEADDR: {}",
|
||||
get_last_error_string());
|
||||
}
|
||||
|
||||
sockaddr_in server_address {};
|
||||
server_address.sin_family = AF_INET;
|
||||
server_address.sin_port = htons(this->port);
|
||||
server_address.sin_addr.s_addr = INADDR_ANY;
|
||||
|
||||
if (bind(this->listener, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
|
||||
log_warning("api::stream", "could not bind socket on port {}: {}",
|
||||
this->port, get_last_error_string());
|
||||
closesocket(this->listener);
|
||||
this->listener = INVALID_SOCKET;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(this->listener, server_backlog) == -1) {
|
||||
log_warning("api::stream", "could not listen on port {}: {}",
|
||||
this->port, get_last_error_string());
|
||||
closesocket(this->listener);
|
||||
this->listener = INVALID_SOCKET;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
StreamServer::~StreamServer() {
|
||||
|
||||
this->running = false;
|
||||
LISTENING_PORT = 0;
|
||||
|
||||
if (this->listener != INVALID_SOCKET) {
|
||||
closesocket(this->listener);
|
||||
this->listener = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
// drops the client threads out of their blocking send/recv
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
for (auto &client : this->clients) {
|
||||
if (client.socket != INVALID_SOCKET) {
|
||||
::shutdown(client.socket, SD_BOTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->acceptor.joinable()) {
|
||||
this->acceptor.join();
|
||||
}
|
||||
|
||||
// joining is what guarantees no client thread outlives this object
|
||||
for (auto &client : this->clients) {
|
||||
if (client.thread.joinable()) {
|
||||
client.thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->wsa_started) {
|
||||
WSACleanup();
|
||||
}
|
||||
}
|
||||
|
||||
void StreamServer::accept_worker() {
|
||||
|
||||
while (this->running) {
|
||||
sockaddr_in client_address {};
|
||||
int client_address_size = sizeof(sockaddr_in);
|
||||
|
||||
const SOCKET client = accept(
|
||||
this->listener, (sockaddr *) &client_address, &client_address_size);
|
||||
if (client == INVALID_SOCKET) {
|
||||
// on shutdown the listener is closed under us; otherwise do not spin
|
||||
if (this->running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this->running) {
|
||||
closesocket(client);
|
||||
break;
|
||||
}
|
||||
|
||||
char address_data[INET_ADDRSTRLEN] {};
|
||||
inet_ntop(AF_INET, &client_address.sin_addr, address_data, INET_ADDRSTRLEN);
|
||||
std::string address(address_data);
|
||||
|
||||
// every client costs an encode and real bandwidth, so the cap protects the game
|
||||
int slot = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
for (size_t i = 0; i < this->clients.size(); i++) {
|
||||
if (!this->clients[i].active) {
|
||||
this->clients[i].active = true;
|
||||
this->clients[i].socket = client;
|
||||
slot = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (slot < 0) {
|
||||
log_warning("api::stream", "client limit of {} hit", client_limit);
|
||||
overlay::notifications::add_throttled(
|
||||
overlay::notifications::Severity::Warning,
|
||||
"api::stream.client_limit",
|
||||
notification_throttle_seconds,
|
||||
fmt::format("Video stream refused: client limit reached ({})", address));
|
||||
send_error(client, "503 Service Unavailable");
|
||||
closesocket(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
// this thread is the only one that touches the thread objects, so the slot's
|
||||
// previous occupant gets reaped here rather than being detached
|
||||
if (this->clients[slot].thread.joinable()) {
|
||||
this->clients[slot].thread.join();
|
||||
}
|
||||
|
||||
this->clients[slot].thread = std::thread([this, slot, client, address] {
|
||||
this->client_worker(slot, client, address);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void StreamServer::client_worker(int slot, SOCKET socket, std::string address) {
|
||||
|
||||
DWORD timeout = request_timeout_ms;
|
||||
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO,
|
||||
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
||||
|
||||
timeout = send_timeout_ms;
|
||||
setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO,
|
||||
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
||||
|
||||
int opt_enable = 1;
|
||||
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
|
||||
reinterpret_cast<const char *>(&opt_enable), sizeof(int));
|
||||
|
||||
// whatever sits in the send buffer is already stale, and the default holds about a
|
||||
// third of a second of H.264 because the bitrate is so low. keeping it small makes a
|
||||
// slow reader block the sender, which then skips to the newest frame instead of
|
||||
// handing over a backlog
|
||||
int send_buffer = send_buffer_bytes;
|
||||
setsockopt(socket, SOL_SOCKET, SO_SNDBUF,
|
||||
reinterpret_cast<const char *>(&send_buffer), sizeof(send_buffer));
|
||||
|
||||
HttpRequest request;
|
||||
if (read_request(socket, request_size_limit, request)) {
|
||||
if (request.method != "GET") {
|
||||
send_error(socket, "405 Method Not Allowed");
|
||||
} else {
|
||||
const int fps = query_int(request, "fps", 30, 1, fps_limit);
|
||||
const int quality = query_int(request, "q", 70, 1, 100);
|
||||
|
||||
auto writer = make_stream_writer(request.path, quality, fps);
|
||||
if (!writer) {
|
||||
send_error(socket, "404 Not Found");
|
||||
} else {
|
||||
std::vector<int> screens;
|
||||
graphics_screens_get(screens);
|
||||
|
||||
// registration takes a raw swapchain index and never bounds it, so the
|
||||
// capture range has to be enforced here rather than assumed
|
||||
const auto streamable = [&screens](int screen) {
|
||||
return screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
|
||||
&& std::find(screens.begin(), screens.end(), screen)
|
||||
!= screens.end();
|
||||
};
|
||||
|
||||
// screen 1 is the subscreen in every game that has one; single-screen games
|
||||
// only ever register screen 0, so resolve the default against what exists.
|
||||
// left unclamped so a nonsense screen is reported as what was asked for
|
||||
int screen = query_int(request, "screen", -1, 0,
|
||||
std::numeric_limits<int>::max());
|
||||
if (screen < 0) {
|
||||
screen = streamable(1) ? 1 : 0;
|
||||
}
|
||||
|
||||
// the default always lands on a screen that exists, so this is only ever
|
||||
// an explicit request for one that cannot be captured
|
||||
if (!streamable(screen)) {
|
||||
log_warning("api::stream",
|
||||
"screen {} is not available, refusing {}", screen, address);
|
||||
overlay::notifications::add_throttled(
|
||||
overlay::notifications::Severity::Warning,
|
||||
fmt::format("api::stream.screen_unavailable.{}", screen),
|
||||
notification_throttle_seconds,
|
||||
fmt::format("Video stream refused: screen {} not available ({})",
|
||||
screen, address));
|
||||
send_error(socket, "404 Not Found");
|
||||
} else if (!capture_pump::claim_screen(screen)) {
|
||||
log_warning("api::stream",
|
||||
"screen {} is already being streamed, refusing {}",
|
||||
screen, address);
|
||||
overlay::notifications::add_throttled(
|
||||
overlay::notifications::Severity::Warning,
|
||||
fmt::format("api::stream.screen_claimed.{}", screen),
|
||||
notification_throttle_seconds,
|
||||
fmt::format("Video stream refused: screen {} already streaming ({})",
|
||||
screen, address));
|
||||
send_error(socket, "503 Service Unavailable");
|
||||
} else {
|
||||
log_info("api::stream",
|
||||
"client connected: {} ({}, screen={}, fps={}, quality={})",
|
||||
address, request.path, screen, fps, quality);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
fmt::format("Video stream client connected ({}, screen {})",
|
||||
address, screen));
|
||||
|
||||
const std::string header =
|
||||
"HTTP/1.0 200 OK\r\n"
|
||||
"Connection: close\r\n"
|
||||
+ std::string(cors_header) +
|
||||
"Cache-Control: no-store, no-cache, must-revalidate\r\n"
|
||||
"Pragma: no-cache\r\n"
|
||||
"Content-Type: " + writer->content_type() + "\r\n"
|
||||
"\r\n";
|
||||
|
||||
const StreamSend stream_send = [socket](const void *data, size_t size) {
|
||||
return send_all(socket, data, size);
|
||||
};
|
||||
|
||||
if (send_all(socket, header) && writer->begin(stream_send)) {
|
||||
const auto interval = std::chrono::microseconds(1000000 / fps);
|
||||
|
||||
while (this->running) {
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
|
||||
capture_pump::Frame frame;
|
||||
const bool ok = capture_pump::capture_direct(
|
||||
screen, frame.pixels, 1,
|
||||
&frame.timestamp, &frame.width, &frame.height);
|
||||
|
||||
if (ok && frame.pixels) {
|
||||
if (!writer->write(stream_send, frame)) {
|
||||
break;
|
||||
}
|
||||
} else if (client_gone(socket)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// a failed capture still paces, or a stalled game spins this
|
||||
std::this_thread::sleep_until(started + interval);
|
||||
}
|
||||
}
|
||||
|
||||
capture_pump::release_screen(screen);
|
||||
log_info("api::stream", "client disconnected: {}", address);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Info,
|
||||
fmt::format("Video stream client disconnected ({})", address));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
this->clients[slot].socket = INVALID_SOCKET;
|
||||
this->clients[slot].active = false;
|
||||
}
|
||||
|
||||
closesocket(socket);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <winsock2.h>
|
||||
|
||||
namespace api {
|
||||
|
||||
// 0 while no stream server is listening, so the API can tell clients not to look for one
|
||||
unsigned short stream_server_port();
|
||||
|
||||
class StreamServer {
|
||||
public:
|
||||
|
||||
explicit StreamServer(unsigned short port);
|
||||
~StreamServer();
|
||||
|
||||
StreamServer(const StreamServer &) = delete;
|
||||
StreamServer &operator=(const StreamServer &) = delete;
|
||||
|
||||
private:
|
||||
|
||||
// configuration
|
||||
static constexpr int server_backlog = 4;
|
||||
static constexpr int client_limit = 4;
|
||||
static constexpr int request_size_limit = 8 * 1024;
|
||||
static constexpr int request_timeout_ms = 5000;
|
||||
static constexpr int send_timeout_ms = 5000;
|
||||
// small enough that a low bitrate stream cannot hide a backlog of stale frames in it
|
||||
static constexpr int send_buffer_bytes = 16 * 1024;
|
||||
static constexpr int fps_limit = 60;
|
||||
|
||||
struct Client {
|
||||
std::thread thread;
|
||||
SOCKET socket = INVALID_SOCKET;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
void accept_worker();
|
||||
bool open_listener();
|
||||
void client_worker(int slot, SOCKET socket, std::string address);
|
||||
|
||||
unsigned short port;
|
||||
SOCKET listener = INVALID_SOCKET;
|
||||
bool wsa_started = false;
|
||||
std::atomic_bool running { false };
|
||||
std::thread acceptor;
|
||||
std::mutex clients_m;
|
||||
// socket and active are guarded by clients_m; only the acceptor touches thread
|
||||
std::array<Client, client_limit> clients;
|
||||
};
|
||||
}
|
||||
+547
-137
@@ -1,169 +1,579 @@
|
||||
#define HEADSOCKET_IMPLEMENTATION
|
||||
#include "external/headsocket.h"
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include "websocket.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/rc4.h"
|
||||
#include "util/logging.h"
|
||||
#include "controller.h"
|
||||
|
||||
using namespace headsocket;
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "external/wslay/msvc_compat.h"
|
||||
#include <wslay/wslay.h>
|
||||
|
||||
#include "controller.h"
|
||||
#include "external/hash-library/sha1.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
/*
|
||||
* Client class declaration
|
||||
*/
|
||||
class WebSocketClient : public web_socket_client {
|
||||
namespace {
|
||||
|
||||
// required class header
|
||||
HEADSOCKET_CLIENT(WebSocketClient, web_socket_client);
|
||||
constexpr int server_backlog = 4;
|
||||
constexpr size_t client_limit = 8;
|
||||
|
||||
private:
|
||||
ClientState *state = nullptr;
|
||||
// a peer that connects and then says nothing must not hold a slot forever
|
||||
constexpr int handshake_timeout_ms = 5000;
|
||||
constexpr size_t request_size_limit = 8 * 1024;
|
||||
constexpr uint64_t message_size_limit = 64 * 1024;
|
||||
|
||||
protected:
|
||||
bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) override;
|
||||
// how long a quiet connection waits before the loop rechecks whether we are stopping
|
||||
constexpr int idle_poll_ms = 500;
|
||||
|
||||
void on_accept() override;
|
||||
void on_disconnect() override;
|
||||
};
|
||||
// RFC 6455 appends this to the client key before hashing
|
||||
constexpr const char *websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
/*
|
||||
* Server class declaration
|
||||
*/
|
||||
class WebSocketServer : public web_socket_server<WebSocketClient> {
|
||||
HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
|
||||
public:
|
||||
WebSocketController *websocket;
|
||||
};
|
||||
constexpr double notification_throttle_seconds = 0.5;
|
||||
|
||||
void api::WebSocketServer::init() {}
|
||||
std::string trim(const std::string &text) {
|
||||
const auto begin = text.find_first_not_of(" \t");
|
||||
if (begin == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
return text.substr(begin, text.find_last_not_of(" \t") - begin + 1);
|
||||
}
|
||||
|
||||
std::string to_lower(std::string text) {
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
bool contains_token(const std::string &value, const std::string &expected) {
|
||||
size_t pos = 0;
|
||||
while (pos < value.size()) {
|
||||
const size_t end = value.find(',', pos);
|
||||
if (to_lower(trim(value.substr(pos, end - pos))) == expected) {
|
||||
return true;
|
||||
}
|
||||
if (end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool valid_websocket_key(const std::string &key) {
|
||||
if (key.size() != 24 || key[22] != '=' || key[23] != '=') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string alphabet =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
for (size_t i = 0; i < 22; i++) {
|
||||
if (alphabet.find(key[i]) == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool send_all(SOCKET socket, const std::string &text) {
|
||||
size_t remaining = text.size();
|
||||
const char *cursor = text.data();
|
||||
|
||||
while (remaining > 0) {
|
||||
const int sent = send(socket, cursor, static_cast<int>(remaining), 0);
|
||||
if (sent <= 0) {
|
||||
return false;
|
||||
}
|
||||
cursor += sent;
|
||||
remaining -= static_cast<size_t>(sent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void set_recv_timeout(SOCKET socket, int milliseconds) {
|
||||
const DWORD timeout = static_cast<DWORD>(milliseconds);
|
||||
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO,
|
||||
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
||||
}
|
||||
|
||||
std::string accept_key(const std::string &client_key) {
|
||||
SHA1 sha1;
|
||||
sha1.add(client_key.data(), client_key.size());
|
||||
sha1.add(websocket_guid, strlen(websocket_guid));
|
||||
|
||||
unsigned char digest[SHA1::HashBytes] {};
|
||||
sha1.getHash(digest);
|
||||
|
||||
return crypt::base64_encode(digest, sizeof(digest));
|
||||
}
|
||||
|
||||
bool handshake(SOCKET socket) {
|
||||
std::string request;
|
||||
char byte = 0;
|
||||
bool complete = false;
|
||||
|
||||
while (request.size() < request_size_limit) {
|
||||
const int read = recv(socket, &byte, 1, 0);
|
||||
if (read <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
request.push_back(byte);
|
||||
if (request.size() >= 4
|
||||
&& request.compare(request.size() - 4, 4, "\r\n\r\n") == 0) {
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string key;
|
||||
std::string version;
|
||||
bool connection_upgrade = false;
|
||||
bool upgrade = false;
|
||||
const size_t request_line_end = request.find("\r\n");
|
||||
const std::string request_line = request.substr(0, request_line_end);
|
||||
const size_t target_end = request_line.find(' ', 4);
|
||||
const bool valid_request_line = request_line_end != std::string::npos
|
||||
&& request_line.compare(0, 4, "GET ") == 0
|
||||
&& target_end != std::string::npos
|
||||
&& target_end > 4
|
||||
&& target_end == request_line.rfind(' ')
|
||||
&& request_line.substr(target_end + 1) == "HTTP/1.1";
|
||||
size_t pos = request_line_end;
|
||||
|
||||
while (pos != std::string::npos) {
|
||||
const size_t end = request.find("\r\n", pos + 2);
|
||||
if (end == std::string::npos || end == pos + 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
const std::string line = request.substr(pos + 2, end - pos - 2);
|
||||
const size_t colon = line.find(':');
|
||||
if (colon != std::string::npos) {
|
||||
const std::string name = to_lower(trim(line.substr(0, colon)));
|
||||
const std::string value = trim(line.substr(colon + 1));
|
||||
|
||||
if (name == "sec-websocket-key") {
|
||||
key = value;
|
||||
} else if (name == "sec-websocket-version") {
|
||||
version = value;
|
||||
} else if (name == "connection") {
|
||||
connection_upgrade = connection_upgrade
|
||||
|| contains_token(value, "upgrade");
|
||||
} else if (name == "upgrade") {
|
||||
upgrade = upgrade || contains_token(value, "websocket");
|
||||
}
|
||||
}
|
||||
|
||||
pos = end;
|
||||
}
|
||||
|
||||
if (!complete || !valid_request_line || !connection_upgrade || !upgrade
|
||||
|| version != "13" || !valid_websocket_key(key)) {
|
||||
send_all(socket,
|
||||
"HTTP/1.1 400 Bad Request\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Length: 0\r\n"
|
||||
"\r\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// deliberately echoes back no extension or subprotocol: naming one the client did
|
||||
// not offer is a handshake failure, and none of them are wanted here
|
||||
return send_all(socket,
|
||||
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Accept: " + accept_key(key) + "\r\n"
|
||||
"\r\n");
|
||||
}
|
||||
|
||||
// everything one connection needs; wslay hands this back to the callbacks
|
||||
struct Session {
|
||||
SOCKET socket = INVALID_SOCKET;
|
||||
Controller *controller = nullptr;
|
||||
ClientState *state = nullptr;
|
||||
bool failed = false;
|
||||
};
|
||||
|
||||
ssize_t recv_callback(wslay_event_context_ptr ctx, uint8_t *buffer, size_t length,
|
||||
int flags, void *user_data) {
|
||||
|
||||
(void) flags;
|
||||
|
||||
auto *session = static_cast<Session *>(user_data);
|
||||
const int read = recv(session->socket, reinterpret_cast<char *>(buffer),
|
||||
static_cast<int>(length), 0);
|
||||
|
||||
if (read > 0) {
|
||||
return read;
|
||||
}
|
||||
|
||||
// the socket is non-blocking, so an empty one has to read as "nothing yet"
|
||||
// rather than as a dead peer
|
||||
if (read < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||
return -1;
|
||||
}
|
||||
|
||||
wslay_event_set_error(ctx,
|
||||
read == 0 ? WSLAY_ERR_NO_MORE_MSG : WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t length,
|
||||
int flags, void *user_data) {
|
||||
|
||||
(void) flags;
|
||||
|
||||
auto *session = static_cast<Session *>(user_data);
|
||||
const int sent = send(session->socket, reinterpret_cast<const char *>(data),
|
||||
static_cast<int>(length), 0);
|
||||
|
||||
if (sent > 0) {
|
||||
return sent;
|
||||
}
|
||||
|
||||
if (sent < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||
return -1;
|
||||
}
|
||||
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void on_msg_recv(wslay_event_context_ptr ctx,
|
||||
const struct wslay_event_on_msg_recv_arg *arg, void *user_data) {
|
||||
|
||||
auto *session = static_cast<Session *>(user_data);
|
||||
|
||||
// pings and closes are wslay's business, it answers them itself
|
||||
if (wslay_is_ctrl_frame(arg->opcode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg->opcode != WSLAY_BINARY_FRAME) {
|
||||
log_warning("api::websocket", "ignoring a non-binary message");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<char> in(arg->msg, arg->msg + arg->msg_length);
|
||||
std::vector<char> out;
|
||||
|
||||
if (session->state->cipher) {
|
||||
session->state->cipher->crypt(
|
||||
reinterpret_cast<uint8_t *>(in.data()), in.size());
|
||||
}
|
||||
|
||||
session->controller->process_request(session->state, &in, &out);
|
||||
|
||||
if (session->state->cipher) {
|
||||
session->state->cipher->crypt(
|
||||
reinterpret_cast<uint8_t *>(out.data()), out.size());
|
||||
}
|
||||
|
||||
wslay_event_msg reply {};
|
||||
reply.opcode = WSLAY_BINARY_FRAME;
|
||||
reply.msg = reinterpret_cast<const uint8_t *>(out.data());
|
||||
reply.msg_length = out.size();
|
||||
|
||||
if (wslay_event_queue_msg(ctx, &reply) != 0) {
|
||||
session->failed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Controller::process_password_change(session->state);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Controller state so we don't have to import headsocket stuff in our header
|
||||
*/
|
||||
struct WebSocketControllerState {
|
||||
std::shared_ptr<WebSocketServer> server;
|
||||
struct Client {
|
||||
std::thread thread;
|
||||
SOCKET socket = INVALID_SOCKET;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
Controller *controller = nullptr;
|
||||
unsigned short port = 0;
|
||||
SOCKET listener = INVALID_SOCKET;
|
||||
bool wsa_started = false;
|
||||
std::atomic_bool running { false };
|
||||
std::thread acceptor;
|
||||
std::mutex clients_m;
|
||||
std::array<Client, client_limit> clients;
|
||||
|
||||
bool open_listener();
|
||||
void accept_worker();
|
||||
void client_worker(int slot, SOCKET socket, std::string address);
|
||||
void stop();
|
||||
};
|
||||
|
||||
bool WebSocketControllerState::open_listener() {
|
||||
WSADATA wsa_data;
|
||||
const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
|
||||
if (error != 0) {
|
||||
log_warning("api::websocket", "WSAStartup() returned {}", error);
|
||||
return false;
|
||||
}
|
||||
this->wsa_started = true;
|
||||
|
||||
this->listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (this->listener == INVALID_SOCKET) {
|
||||
log_warning("api::websocket", "socket() returned {}", WSAGetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_in address {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(this->port);
|
||||
|
||||
if (bind(this->listener, reinterpret_cast<sockaddr *>(&address), sizeof(address))
|
||||
== SOCKET_ERROR) {
|
||||
log_warning("api::websocket", "bind() returned {}", WSAGetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(this->listener, server_backlog) == SOCKET_ERROR) {
|
||||
log_warning("api::websocket", "listen() returned {}", WSAGetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketControllerState::accept_worker() {
|
||||
|
||||
while (this->running) {
|
||||
sockaddr_in client_address {};
|
||||
int client_address_size = sizeof(sockaddr_in);
|
||||
|
||||
const SOCKET client = accept(
|
||||
this->listener, reinterpret_cast<sockaddr *>(&client_address),
|
||||
&client_address_size);
|
||||
|
||||
if (client == INVALID_SOCKET) {
|
||||
// on shutdown the listener is closed under us; otherwise do not spin
|
||||
if (this->running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this->running) {
|
||||
closesocket(client);
|
||||
break;
|
||||
}
|
||||
|
||||
// formatted by hand rather than with inet_ntop, which needs a newer Windows than
|
||||
// the XP toolchain targets, or inet_ntoa, which answers from a shared buffer
|
||||
const uint32_t raw = ntohl(client_address.sin_addr.s_addr);
|
||||
const std::string address = fmt::format("{}.{}.{}.{}",
|
||||
(raw >> 24) & 0xff, (raw >> 16) & 0xff, (raw >> 8) & 0xff, raw & 0xff);
|
||||
|
||||
int slot = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
for (size_t i = 0; i < this->clients.size(); i++) {
|
||||
if (!this->clients[i].active) {
|
||||
this->clients[i].active = true;
|
||||
this->clients[i].socket = client;
|
||||
slot = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (slot < 0) {
|
||||
log_warning("api::websocket", "client limit of {} hit", client_limit);
|
||||
overlay::notifications::add_throttled(
|
||||
overlay::notifications::Severity::Warning,
|
||||
"api::websocket.client_limit",
|
||||
notification_throttle_seconds,
|
||||
fmt::format("API websocket refused: client limit reached ({})", address));
|
||||
closesocket(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
// this thread is the only one that touches the thread objects, so the slot's
|
||||
// previous occupant gets reaped here rather than being detached
|
||||
if (this->clients[slot].thread.joinable()) {
|
||||
this->clients[slot].thread.join();
|
||||
}
|
||||
|
||||
// the handshake runs on the client thread on purpose: doing it here would put
|
||||
// every later connection behind whatever this one is waiting for
|
||||
this->clients[slot].thread = std::thread([this, slot, client, address] {
|
||||
this->client_worker(slot, client, address);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketControllerState::client_worker(int slot, SOCKET socket, std::string address) {
|
||||
|
||||
// wslay writes a frame header and its payload as separate sends, so leaving Nagle on
|
||||
// holds the payload back until the peer acknowledges the header, costing a delayed
|
||||
// ack per message; requests here are small and latency sensitive
|
||||
int nodelay = 1;
|
||||
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
|
||||
reinterpret_cast<const char *>(&nodelay), sizeof(nodelay));
|
||||
|
||||
set_recv_timeout(socket, handshake_timeout_ms);
|
||||
|
||||
if (handshake(socket)) {
|
||||
// wslay reads until the socket would block, so leaving it blocking would make
|
||||
// every reply wait out a receive timeout before the send got a turn
|
||||
u_long non_blocking = 1;
|
||||
ioctlsocket(socket, FIONBIO, &non_blocking);
|
||||
|
||||
Session session;
|
||||
session.socket = socket;
|
||||
session.controller = this->controller;
|
||||
session.state = new ClientState();
|
||||
this->controller->init_state(session.state);
|
||||
|
||||
log_info("api::websocket", "client connected: {}", address);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
fmt::format("API websocket client connected ({})", address));
|
||||
|
||||
wslay_event_callbacks callbacks {};
|
||||
callbacks.recv_callback = recv_callback;
|
||||
callbacks.send_callback = send_callback;
|
||||
callbacks.on_msg_recv_callback = on_msg_recv;
|
||||
|
||||
wslay_event_context_ptr ctx = nullptr;
|
||||
if (wslay_event_context_server_init(&ctx, &callbacks, &session) == 0) {
|
||||
wslay_event_config_set_max_recv_msg_length(ctx, message_size_limit);
|
||||
while (this->running && !session.failed
|
||||
&& (wslay_event_want_read(ctx) || wslay_event_want_write(ctx))) {
|
||||
|
||||
fd_set read_set;
|
||||
fd_set write_set;
|
||||
FD_ZERO(&read_set);
|
||||
FD_ZERO(&write_set);
|
||||
|
||||
if (wslay_event_want_read(ctx)) {
|
||||
FD_SET(socket, &read_set);
|
||||
}
|
||||
if (wslay_event_want_write(ctx)) {
|
||||
FD_SET(socket, &write_set);
|
||||
}
|
||||
|
||||
// bounded so a silent connection still notices us shutting down
|
||||
timeval timeout {};
|
||||
timeout.tv_usec = idle_poll_ms * 1000;
|
||||
|
||||
if (select(0, &read_set, &write_set, nullptr, &timeout) < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (FD_ISSET(socket, &read_set) && wslay_event_recv(ctx) != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// unconditional, so a reply queued by the read above goes out now
|
||||
if (wslay_event_send(ctx) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wslay_event_context_free(ctx);
|
||||
}
|
||||
|
||||
Controller::free_state(session.state);
|
||||
delete session.state;
|
||||
|
||||
log_info("api::websocket", "client disconnected: {}", address);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Info,
|
||||
fmt::format("API websocket client disconnected ({})", address));
|
||||
}
|
||||
|
||||
closesocket(socket);
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
this->clients[slot].socket = INVALID_SOCKET;
|
||||
this->clients[slot].active = false;
|
||||
}
|
||||
|
||||
void WebSocketControllerState::stop() {
|
||||
this->running = false;
|
||||
|
||||
if (this->listener != INVALID_SOCKET) {
|
||||
closesocket(this->listener);
|
||||
this->listener = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
// drops the client threads out of their blocking send/recv
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
for (auto &client : this->clients) {
|
||||
if (client.socket != INVALID_SOCKET) {
|
||||
::shutdown(client.socket, SD_BOTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->acceptor.joinable()) {
|
||||
this->acceptor.join();
|
||||
}
|
||||
|
||||
// joining is what guarantees no client thread outlives this object
|
||||
for (auto &client : this->clients) {
|
||||
if (client.thread.joinable()) {
|
||||
client.thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->wsa_started) {
|
||||
WSACleanup();
|
||||
this->wsa_started = false;
|
||||
}
|
||||
}
|
||||
|
||||
WebSocketController::WebSocketController(Controller *controller, uint16_t port) {
|
||||
this->controller = controller;
|
||||
|
||||
// create state
|
||||
this->state = new WebSocketControllerState();
|
||||
this->state->controller = controller;
|
||||
this->state->port = port;
|
||||
|
||||
// start server
|
||||
this->state->server = WebSocketServer::create(port);
|
||||
this->state->server->websocket = this;
|
||||
if (this->state->server->is_running()) {
|
||||
log_info("api::websocket", "server listening on port: {}", port);
|
||||
} else {
|
||||
if (!this->state->open_listener()) {
|
||||
log_warning("api::websocket", "server failed to listen on port: {}", port);
|
||||
return;
|
||||
}
|
||||
|
||||
this->state->running = true;
|
||||
this->state->acceptor = std::thread([this] {
|
||||
this->state->accept_worker();
|
||||
});
|
||||
|
||||
log_info("api::websocket", "server listening on port: {}", port);
|
||||
}
|
||||
|
||||
WebSocketController::~WebSocketController() {
|
||||
|
||||
// stop server
|
||||
this->state->server->stop();
|
||||
|
||||
// delete state
|
||||
this->state->stop();
|
||||
delete this->state;
|
||||
this->state = nullptr;
|
||||
}
|
||||
|
||||
void WebSocketController::free_socket() {
|
||||
this->state->server->stop();
|
||||
}
|
||||
|
||||
void WebSocketClient::on_accept() {
|
||||
web_socket_client::on_accept();
|
||||
|
||||
// get pointer to server
|
||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||
if (!srv || !srv->websocket) {
|
||||
log_fatal("api::websocket", "on_accept has no server");
|
||||
}
|
||||
|
||||
// check for init
|
||||
state = new ClientState();
|
||||
srv->websocket->controller->init_state(state);
|
||||
|
||||
// log connection
|
||||
log_info("api::websocket", "client connected");
|
||||
}
|
||||
|
||||
void WebSocketClient::on_disconnect() {
|
||||
|
||||
// log disconnection
|
||||
log_info("api::websocket", "client disconnected");
|
||||
|
||||
// get pointer to server
|
||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||
if (!srv || !srv->websocket) {
|
||||
log_fatal("api::websocket", "on_disconnect has no server");
|
||||
}
|
||||
|
||||
// clean up state
|
||||
srv->websocket->controller->free_state(state);
|
||||
delete state;
|
||||
state = nullptr;
|
||||
|
||||
// call super
|
||||
web_socket_client::on_disconnect();
|
||||
}
|
||||
|
||||
/*
|
||||
* This is where business actually happens, gets called on every datablock receive
|
||||
*/
|
||||
bool WebSocketClient::async_received_data(const data_block &db, uint8_t *ptr, size_t length) {
|
||||
|
||||
// get pointer to server
|
||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||
if (!srv || !srv->websocket) {
|
||||
log_fatal("api::websocket", "received datablock without server");
|
||||
}
|
||||
|
||||
// check state
|
||||
if (!state) {
|
||||
log_fatal("api::websocket", "client with no state received datablock");
|
||||
}
|
||||
|
||||
// check datablock type
|
||||
switch (db.op) {
|
||||
case opcode::binary: {
|
||||
|
||||
// allocate buffers
|
||||
std::vector<char> in(ptr, ptr + length);
|
||||
std::vector<char> out;
|
||||
|
||||
// crypt in-data
|
||||
if (state->cipher) {
|
||||
state->cipher->crypt(reinterpret_cast<uint8_t *>(in.data()), in.size());
|
||||
}
|
||||
|
||||
// process request
|
||||
srv->websocket->controller->process_request(state, &in, &out);
|
||||
|
||||
// crypt out-data
|
||||
if (state->cipher) {
|
||||
state->cipher->crypt(reinterpret_cast<uint8_t *>(out.data()), out.size());
|
||||
}
|
||||
|
||||
// send answer
|
||||
push(out.data(), out.size());
|
||||
|
||||
// check for password change
|
||||
srv->websocket->controller->process_password_change(state);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
log_warning("api::websocket", "datablock received with non-binary type");
|
||||
break;
|
||||
}
|
||||
|
||||
// always consume the datablock, nomnom
|
||||
return true;
|
||||
this->state->stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#include "external/robin_hood.h"
|
||||
#include "games/popn/popn.h"
|
||||
#include "launcher/logger.h"
|
||||
#include "launcher/signal.h"
|
||||
#include "util/deferlog.h"
|
||||
@@ -1113,6 +1114,7 @@ namespace avs {
|
||||
#endif
|
||||
// jubeat
|
||||
{"jubeat.dll", 0x2000000},
|
||||
{"jubeat2019.dll", 0x10000000},
|
||||
|
||||
// MUSECA
|
||||
{"museca.dll", 0xC000000},
|
||||
@@ -1179,6 +1181,15 @@ namespace avs {
|
||||
return;
|
||||
}
|
||||
|
||||
// hack, both hello! popn and popn hc using popn.dll
|
||||
if (games::popn::is_pikapika_model()) {
|
||||
auto old_size = HEAP_SIZE;
|
||||
HEAP_SIZE = 0x20000000;
|
||||
DEFAULT_HEAP_SIZE_SET = true;
|
||||
log_info("avs-core", "updated heap size: {} -> {}", old_size, avs::core::HEAP_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HEAP_SIZE_DEFAULTS.find(dll_name) != HEAP_SIZE_DEFAULTS.end()) {
|
||||
auto old_size = HEAP_SIZE;
|
||||
|
||||
@@ -1506,7 +1517,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);
|
||||
}
|
||||
@@ -1747,8 +1758,8 @@ 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()),
|
||||
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"
|
||||
});
|
||||
}
|
||||
@@ -1770,14 +1781,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());
|
||||
}
|
||||
}
|
||||
@@ -1911,6 +1922,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(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");
|
||||
|
||||
@@ -151,7 +151,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 ea3 DLL on disk (avs2-ea3.dll OR {})", DLL_NAME);
|
||||
@@ -217,8 +217,10 @@ namespace avs {
|
||||
ea3_config_name = "prop/eamuse-config.xml";
|
||||
}
|
||||
|
||||
if (avs::core::file_exists(ea3_config_name)) {
|
||||
log_info("avs-ea3", "booting (using {})", ea3_config_name);
|
||||
if (fileutils::file_exists(ea3_config_name)) {
|
||||
log_info("avs-ea3", "found {} on disk", ea3_config_name);
|
||||
} else if (CFG_PATH.size()) {
|
||||
log_fatal("avs-ea3", "user-specified ea3 config file is missing: {}", ea3_config_name);
|
||||
} else {
|
||||
log_warning("avs-ea3", "looked for the following files in order:");
|
||||
log_warning("avs-ea3", " * prop/ea3-config.xml");
|
||||
@@ -388,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
|
||||
@@ -589,6 +591,7 @@ namespace avs {
|
||||
// for proper reporting of Omnimix and other song packs
|
||||
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
|
||||
_stricmp(EA3_MODEL, "L44") == 0 ||
|
||||
_stricmp(EA3_MODEL, "T44") == 0 ||
|
||||
_stricmp(EA3_MODEL, "M39") == 0 ||
|
||||
_stricmp(EA3_MODEL, "KFC") == 0)
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<dpiAware>true</dpiAware>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<dependency>
|
||||
@@ -23,7 +24,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
+145
-18
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# set to EN-US for consistency
|
||||
LANG=en_us_8859_1
|
||||
@@ -43,10 +43,16 @@ GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2> /dev/null || echo "none")
|
||||
GIT_HEAD=$(git rev-parse HEAD || echo "none")
|
||||
TOOLCHAIN_32="${TOOLCHAIN_32:-"/usr/share/mingw/toolchain-i686-w64-mingw32.cmake"}"
|
||||
TOOLCHAIN_64="${TOOLCHAIN_64:-"/usr/share/mingw/toolchain-x86_64-w64-mingw32.cmake"}"
|
||||
TOOLCHAIN_WINXP_32="${TOOLCHAIN_WINXP_32:-"/opt/llvm-mingw-xp/toolchain-files/cmake/i686-mingw32-clang.cmake"}"
|
||||
TOOLCHAIN_WINXP_64="${TOOLCHAIN_WINXP_64:-"/opt/llvm-mingw-xp/toolchain-files/cmake/x86_64-mingw32-clang.cmake"}"
|
||||
BUILDDIR_32_RELEASE="./cmake-build-release-32"
|
||||
BUILDDIR_32_DEBUG="./cmake-build-debug-32"
|
||||
BUILDDIR_64_RELEASE="./cmake-build-release-64"
|
||||
BUILDDIR_64_DEBUG="./cmake-build-debug-64"
|
||||
BUILDDIR_WINXP_32_RELEASE="./cmake-build-release-winxp-32"
|
||||
BUILDDIR_WINXP_32_DEBUG="./cmake-build-debug-winxp-32"
|
||||
BUILDDIR_WINXP_64_RELEASE="./cmake-build-release-winxp-64"
|
||||
BUILDDIR_WINXP_64_DEBUG="./cmake-build-debug-winxp-64"
|
||||
DEBUG=0
|
||||
OUTDIR="./bin/spice2x"
|
||||
OUTDIR_EXTRAS="./bin/spice2x/extras"
|
||||
@@ -62,21 +68,44 @@ 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"
|
||||
|
||||
# determine build type
|
||||
BUILD_TYPE="Release"
|
||||
BUILDDIR_32=${BUILDDIR_32_RELEASE}
|
||||
BUILDDIR_64=${BUILDDIR_64_RELEASE}
|
||||
BUILDDIR_WINXP_32=${BUILDDIR_WINXP_32_RELEASE}
|
||||
BUILDDIR_WINXP_64=${BUILDDIR_WINXP_64_RELEASE}
|
||||
if ((DEBUG > 0))
|
||||
then
|
||||
BUILD_TYPE="Debug"
|
||||
BUILDDIR_32=${BUILDDIR_32_DEBUG}
|
||||
BUILDDIR_64=${BUILDDIR_64_DEBUG}
|
||||
BUILDDIR_WINXP_32=${BUILDDIR_WINXP_32_DEBUG}
|
||||
BUILDDIR_WINXP_64=${BUILDDIR_WINXP_64_DEBUG}
|
||||
DIST_NAME=$(echo "${DIST_NAME}" | sed 's/\.[^.]*$/-dbg&/')
|
||||
fi
|
||||
|
||||
# is the XP-compatible toolchain installed?
|
||||
XP_MUST_BUILD=0
|
||||
# 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 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)
|
||||
|
||||
@@ -90,6 +119,18 @@ 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_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 64bit builds disabled, skipping WinXP 64bit builds"
|
||||
fi
|
||||
echo "Distribution Name: $DIST_NAME"
|
||||
echo "Build Type: $BUILD_TYPE"
|
||||
echo "Cores: $CORES"
|
||||
@@ -130,11 +171,84 @@ time (
|
||||
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_64}
|
||||
popd > /dev/null
|
||||
|
||||
if ((BUILD_XP_32 > 0))
|
||||
then
|
||||
# 32 bit Windows XP
|
||||
echo ""
|
||||
echo "Building 32bit targets (WinXP toolchain)..."
|
||||
echo "========================="
|
||||
if ((CLEAN_BUILD > 0))
|
||||
then
|
||||
rm -rf ${BUILDDIR_WINXP_32}
|
||||
fi
|
||||
mkdir -p ${BUILDDIR_WINXP_32}
|
||||
pushd ${BUILDDIR_WINXP_32} > /dev/null
|
||||
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)..."
|
||||
echo "========================="
|
||||
if ((CLEAN_BUILD > 0))
|
||||
then
|
||||
rm -rf ${BUILDDIR_WINXP_64}
|
||||
fi
|
||||
mkdir -p ${BUILDDIR_WINXP_64}
|
||||
pushd ${BUILDDIR_WINXP_64} > /dev/null
|
||||
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 64bit builds"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Compilation process done :)"
|
||||
echo "==========================="
|
||||
)
|
||||
|
||||
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..."
|
||||
echo "============================="
|
||||
if ! command -v windows_dll_compat_checker &> /dev/null; then
|
||||
echo "WARNING: windows_dll_compat_checker not found, skipping XP compatibility check"
|
||||
else
|
||||
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
|
||||
|
||||
# generate PDBs
|
||||
if false # ((DEBUG > 0))
|
||||
then
|
||||
@@ -191,6 +305,13 @@ rm -rf ${OUTDIR_EXTRAS}
|
||||
mkdir -p ${OUTDIR_EXTRAS}
|
||||
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
|
||||
mkdir -p ${OUTDIR_EXTRAS}/linux
|
||||
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
|
||||
|
||||
if false # ((DEBUG > 0))
|
||||
then
|
||||
@@ -199,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
|
||||
@@ -216,16 +329,24 @@ 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
|
||||
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
|
||||
|
||||
# pack source files to output directory
|
||||
@@ -238,22 +359,28 @@ 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
|
||||
echo "Building dist..."
|
||||
mkdir -p ${DIST_FOLDER}
|
||||
rm -rf "${DIST_FOLDER}/${DIST_NAME}"
|
||||
pushd ${OUTDIR}/.. > /dev/null
|
||||
zip -qrXT9 "$OLDPWD/${DIST_FOLDER}/${DIST_NAME}" spice2x -x "spice2x/extras/*" -z <<< "$DIST_COMMENT"
|
||||
pushd ${OUTDIR} > /dev/null
|
||||
zip -qrXT9 "$OLDPWD/${DIST_FOLDER}/${DIST_NAME}" . -x "extras/*" -z <<< "$DIST_COMMENT"
|
||||
echo "Building extras..."
|
||||
zip -qrXT9 "$OLDPWD/${DIST_FOLDER}/${DIST_NAME_EXTRAS}" spice2x -z <<< "$DIST_COMMENT"
|
||||
zip -qrXT9 "$OLDPWD/${DIST_FOLDER}/${DIST_NAME_EXTRAS}" . -z <<< "$DIST_COMMENT"
|
||||
popd > /dev/null
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
docker build --pull "$PWD/external/docker" -t spicetools/deps --platform linux/x86_64
|
||||
docker build --build-context gitroot="$PWD/../../.git" . -t spicetools/spice:latest
|
||||
docker run --rm -v "$PWD/dist:/src/src/spice2x/dist" -v "$PWD/bin:/src/src/spice2x/bin" -v "$PWD/.ccache:/src/src/spice2x/.ccache" spicetools/spice "$@"
|
||||
|
||||
# Interactive TTY if available, so docker build can be Ctrl+C'd
|
||||
DOCKER_FLAGS=""
|
||||
[ -t 0 ] && DOCKER_FLAGS="-it"
|
||||
|
||||
docker run $DOCKER_FLAGS --rm -v "$PWD/dist:/src/src/spice2x/dist" -v "$PWD/bin:/src/src/spice2x/bin" -v "$PWD/.ccache:/src/src/spice2x/.ccache" spicetools/spice "$@"
|
||||
|
||||
@@ -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"
|
||||
+105
-14
@@ -90,6 +90,10 @@ std::string Analog::getDisplayString(rawinput::RawInputManager *manager) {
|
||||
return "MIDI Unknown Index " + indexString + " (" + device->desc + ")";
|
||||
}
|
||||
}
|
||||
case rawinput::XINPUT_GAMEPAD:
|
||||
return fmt::format("{} ({})",
|
||||
xinput::get_analog_string(static_cast<xinput::XInputAnalogEnum>(index)),
|
||||
device->desc);
|
||||
case rawinput::DESTROYED:
|
||||
return "Device unplugged (" + indexString + ")";
|
||||
default:
|
||||
@@ -203,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;
|
||||
@@ -251,7 +271,7 @@ float Analog::applyDeadzone(float raw_value) {
|
||||
}
|
||||
}
|
||||
|
||||
} else if (deadzone < 0) {
|
||||
} else {
|
||||
|
||||
// invert for mirror
|
||||
if (this->getDeadzoneMirror()) {
|
||||
@@ -259,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;
|
||||
}
|
||||
@@ -272,3 +292,74 @@ 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
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
+233
-74
@@ -1,8 +1,11 @@
|
||||
#include "api.h"
|
||||
|
||||
#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"
|
||||
@@ -61,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) {
|
||||
@@ -75,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;
|
||||
@@ -198,9 +254,19 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
if (vKey < value_states->size()) {
|
||||
auto value = value_states->at(vKey);
|
||||
if (current_button->getAnalogType() == BAT_POSITIVE) {
|
||||
state = value > 0.6f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
float threshold = 0.6f;
|
||||
if (current_button->getBatThreshold() > 0) {
|
||||
threshold = std::clamp(current_button->getBatThreshold() / 100.f, 0.01f, 0.99f);
|
||||
}
|
||||
state = value > threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
|
||||
} else if (current_button->getAnalogType() == BAT_NEGATIVE) {
|
||||
state = value < 0.4f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
float threshold = 0.4f;
|
||||
if (current_button->getBatThreshold() > 0) {
|
||||
threshold = std::clamp((100 - current_button->getBatThreshold()) / 100.f, 0.01f, 0.99f);
|
||||
}
|
||||
state = value < threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
|
||||
} else {
|
||||
state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
}
|
||||
@@ -389,7 +455,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
}
|
||||
case rawinput::PIUIO_DEVICE: {
|
||||
state = device->piuioDev->IsPressed(vKey) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
break;
|
||||
}
|
||||
case rawinput::XINPUT_GAMEPAD: {
|
||||
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
|
||||
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
|
||||
state = manager->XINPUT_MGR->is_button_pressed(
|
||||
player,
|
||||
static_cast<xinput::XInputButtonEnum>(vKey)) ?
|
||||
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;
|
||||
}
|
||||
@@ -442,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);
|
||||
@@ -457,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) {
|
||||
@@ -586,7 +668,7 @@ float Buttons::getVelocity(std::unique_ptr<rawinput::RawInputManager> &manager,
|
||||
}
|
||||
}
|
||||
|
||||
float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
|
||||
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::Device *device, Analog &analog) {
|
||||
float value = 0.5f;
|
||||
if (!device) {
|
||||
return value;
|
||||
@@ -658,81 +740,98 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -767,6 +866,18 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case rawinput::XINPUT_GAMEPAD: {
|
||||
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
|
||||
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
|
||||
value = manager->XINPUT_MGR->get_analog_state(
|
||||
player,
|
||||
static_cast<xinput::XInputAnalogEnum>(index));
|
||||
// invert value
|
||||
if (inverted) {
|
||||
value = 1.f - value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -806,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
|
||||
@@ -822,7 +969,7 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &ana
|
||||
return analog.getLastState();
|
||||
}
|
||||
|
||||
float state = getState(device, analog);
|
||||
float state = getState(manager, device, analog);
|
||||
analog.setLastState(state);
|
||||
|
||||
return state;
|
||||
@@ -904,7 +1051,7 @@ void GameAPI::Lights::sortLightsWithCategory(
|
||||
}
|
||||
|
||||
|
||||
void GameAPI::Lights::writeLight(rawinput::Device *device, int index, float value) {
|
||||
void GameAPI::Lights::writeLight(rawinput::RawInputManager *manager, rawinput::Device *device, int index, float value) {
|
||||
|
||||
// check device
|
||||
if (!device) {
|
||||
@@ -991,6 +1138,18 @@ void GameAPI::Lights::writeLight(rawinput::Device *device, int index, float valu
|
||||
}
|
||||
break;
|
||||
}
|
||||
case rawinput::XINPUT_GAMEPAD: {
|
||||
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
|
||||
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
|
||||
if (index < static_cast<int>(xinput::XInputOutputEnum::COUNT)) {
|
||||
manager->XINPUT_MGR->set_output_state(
|
||||
player, static_cast<xinput::XInputOutputEnum>(index), value);
|
||||
// no need to set output_pending; xinput output handled immediately
|
||||
} else {
|
||||
log_warning("api", "invalid xinput light index: {}", index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1016,9 +1175,9 @@ void GameAPI::Lights::writeLight(rawinput::RawInputManager *manager, Light &ligh
|
||||
|
||||
// write state
|
||||
if (light.override_enabled) {
|
||||
writeLight(device, light.getIndex(), light.override_state);
|
||||
writeLight(manager, device, light.getIndex(), light.override_state);
|
||||
} else {
|
||||
writeLight(device, light.getIndex(), value);
|
||||
writeLight(manager, device, light.getIndex(), value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-9
@@ -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) {
|
||||
@@ -103,7 +104,7 @@ namespace GameAPI {
|
||||
}
|
||||
}
|
||||
|
||||
float getState(rawinput::Device *device, Analog &analog);
|
||||
float getState(rawinput::RawInputManager *manager, rawinput::Device *device, Analog &analog);
|
||||
float getState(rawinput::RawInputManager *manager, Analog &analog);
|
||||
float getState(std::unique_ptr<rawinput::RawInputManager> &manager, Analog &analog);
|
||||
}
|
||||
@@ -135,7 +136,7 @@ namespace GameAPI {
|
||||
|
||||
void sortLightsWithCategory(std::vector<Light> *lights, const std::initializer_list<LightAndCategory> list);
|
||||
|
||||
void writeLight(rawinput::Device *device, int index, float value);
|
||||
void writeLight(rawinput::RawInputManager *manager, rawinput::Device *device, int index, float value);
|
||||
void writeLight(rawinput::RawInputManager *manager, Light &light, float value);
|
||||
void writeLight(std::unique_ptr<rawinput::RawInputManager> &manager, Light &light, float value);
|
||||
|
||||
|
||||
@@ -278,6 +278,8 @@ std::string Button::getVKeyString() {
|
||||
return "Left Alt";
|
||||
case 0xA5:
|
||||
return "Right Alt";
|
||||
case INVALID_VKEY:
|
||||
return "Invalid";
|
||||
default:
|
||||
|
||||
// check win API
|
||||
@@ -297,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 "";
|
||||
}
|
||||
|
||||
@@ -415,6 +440,10 @@ std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
|
||||
}
|
||||
case rawinput::PIUIO_DEVICE:
|
||||
return "PIUIO " + vKeyString;
|
||||
case rawinput::XINPUT_GAMEPAD:
|
||||
return fmt::format("{} ({})",
|
||||
xinput::get_button_string(static_cast<xinput::XInputButtonEnum>(vKey)),
|
||||
device->desc);
|
||||
case rawinput::DESTROYED:
|
||||
return "Device unplugged (" + vKeyString + ")";
|
||||
default:
|
||||
|
||||
@@ -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;
|
||||
@@ -56,6 +57,7 @@ private:
|
||||
ButtonAnalogType analog_type = BAT_NONE;
|
||||
double debounce_up = 0.0;
|
||||
double debounce_down = 0.0;
|
||||
int bat_threshold = 0; // for positive/negative only, value in percentage, zero is default
|
||||
bool invert = false;
|
||||
bool is_temporary = false;
|
||||
|
||||
@@ -64,6 +66,7 @@ private:
|
||||
unsigned short velocity_threshold = 0;
|
||||
|
||||
std::string getMidiNoteString();
|
||||
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
|
||||
|
||||
public:
|
||||
std::string getVKeyString();
|
||||
@@ -83,12 +86,20 @@ public:
|
||||
if (this->override_enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// has a device
|
||||
if (!this->device_identifier.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// naive, and has a valid vKey
|
||||
if (this->vKey != INVALID_VKEY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (auto &alternative : this->alternatives) {
|
||||
if (alternative.vKey != INVALID_VKEY) {
|
||||
if (!alternative.getDeviceIdentifier().empty() ||
|
||||
alternative.getVKey() != INVALID_VKEY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -101,6 +112,8 @@ public:
|
||||
alternatives.clear();
|
||||
device_identifier = "";
|
||||
analog_type = BAT_NONE;
|
||||
bat_threshold = 0;
|
||||
modifier_mask = 0;
|
||||
}
|
||||
|
||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||
@@ -113,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;
|
||||
}
|
||||
@@ -161,6 +182,14 @@ public:
|
||||
this->debounce_down = debounce_time_down;
|
||||
}
|
||||
|
||||
inline void setBatThreshold(int bat_threshold) {
|
||||
this->bat_threshold = bat_threshold;
|
||||
}
|
||||
|
||||
inline int getBatThreshold() const {
|
||||
return this->bat_threshold;
|
||||
}
|
||||
|
||||
inline bool getInvert() const {
|
||||
return this->invert;
|
||||
}
|
||||
|
||||
+70
-24
@@ -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");
|
||||
|
||||
@@ -162,15 +162,19 @@ bool Config::addGame(Game &game) {
|
||||
auto analogType = (int) BAT_NONE;
|
||||
double debounce_up = 0.0;
|
||||
double debounce_down = 0.0;
|
||||
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);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
|
||||
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");
|
||||
@@ -180,16 +184,20 @@ bool Config::addGame(Game &game) {
|
||||
gameButtonNode->SetAttribute("devid", button->getDeviceIdentifier().c_str());
|
||||
gameButtonNode->SetAttribute("debounce_up", debounce_up);
|
||||
gameButtonNode->SetAttribute("debounce_down", debounce_down);
|
||||
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));
|
||||
button->setAnalogType((ButtonAnalogType) analogType);
|
||||
button->setDebounceUp(debounce_up);
|
||||
button->setDebounceDown(debounce_down);
|
||||
button->setBatThreshold(bat_threshold);
|
||||
button->setVelocityThreshold(velocity_threshold);
|
||||
button->setInvert(invert);
|
||||
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button->setDeviceIdentifier(devid);
|
||||
}
|
||||
@@ -207,8 +215,10 @@ bool Config::addGame(Game &game) {
|
||||
gameButtonNode->SetAttribute("analogtype", (int) it.getAnalogType());
|
||||
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
|
||||
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
|
||||
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);
|
||||
}
|
||||
@@ -248,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);
|
||||
@@ -257,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) {
|
||||
@@ -273,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));
|
||||
@@ -285,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");
|
||||
@@ -298,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);
|
||||
}
|
||||
@@ -432,8 +442,10 @@ bool Config::addGame(Game &game) {
|
||||
gameButtonNode->SetAttribute("analogtype", it.getAnalogType());
|
||||
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
|
||||
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
|
||||
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);
|
||||
}
|
||||
@@ -452,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);
|
||||
}
|
||||
|
||||
@@ -480,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
|
||||
@@ -535,9 +554,11 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("analogtype", (int) button.getAnalogType());
|
||||
gameButtonNode->SetAttribute("debounce_up", button.getDebounceUp());
|
||||
gameButtonNode->SetAttribute("debounce_down", button.getDebounceDown());
|
||||
gameButtonNode->SetAttribute("bat_threshold", button.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", button.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
||||
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -551,9 +572,11 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("analogtype", 0);
|
||||
gameButtonNode->SetAttribute("debounce_up", 0.0);
|
||||
gameButtonNode->SetAttribute("debounce_down", 0.0);
|
||||
gameButtonNode->SetAttribute("bat_threshold", 0);
|
||||
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
||||
gameButtonNode->SetAttribute("invert", false);
|
||||
gameButtonNode->SetAttribute("devid", "");
|
||||
gameButtonNode->SetAttribute("modifiers", 0);
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
}
|
||||
@@ -684,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");
|
||||
@@ -696,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);
|
||||
}
|
||||
@@ -946,14 +969,18 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
auto analogType = (int) BAT_NONE;
|
||||
double debounce_up = 0.0;
|
||||
double debounce_down = 0.0;
|
||||
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);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
|
||||
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
|
||||
@@ -966,8 +993,10 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
alt.setAnalogType((ButtonAnalogType) analogType);
|
||||
alt.setDebounceUp(debounce_up);
|
||||
alt.setDebounceDown(debounce_down);
|
||||
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));
|
||||
}
|
||||
@@ -984,8 +1013,10 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
button.setAnalogType((ButtonAnalogType) analogType);
|
||||
button.setDebounceUp(debounce_up);
|
||||
button.setDebounceDown(debounce_down);
|
||||
button.setBatThreshold(bat_threshold);
|
||||
button.setVelocityThreshold(velocity_threshold);
|
||||
button.setInvert(invert);
|
||||
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button.setDeviceIdentifier(devid);
|
||||
}
|
||||
@@ -1130,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);
|
||||
@@ -1139,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
|
||||
@@ -1152,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);
|
||||
}
|
||||
@@ -1312,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());
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace cfg {
|
||||
// globals
|
||||
extern bool CONFIGURATOR_STANDALONE;
|
||||
extern ConfigType CONFIGURATOR_TYPE;
|
||||
extern bool CONFIGURATOR_FORCE_SOFTWARE_RENDER;
|
||||
|
||||
class Configurator {
|
||||
private:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ namespace overlay::windows {
|
||||
el->SetAttribute("invert", entry.invert);
|
||||
el->SetAttribute("debounce_up", entry.debounce_up);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -49,6 +51,14 @@ namespace overlay::windows {
|
||||
el->QueryIntAttribute("velocity_threshold", &vel);
|
||||
entry.velocity_threshold = (unsigned short)vel;
|
||||
|
||||
int bat = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -65,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);
|
||||
}
|
||||
|
||||
@@ -88,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;
|
||||
}
|
||||
@@ -155,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) {
|
||||
@@ -239,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) {
|
||||
@@ -295,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");
|
||||
@@ -354,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;
|
||||
}
|
||||
|
||||
@@ -392,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,15 @@ std::string Light::getDisplayString(rawinput::RawInputManager* manager) {
|
||||
|
||||
return "Invalid SMX Dedicab Light (" + index_string + ")";
|
||||
}
|
||||
case rawinput::XINPUT_GAMEPAD: {
|
||||
if (index < static_cast<size_t>(xinput::XInputOutputEnum::COUNT)) {
|
||||
return fmt::format("{} ({})",
|
||||
xinput::get_output_string(static_cast<xinput::XInputOutputEnum>(index)),
|
||||
device->desc);
|
||||
}
|
||||
|
||||
return "Invalid XInput Gamepad Light (" + index_string + ")";
|
||||
}
|
||||
case rawinput::DESTROYED:
|
||||
return "Unplugged device (" + index_string + ")";
|
||||
default:
|
||||
|
||||
@@ -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>
|
||||
@@ -20,6 +20,7 @@ enum class OptionPickerType {
|
||||
CpuAffinity,
|
||||
FilePath,
|
||||
DirectoryPath,
|
||||
Monitor,
|
||||
};
|
||||
|
||||
struct OptionDefinition {
|
||||
@@ -45,6 +46,8 @@ struct OptionDefinition {
|
||||
|
||||
// for OptionPickerType::FilePath
|
||||
std::string file_extension = "";
|
||||
|
||||
std::string quick_setting_category = "";
|
||||
};
|
||||
|
||||
class Option {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace cfg {
|
||||
// window = rectangle including the frame
|
||||
// client = just the content area without frames.
|
||||
bool window_always_on_top = false;
|
||||
bool window_disable_round_corners = false;
|
||||
bool client_keep_aspect_ratio = true;
|
||||
bool enable_window_resize = false;
|
||||
int window_decoration = 0; // enum type WindowDecorationMode
|
||||
|
||||
@@ -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}")
|
||||
+18
@@ -7,6 +7,7 @@ RUN pacman --noconfirm -Syu git \
|
||||
ninja \
|
||||
cmake \
|
||||
unzip \
|
||||
nasm \
|
||||
mingw-w64-crt \
|
||||
mingw-w64-winpthreads \
|
||||
mingw-w64-gcc \
|
||||
@@ -15,3 +16,20 @@ RUN pacman --noconfirm -Syu git \
|
||||
ccache
|
||||
RUN useradd user -m && echo "ALL ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
RUN su user -c "cd /tmp/ && git clone https://aur.archlinux.org/yay-bin.git --depth=1 && cd yay-bin && makepkg --noconfirm -si && yay --noconfirm -S mingw-w64-cmake"
|
||||
|
||||
RUN mkdir -p /opt/llvm-mingw-xp \
|
||||
&& curl -fsSL "https://github.com/mon/llvm-mingw-xp/releases/download/llvm-mingw-xp-22.1.0/llvm-mingw-llvm-mingw-xp-22.1.0-msvcrt-ubuntu-22.04-x86_64.tar.xz" \
|
||||
| tar -xJ --strip-components=1 -C /opt/llvm-mingw-xp
|
||||
ENV PATH="$PATH:/opt/llvm-mingw-xp/bin"
|
||||
|
||||
RUN curl -fsSL "https://github.com/mon/windows-dll-compat-checker/releases/download/v1.3/windows_dll_compat_checker-linux-x86_64.tar.xz" \
|
||||
| tar -xJ -C /usr/local/bin
|
||||
|
||||
# Stock makepkg.conf builds serially; this makes the AUR compiles below parallel.
|
||||
RUN printf '%s\n' 'MAKEFLAGS="-j$(nproc)"' > /home/user/.makepkg.conf \
|
||||
&& chown user: /home/user/.makepkg.conf
|
||||
|
||||
# libjpeg-turbo for JPEG encoding, x264 for the API H.264 video stream. Only the
|
||||
# mingw-w64 toolchains get these; the WinXP targets build without JPEG support
|
||||
# and without the stream encoder.
|
||||
RUN su user -c "yay --noconfirm -S mingw-w64-libjpeg-turbo mingw-w64-x264"
|
||||
|
||||
+552
@@ -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
|
||||
@@ -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 */
|
||||
Vendored
+3222
File diff suppressed because it is too large
Load Diff
Vendored
+122
@@ -0,0 +1,122 @@
|
||||
// fpng.h - unlicense (see end of fpng.cpp)
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
#ifndef FPNG_TRAIN_HUFFMAN_TABLES
|
||||
// Set to 1 when using the -t (training) option in fpng_test to generate new opaque/alpha Huffman tables for the single pass encoder.
|
||||
#define FPNG_TRAIN_HUFFMAN_TABLES (0)
|
||||
#endif
|
||||
|
||||
namespace fpng
|
||||
{
|
||||
// ---- Library initialization - call once to identify if the processor supports SSE.
|
||||
// Otherwise you'll only get scalar fallbacks.
|
||||
void fpng_init();
|
||||
|
||||
// ---- Useful Utilities
|
||||
|
||||
// Returns true if the CPU supports SSE 4.1, and SSE support wasn't disabled by setting FPNG_NO_SSE=1.
|
||||
// fpng_init() must have been called first, or it'll assert and return false.
|
||||
bool fpng_cpu_supports_sse41();
|
||||
|
||||
// Fast CRC-32 SSE4.1+pclmul or a scalar fallback (slice by 4)
|
||||
const uint32_t FPNG_CRC32_INIT = 0;
|
||||
uint32_t fpng_crc32(const void* pData, size_t size, uint32_t prev_crc32 = FPNG_CRC32_INIT);
|
||||
|
||||
// Fast Adler32 SSE4.1 Adler-32 with a scalar fallback.
|
||||
const uint32_t FPNG_ADLER32_INIT = 1;
|
||||
uint32_t fpng_adler32(const void* pData, size_t size, uint32_t adler = FPNG_ADLER32_INIT);
|
||||
|
||||
// ---- Compression
|
||||
enum
|
||||
{
|
||||
// Enables computing custom Huffman tables for each file, instead of using the custom global tables.
|
||||
// Results in roughly 6% smaller files on average, but compression is around 40% slower.
|
||||
FPNG_ENCODE_SLOWER = 1,
|
||||
|
||||
// Only use raw Deflate blocks (no compression at all). Intended for testing.
|
||||
FPNG_FORCE_UNCOMPRESSED = 2,
|
||||
};
|
||||
|
||||
// Fast PNG encoding. The resulting file can be decoded either using a standard PNG decoder or by the fpng_decode_memory() function below.
|
||||
// pImage: pointer to RGB or RGBA image pixels, R first in memory, B/A last.
|
||||
// w/h - image dimensions. Image's row pitch in bytes must is w*num_chans.
|
||||
// num_chans must be 3 or 4.
|
||||
bool fpng_encode_image_to_memory(const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, std::vector<uint8_t>& out_buf, uint32_t flags = 0);
|
||||
|
||||
#ifndef FPNG_NO_STDIO
|
||||
// Fast PNG encoding to the specified file.
|
||||
bool fpng_encode_image_to_file(const char* pFilename, const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, uint32_t flags = 0);
|
||||
#endif
|
||||
|
||||
// ---- Decompression
|
||||
|
||||
enum
|
||||
{
|
||||
FPNG_DECODE_SUCCESS = 0, // file is a valid PNG file and written by FPNG and the decode succeeded
|
||||
|
||||
FPNG_DECODE_NOT_FPNG, // file is a valid PNG file, but it wasn't written by FPNG so you should try decoding it with a general purpose PNG decoder
|
||||
|
||||
FPNG_DECODE_INVALID_ARG, // invalid function parameter
|
||||
|
||||
FPNG_DECODE_FAILED_NOT_PNG, // file cannot be a PNG file
|
||||
FPNG_DECODE_FAILED_HEADER_CRC32, // a chunk CRC32 check failed, file is likely corrupted or not PNG
|
||||
FPNG_DECODE_FAILED_INVALID_DIMENSIONS, // invalid image dimensions in IHDR chunk (0 or too large)
|
||||
FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE, // decoding the file fully into memory would likely require too much memory (only on 32bpp builds)
|
||||
FPNG_DECODE_FAILED_CHUNK_PARSING, // failed while parsing the chunk headers, or file is corrupted
|
||||
FPNG_DECODE_FAILED_INVALID_IDAT, // IDAT data length is too small and cannot be valid, file is either corrupted or it's a bug
|
||||
|
||||
// fpng_decode_file() specific errors
|
||||
FPNG_DECODE_FILE_OPEN_FAILED,
|
||||
FPNG_DECODE_FILE_TOO_LARGE,
|
||||
FPNG_DECODE_FILE_READ_FAILED,
|
||||
FPNG_DECODE_FILE_SEEK_FAILED
|
||||
};
|
||||
|
||||
// Fast PNG decoding of files ONLY created by fpng_encode_image_to_memory() or fpng_encode_image_to_file().
|
||||
// If fpng_get_info() or fpng_decode_memory() returns FPNG_DECODE_NOT_FPNG, you should decode the PNG by falling back to a general purpose decoder.
|
||||
//
|
||||
// fpng_get_info() parses the PNG header and iterates through all chunks to determine if it's a file written by FPNG, but does not decompress the actual image data so it's relatively fast.
|
||||
//
|
||||
// pImage, image_size: Pointer to PNG image data and its size
|
||||
// width, height: output image's dimensions
|
||||
// channels_in_file: will be 3 or 4
|
||||
//
|
||||
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
|
||||
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
|
||||
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
|
||||
int fpng_get_info(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file);
|
||||
|
||||
// fpng_decode_memory() decompresses 24/32bpp PNG files ONLY encoded by this module.
|
||||
// If the image was written by FPNG, it will decompress the image data, otherwise it will return FPNG_DECODE_NOT_FPNG in which case you should fall back to a general purpose PNG decoder (lodepng, stb_image, libpng, etc.)
|
||||
//
|
||||
// pImage, image_size: Pointer to PNG image data and its size
|
||||
// out: Output 24/32bpp image buffer
|
||||
// width, height: output image's dimensions
|
||||
// channels_in_file: will be 3 or 4
|
||||
// desired_channels: must be 3 or 4
|
||||
//
|
||||
// If the image is 24bpp and 32bpp is requested, the alpha values will be set to 0xFF.
|
||||
// If the image is 32bpp and 24bpp is requested, the alpha values will be discarded.
|
||||
//
|
||||
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
|
||||
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
|
||||
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
|
||||
int fpng_decode_memory(const void* pImage, uint32_t image_size, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
|
||||
|
||||
#ifndef FPNG_NO_STDIO
|
||||
int fpng_decode_file(const char* pFilename, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
|
||||
#endif
|
||||
|
||||
// ---- Internal API used for Huffman table training purposes
|
||||
|
||||
#if FPNG_TRAIN_HUFFMAN_TABLES
|
||||
const uint32_t HUFF_COUNTS_SIZE = 288;
|
||||
extern uint64_t g_huff_counts[HUFF_COUNTS_SIZE];
|
||||
bool create_dynamic_block_prefix(uint64_t* pFreq, uint32_t num_chans, std::vector<uint8_t>& prefix, uint64_t& bit_buf, int& bit_buf_size, uint32_t *pCodes, uint8_t *pCodesizes);
|
||||
#endif
|
||||
|
||||
} // namespace fpng
|
||||
Vendored
-2191
File diff suppressed because it is too large
Load Diff
+6
@@ -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})
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2023 Omar Cornut
|
||||
Copyright (c) 2014-2026 Omar Cornut
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+665
-256
File diff suppressed because it is too large
Load Diff
+25
-12
@@ -1,25 +1,38 @@
|
||||
// dear imgui: Renderer for DirectX9
|
||||
// This needs to be used along with a Platform Binding (e.g. Win32)
|
||||
// dear imgui: Renderer Backend for DirectX9
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'LPDIRECT3DTEXTURE9' as ImTextureID. Read the FAQ about ImTextureID in imgui.cpp.
|
||||
// [X] Renderer: Support for large meshes (64k+ vertices) with 16-bits indices.
|
||||
// [X] Renderer: User texture binding. Use 'LPDIRECT3DTEXTURE9' 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: IMGUI_USE_BGRA_PACKED_COLOR support, as this is the optimal color encoding for DirectX9.
|
||||
// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'.
|
||||
|
||||
// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this.
|
||||
// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp.
|
||||
// https://github.com/ocornut/imgui
|
||||
// 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"
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
struct IDirect3DDevice9;
|
||||
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX9_Init(IDirect3DDevice9 *device);
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX9_Init(IDirect3DDevice9* device);
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_NewFrame();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_RenderDrawData(ImDrawData *draw_data);
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data);
|
||||
|
||||
// Use if you want to reset your rendering device without losing ImGui state.
|
||||
// Use if you want to reset your rendering device without losing Dear ImGui state.
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX9_CreateDeviceObjects();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_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_ImplDX9_UpdateTexture(ImTextureData* tex);
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
|
||||
+13
-5
@@ -15,7 +15,8 @@
|
||||
#pragma once
|
||||
|
||||
//---- Define assertion handler. Defaults to calling assert().
|
||||
// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
|
||||
// - If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
|
||||
// - Compiling with NDEBUG will usually strip out assert() to nothing, which is NOT recommended because we use asserts to notify of programmer mistakes.
|
||||
//#define IM_ASSERT(_EXPR) MyAssert(_EXPR)
|
||||
#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts
|
||||
|
||||
@@ -29,7 +30,6 @@
|
||||
|
||||
//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names.
|
||||
//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
//#define IMGUI_DISABLE_OBSOLETE_KEYIO // 1.87+ disable legacy io.KeyMap[]+io.KeysDown[] in favor io.AddKeyEvent(). This is automatically done by IMGUI_DISABLE_OBSOLETE_FUNCTIONS.
|
||||
|
||||
//---- Disable all of Dear ImGui or don't implement standard windows/tools.
|
||||
// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp.
|
||||
@@ -49,6 +49,7 @@
|
||||
//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies)
|
||||
//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function.
|
||||
#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions().
|
||||
//#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded fonts (ProggyClean/ProggyForever), remove ~9 KB + ~14 KB from output binary. AddFontDefaultXXX() functions will assert.
|
||||
//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available
|
||||
|
||||
//---- Enable Test Engine / Automation features.
|
||||
@@ -59,12 +60,15 @@
|
||||
//#define IMGUI_INCLUDE_IMGUI_USER_H
|
||||
//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h"
|
||||
|
||||
//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another)
|
||||
//---- Pack vertex colors as BGRA8 instead of RGBA8 (to avoid converting from one to another). Need dedicated backend support.
|
||||
#define IMGUI_USE_BGRA_PACKED_COLOR
|
||||
|
||||
//---- Performance optimization for DX9
|
||||
#define IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT struct ImDrawVert { ImVec2 pos; float z; ImU32 col; ImVec2 uv; }
|
||||
|
||||
//---- Use legacy CRC32-adler tables (used before 1.91.6), in order to preserve old .ini data that you cannot afford to invalidate.
|
||||
//#define IMGUI_USE_LEGACY_CRC32_ADLER
|
||||
|
||||
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
||||
//#define IMGUI_USE_WCHAR32
|
||||
|
||||
@@ -83,13 +87,13 @@
|
||||
|
||||
//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui)
|
||||
// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided).
|
||||
// Note that imgui_freetype.cpp may be used _without_ this define, if you manually call ImFontAtlas::SetFontLoader(). The define is simply a convenience.
|
||||
// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'.
|
||||
//#define IMGUI_ENABLE_FREETYPE
|
||||
|
||||
//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT)
|
||||
// Only works in combination with IMGUI_ENABLE_FREETYPE.
|
||||
// - lunasvg is currently easier to acquire/install, as e.g. it is part of vcpkg.
|
||||
// - plutosvg will support more fonts and may load them faster. It currently requires to be built manually but it is fairly easy. See misc/freetype/README for instructions.
|
||||
// - plutosvg is currently easier to install, as e.g. it is part of vcpkg. It will support more fonts and may load them faster. See misc/freetype/README for instructions.
|
||||
// - Both require headers to be available in the include path + program to be linked with the library code (not provided).
|
||||
// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement)
|
||||
//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
@@ -130,6 +134,10 @@
|
||||
//#define IM_DEBUG_BREAK IM_ASSERT(0)
|
||||
//#define IM_DEBUG_BREAK __debugbreak()
|
||||
|
||||
//---- Debug Tools: Enable highlight ID conflicts _before_ hovering items. When io.ConfigDebugHighlightIdConflicts is set.
|
||||
// (THIS WILL SLOW DOWN DEAR IMGUI. Only use occasionally and disable after use)
|
||||
//#define IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS
|
||||
|
||||
//---- Debug Tools: Enable slower asserts
|
||||
//#define IMGUI_DEBUG_PARANOID
|
||||
|
||||
|
||||
Vendored
+3607
-2027
File diff suppressed because it is too large
Load Diff
Vendored
+1081
-543
File diff suppressed because it is too large
Load Diff
+2964
-2093
File diff suppressed because it is too large
Load Diff
+3341
-1210
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
|
||||
|
||||
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
|
||||
{
|
||||
pwd_ = absolute(pwd);
|
||||
// spice2x
|
||||
// avoid MinGW std::filesystem::absolute() duplicating UNC server/share
|
||||
// prefixes, which makes directory_iterator() throw during startup
|
||||
// spice2x
|
||||
const auto &nativePwd = pwd.native();
|
||||
const bool hasUncPrefix = nativePwd.size() >= 2 &&
|
||||
(nativePwd[0] == '\\' || nativePwd[0] == '/') &&
|
||||
(nativePwd[1] == '\\' || nativePwd[1] == '/');
|
||||
pwd_ = hasUncPrefix ? pwd : absolute(pwd);
|
||||
UpdateFileRecords();
|
||||
selectedFilenames_.clear();
|
||||
(*inputNameBuf_)[0] = '\0';
|
||||
|
||||
+804
-345
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user