mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-03 00:38:14 -07:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
|
||||||
@@ -256,6 +256,57 @@ add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
|
|||||||
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
|
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(external/cpu_features 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
|
# set link time optimizations (disabled for Debug builds for speed, disabled
|
||||||
# for RelWithDebInfo builds due to "lto1: error: two or more sections for"
|
# for RelWithDebInfo builds due to "lto1: error: two or more sections for"
|
||||||
# errors)
|
# errors)
|
||||||
@@ -317,6 +368,10 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
# api
|
# api
|
||||||
api/controller.cpp
|
api/controller.cpp
|
||||||
api/websocket.cpp
|
api/websocket.cpp
|
||||||
|
api/capture_pump.cpp
|
||||||
|
api/h264_stream.cpp
|
||||||
|
api/stream_format.cpp
|
||||||
|
api/stream_server.cpp
|
||||||
api/request.cpp
|
api/request.cpp
|
||||||
api/response.cpp
|
api/response.cpp
|
||||||
api/module.cpp
|
api/module.cpp
|
||||||
@@ -332,6 +387,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
api/modules/control.cpp
|
api/modules/control.cpp
|
||||||
api/modules/touch.cpp
|
api/modules/touch.cpp
|
||||||
api/modules/iidx.cpp
|
api/modules/iidx.cpp
|
||||||
|
api/modules/sdvx.cpp
|
||||||
api/serial.cpp
|
api/serial.cpp
|
||||||
api/modules/drs.cpp
|
api/modules/drs.cpp
|
||||||
api/modules/lcd.cpp
|
api/modules/lcd.cpp
|
||||||
@@ -379,7 +435,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
external/tinyxml2/tinyxml2.cpp
|
external/tinyxml2/tinyxml2.cpp
|
||||||
external/http-parser/http_parser.c
|
external/http-parser/http_parser.c
|
||||||
external/usbhidusage/usb-hid-usage.c
|
external/usbhidusage/usb-hid-usage.c
|
||||||
external/toojpeg/toojpeg.cpp
|
external/fpng/fpng.cpp
|
||||||
external/scard/scard.cpp
|
external/scard/scard.cpp
|
||||||
|
|
||||||
# games
|
# games
|
||||||
@@ -411,6 +467,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/sdvx/sdvx_live2d.cpp
|
games/sdvx/sdvx_live2d.cpp
|
||||||
games/sdvx/io.cpp
|
games/sdvx/io.cpp
|
||||||
games/sdvx/camera.cpp
|
games/sdvx/camera.cpp
|
||||||
|
games/jb/bi2x_hook.cpp
|
||||||
games/jb/jb.cpp
|
games/jb/jb.cpp
|
||||||
games/jb/jb_touch.cpp
|
games/jb/jb_touch.cpp
|
||||||
games/jb/io.cpp
|
games/jb/io.cpp
|
||||||
@@ -530,11 +587,15 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/devicehook.cpp
|
hooks/devicehook.cpp
|
||||||
hooks/graphics/graphics.cpp
|
hooks/graphics/graphics.cpp
|
||||||
hooks/graphics/graphics_windowed.cpp
|
hooks/graphics/graphics_windowed.cpp
|
||||||
|
hooks/graphics/jpeg_encoder.cpp
|
||||||
hooks/graphics/nvapi_impl.cpp
|
hooks/graphics/nvapi_impl.cpp
|
||||||
hooks/graphics/nvapi_hook.cpp
|
hooks/graphics/nvapi_hook.cpp
|
||||||
hooks/graphics/nvenc_hook.cpp
|
hooks/graphics/nvenc_hook.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||||
|
hooks/graphics/backends/d3d9/d3d9_readback.cpp
|
||||||
|
hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_device.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_live2d.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
||||||
@@ -574,6 +635,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
misc/device.cpp
|
misc/device.cpp
|
||||||
misc/eamuse.cpp
|
misc/eamuse.cpp
|
||||||
misc/extdev.cpp
|
misc/extdev.cpp
|
||||||
|
misc/hotkeys.cpp
|
||||||
misc/sciunit.cpp
|
misc/sciunit.cpp
|
||||||
misc/sde.cpp
|
misc/sde.cpp
|
||||||
misc/wintouchemu.cpp
|
misc/wintouchemu.cpp
|
||||||
@@ -735,7 +797,7 @@ endfunction()
|
|||||||
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
|
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
|
||||||
target_link_libraries(spicetools_spice_objs
|
target_link_libraries(spicetools_spice_objs
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
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 imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||||
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
|
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
|
||||||
|
|
||||||
if(NOT MSVC)
|
if(NOT MSVC)
|
||||||
@@ -775,7 +837,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W
|
|||||||
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||||
target_link_libraries(spicetools_spice_linux
|
target_link_libraries(spicetools_spice_linux
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
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 imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||||
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
|
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux")
|
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)
|
target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
|
||||||
@@ -793,7 +855,7 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES})
|
|||||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||||
target_link_libraries(spicetools_spice64
|
target_link_libraries(spicetools_spice64
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
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 imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||||
target_link_libraries(spicetools_spice64 PUBLIC winscard)
|
target_link_libraries(spicetools_spice64 PUBLIC winscard)
|
||||||
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
||||||
@@ -816,7 +878,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
|||||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||||
target_link_libraries(spicetools_spice64_linux
|
target_link_libraries(spicetools_spice64_linux
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
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 imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||||
set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
|
set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
|
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
|
||||||
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
|
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
|
||||||
@@ -835,6 +897,7 @@ endif()
|
|||||||
set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
|
set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
|
||||||
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
|
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
|
||||||
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||||
|
# the configurator serves neither the API nor the video stream, so it needs no codecs
|
||||||
target_link_libraries(spicetools_cfg
|
target_link_libraries(spicetools_cfg
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||||
|
|||||||
@@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly.
|
|||||||
- `Side Panel Right Inner`
|
- `Side Panel Right Inner`
|
||||||
- `Side Panel Right`
|
- `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
|
#### LCD
|
||||||
- info()
|
- info()
|
||||||
- returns information about the serial LCD controller some games use
|
- returns information about the serial LCD controller some games use
|
||||||
@@ -277,6 +291,42 @@ which also means that your hex edits are applicable directly.
|
|||||||
- image_resize_set_scene(scene: int)
|
- image_resize_set_scene(scene: int)
|
||||||
- sets the active scene for image resize state; set to 0 to disable resize
|
- 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.
|
||||||
|
|
||||||
|
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 every
|
||||||
|
endpoint returns 404, and the JSON API's JPEG screen capture is unavailable for
|
||||||
|
the same reason.
|
||||||
|
|
||||||
## Native wrapper libraries
|
## Native wrapper libraries
|
||||||
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
|
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
|
||||||
Python is the only one that is fully spec compliant.
|
Python is the only one that is fully spec compliant.
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
|
|||||||
// SDVX Old cabinet mode
|
// SDVX Old cabinet mode
|
||||||
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
|
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
|
||||||
answer_type = 1;
|
answer_type = 1;
|
||||||
if (avs::game::is_model("L44"))
|
if (avs::game::is_model({ "L44", "T44" }))
|
||||||
answer_type = 2;
|
answer_type = 2;
|
||||||
|
|
||||||
// check answer type
|
// check answer type
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#include "capture_pump.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
|
||||||
|
namespace api::capture_pump {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::array<std::mutex, GRAPHICS_CAPTURE_SCREEN_NO> CONSUMER_M;
|
||||||
|
|
||||||
|
std::mutex CLAIMED_M;
|
||||||
|
std::array<bool, GRAPHICS_CAPTURE_SCREEN_NO> CLAIMED {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(CLAIMED_M);
|
||||||
|
|
||||||
|
if (CLAIMED[screen]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAIMED[screen] = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void release_screen(int screen) {
|
||||||
|
if (!valid_screen(screen)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(CLAIMED_M);
|
||||||
|
CLAIMED[screen] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
@@ -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,6 +5,7 @@
|
|||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "client.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "util/crypt.h"
|
#include "util/crypt.h"
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
#include "modules/lcd.h"
|
#include "modules/lcd.h"
|
||||||
#include "modules/lights.h"
|
#include "modules/lights.h"
|
||||||
#include "modules/memory.h"
|
#include "modules/memory.h"
|
||||||
|
#include "modules/sdvx.h"
|
||||||
#include "modules/touch.h"
|
#include "modules/touch.h"
|
||||||
#include "modules/resize.h"
|
#include "modules/resize.h"
|
||||||
#include "request.h"
|
#include "request.h"
|
||||||
@@ -36,6 +38,8 @@
|
|||||||
using namespace rapidjson;
|
using namespace rapidjson;
|
||||||
using namespace api;
|
using namespace api;
|
||||||
|
|
||||||
|
std::atomic_uint32_t api::CLIENT_COUNT = 0;
|
||||||
|
|
||||||
Controller::Controller(unsigned short port, std::string password, bool pretty)
|
Controller::Controller(unsigned short port, std::string password, bool pretty)
|
||||||
: port(port), password(std::move(password)), pretty(pretty)
|
: port(port), password(std::move(password)), pretty(pretty)
|
||||||
{
|
{
|
||||||
@@ -411,8 +415,11 @@ void Controller::init_state(api::ClientState *state) {
|
|||||||
state->modules.push_back(new modules::LCD());
|
state->modules.push_back(new modules::LCD());
|
||||||
state->modules.push_back(new modules::Lights());
|
state->modules.push_back(new modules::Lights());
|
||||||
state->modules.push_back(new modules::Memory());
|
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::Touch());
|
||||||
state->modules.push_back(new modules::Resize());
|
state->modules.push_back(new modules::Resize());
|
||||||
|
|
||||||
|
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::free_state(api::ClientState *state) {
|
void Controller::free_state(api::ClientState *state) {
|
||||||
@@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) {
|
|||||||
|
|
||||||
// free cipher
|
// free cipher
|
||||||
delete state->cipher;
|
delete state->cipher;
|
||||||
|
|
||||||
|
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::free_socket() {
|
void Controller::free_socket() {
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#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;
|
||||||
|
param.i_threads = 1;
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include "api/capture_pump.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "hooks/graphics/jpeg_encoder.h"
|
||||||
#include "util/crypt.h"
|
#include "util/crypt.h"
|
||||||
|
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
@@ -93,8 +95,6 @@ namespace api::modules {
|
|||||||
* reduce: uint for dividing image size
|
* reduce: uint for dividing image size
|
||||||
*/
|
*/
|
||||||
void Capture::get_jpg(Request &req, Response &res) {
|
void Capture::get_jpg(Request &req, Response &res) {
|
||||||
CAPTURE_BUFFER.clear();
|
|
||||||
CAPTURE_BUFFER.reserve(1024 * 128);
|
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
int screen = 0;
|
int screen = 0;
|
||||||
@@ -120,10 +120,16 @@ namespace api::modules {
|
|||||||
uint64_t timestamp = 0;
|
uint64_t timestamp = 0;
|
||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
graphics_capture_trigger(screen);
|
|
||||||
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
|
std::shared_ptr<uint8_t[]> pixels;
|
||||||
CAPTURE_BUFFER.push_back(byte);
|
bool success = capture_pump::capture_direct(
|
||||||
}, true, quality, true, divide, ×tamp, &width, &height);
|
screen, pixels, divide, ×tamp, &width, &height);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
CAPTURE_BUFFER.clear();
|
||||||
|
success = jpeg_encoder::encode(
|
||||||
|
CAPTURE_BUFFER, pixels.get(), width, height, quality);
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
|
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
|
||||||
|
|||||||
@@ -106,14 +106,16 @@ namespace api::modules {
|
|||||||
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
|
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
|
||||||
// Create an array for the light state
|
// Create an array for the light state
|
||||||
Value light_state(kArrayType);
|
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) {
|
for (const auto [r, g, b] : mapping.data) {
|
||||||
light_state.PushBack(r, res.doc()->GetAllocator());
|
light_state.PushBack(r, res.doc()->GetAllocator());
|
||||||
light_state.PushBack(g, res.doc()->GetAllocator());
|
light_state.PushBack(g, res.doc()->GetAllocator());
|
||||||
light_state.PushBack(b, 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());
|
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
|
||||||
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
#include "keypads.h"
|
#include "keypads.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
|
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
using namespace rapidjson;
|
using namespace rapidjson;
|
||||||
@@ -59,7 +60,6 @@ namespace api::modules {
|
|||||||
// get params
|
// get params
|
||||||
auto keypad = req.params[0].GetUint();
|
auto keypad = req.params[0].GetUint();
|
||||||
auto input = std::string(req.params[1].GetString());
|
auto input = std::string(req.params[1].GetString());
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
// process all chars
|
// process all chars
|
||||||
for (auto c : input) {
|
for (auto c : input) {
|
||||||
@@ -93,11 +93,11 @@ namespace api::modules {
|
|||||||
|
|
||||||
// set
|
// set
|
||||||
eamuse_set_keypad_overrides(keypad, state);
|
eamuse_set_keypad_overrides(keypad, state);
|
||||||
timer.sleep(sleep_time);
|
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||||
|
|
||||||
// unset
|
// unset
|
||||||
eamuse_set_keypad_overrides(keypad, 0);
|
eamuse_set_keypad_overrides(keypad, 0);
|
||||||
timer.sleep(sleep_time);
|
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "touch/native/inject.h"
|
#include "touch/native/inject.h"
|
||||||
#include "touch/native/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
|
#include "touch/native/transform.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
|
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
@@ -40,6 +42,35 @@ namespace api::modules {
|
|||||||
return nativetouch::inject::inject_synthetic_touch(position, 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") {
|
Touch::Touch() : Module("touch") {
|
||||||
is_sdvx = avs::game::is_model("KFC");
|
is_sdvx = avs::game::is_model("KFC");
|
||||||
|
|
||||||
@@ -55,9 +86,12 @@ namespace api::modules {
|
|||||||
native_canvas_w = 0;
|
native_canvas_w = 0;
|
||||||
native_canvas_h = 0;
|
native_canvas_h = 0;
|
||||||
if (is_sdvx) {
|
if (is_sdvx) {
|
||||||
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
|
// windowed API coordinates land on the sub screen window as-is; fullscreen
|
||||||
native_canvas_w = 1080;
|
// coordinates are rotated into the game's touch space by apply_touch_errata
|
||||||
native_canvas_h = 1920;
|
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")) {
|
} else if (avs::game::is_model("LDJ")) {
|
||||||
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
||||||
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
||||||
@@ -66,6 +100,10 @@ namespace api::modules {
|
|||||||
// pop'n music API touch surface
|
// pop'n music API touch surface
|
||||||
native_canvas_w = 1280;
|
native_canvas_w = 1280;
|
||||||
native_canvas_h = 800;
|
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["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||||
@@ -209,19 +247,14 @@ namespace api::modules {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Touch::apply_touch_errata(int &x, int &y) {
|
void Touch::apply_touch_errata(int &x, int &y) {
|
||||||
int x_raw = x;
|
|
||||||
int y_raw = y;
|
|
||||||
|
|
||||||
if (is_tdj_fhd) {
|
if (is_tdj_fhd) {
|
||||||
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p)
|
// 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
|
// 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
|
// the target of the touch events so just assume it's the sub screen
|
||||||
x = x_raw * 1920 / 1280;
|
x = x * 1920 / 1280;
|
||||||
y = y_raw * 1080 / 720;
|
y = y * 1080 / 720;
|
||||||
} else if (is_sdvx) {
|
} else if (is_sdvx) {
|
||||||
// for exceed gear, they are both 1080p screens, but need to apply transformation
|
sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
|
||||||
x = 1080 - y_raw;
|
|
||||||
y = x_raw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .coin import *
|
|||||||
from .control import *
|
from .control import *
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .iidx import *
|
from .iidx import *
|
||||||
|
from .sdvx import *
|
||||||
from .info import *
|
from .info import *
|
||||||
from .keypads import *
|
from .keypads import *
|
||||||
from .lights import *
|
from .lights import *
|
||||||
|
|||||||
@@ -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,11 +1,12 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "serial.h"
|
#include "serial.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ namespace api {
|
|||||||
controller->init_state(this->state);
|
controller->init_state(this->state);
|
||||||
this->thread = new std::thread([this] () {
|
this->thread = new std::thread([this] () {
|
||||||
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
// read buffer
|
// read buffer
|
||||||
uint8_t read_buffer[16*1024];
|
uint8_t read_buffer[16*1024];
|
||||||
@@ -162,7 +162,7 @@ namespace api {
|
|||||||
|
|
||||||
// slow down on reconnect
|
// slow down on reconnect
|
||||||
if (this->running) {
|
if (this->running) {
|
||||||
timer.sleep(retry_time);
|
std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
#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 "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";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamServer::StreamServer(unsigned short port)
|
||||||
|
: port(port)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
send_error(socket, "503 Service Unavailable");
|
||||||
|
} else {
|
||||||
|
log_info("api::stream",
|
||||||
|
"client connected: {} ({}, screen={}, fps={}, quality={})",
|
||||||
|
address, request.path, screen, fps, quality);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
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,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <winsock2.h>
|
||||||
|
|
||||||
|
namespace api {
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -12,6 +12,20 @@ using namespace headsocket;
|
|||||||
|
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// how long a single handshake read may stall before the connection is dropped;
|
||||||
|
// headsocket reads the request a byte at a time, so this is an idle timeout between
|
||||||
|
// bytes rather than a deadline for the whole handshake
|
||||||
|
constexpr int handshake_timeout_ms = 5000;
|
||||||
|
|
||||||
|
void set_recv_timeout(connection &conn, int milliseconds) {
|
||||||
|
DWORD timeout = static_cast<DWORD>(milliseconds);
|
||||||
|
setsockopt(conn.impl()->socket, SOL_SOCKET, SO_RCVTIMEO,
|
||||||
|
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Client class declaration
|
* Client class declaration
|
||||||
*/
|
*/
|
||||||
@@ -37,6 +51,21 @@ namespace api {
|
|||||||
HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
|
HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
|
||||||
public:
|
public:
|
||||||
WebSocketController *websocket;
|
WebSocketController *websocket;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool handshake(connection &conn) override {
|
||||||
|
|
||||||
|
// headsocket runs the handshake on its single accept thread with a blocking
|
||||||
|
// recv, so a peer that connects and then says nothing would park that thread and
|
||||||
|
// leave every later connection sitting unaccepted in the backlog
|
||||||
|
set_recv_timeout(conn, handshake_timeout_ms);
|
||||||
|
const bool accepted = base_t::handshake(conn);
|
||||||
|
|
||||||
|
// from here the client thread owns the socket and wants to block on reads
|
||||||
|
set_recv_timeout(conn, 0);
|
||||||
|
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void api::WebSocketServer::init() {}
|
void api::WebSocketServer::init() {}
|
||||||
|
|||||||
@@ -1114,6 +1114,7 @@ namespace avs {
|
|||||||
#endif
|
#endif
|
||||||
// jubeat
|
// jubeat
|
||||||
{"jubeat.dll", 0x2000000},
|
{"jubeat.dll", 0x2000000},
|
||||||
|
{"jubeat2019.dll", 0x10000000},
|
||||||
|
|
||||||
// MUSECA
|
// MUSECA
|
||||||
{"museca.dll", 0xC000000},
|
{"museca.dll", 0xC000000},
|
||||||
@@ -1924,7 +1925,7 @@ namespace avs {
|
|||||||
#if !SPICE64
|
#if !SPICE64
|
||||||
// sdvx4 bad log config fix
|
// sdvx4 bad log config fix
|
||||||
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
||||||
property_search_safe(config, config_node, "/log/enable_console")) {
|
property_search(config, config_node, "/log/enable_console")) {
|
||||||
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <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");
|
property_search_remove_safe(config, config_node, "/log/enable_console");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -591,6 +591,7 @@ namespace avs {
|
|||||||
// for proper reporting of Omnimix and other song packs
|
// for proper reporting of Omnimix and other song packs
|
||||||
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
|
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
|
||||||
_stricmp(EA3_MODEL, "L44") == 0 ||
|
_stricmp(EA3_MODEL, "L44") == 0 ||
|
||||||
|
_stricmp(EA3_MODEL, "T44") == 0 ||
|
||||||
_stricmp(EA3_MODEL, "M39") == 0 ||
|
_stricmp(EA3_MODEL, "M39") == 0 ||
|
||||||
_stricmp(EA3_MODEL, "KFC") == 0)
|
_stricmp(EA3_MODEL, "KFC") == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
<dpiAware>true</dpiAware>
|
<dpiAware>true</dpiAware>
|
||||||
|
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
|
||||||
</asmv3:windowsSettings>
|
</asmv3:windowsSettings>
|
||||||
</asmv3:application>
|
</asmv3:application>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
+9
@@ -7,6 +7,7 @@ RUN pacman --noconfirm -Syu git \
|
|||||||
ninja \
|
ninja \
|
||||||
cmake \
|
cmake \
|
||||||
unzip \
|
unzip \
|
||||||
|
nasm \
|
||||||
mingw-w64-crt \
|
mingw-w64-crt \
|
||||||
mingw-w64-winpthreads \
|
mingw-w64-winpthreads \
|
||||||
mingw-w64-gcc \
|
mingw-w64-gcc \
|
||||||
@@ -23,3 +24,11 @@ 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" \
|
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
|
| tar -xJ -C /usr/local/bin
|
||||||
|
|
||||||
|
# libjpeg-turbo for JPEG encoding. Only the mingw-w64 toolchains get it;
|
||||||
|
# the WinXP targets build without JPEG support entirely.
|
||||||
|
RUN su user -c "yay --noconfirm -S mingw-w64-libjpeg-turbo"
|
||||||
|
|
||||||
|
# x264 for the API H.264 video stream. Only the mingw-w64 toolchains need it;
|
||||||
|
# the WinXP targets build without the stream encoder.
|
||||||
|
RUN su user -c "yay --noconfirm -S mingw-w64-x264"
|
||||||
|
|||||||
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
|
||||||
+9
-1
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
|
|||||||
|
|
||||||
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
|
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();
|
UpdateFileRecords();
|
||||||
selectedFilenames_.clear();
|
selectedFilenames_.clear();
|
||||||
(*inputNameBuf_)[0] = '\0';
|
(*inputNameBuf_)[0] = '\0';
|
||||||
|
|||||||
Vendored
-10
@@ -1,10 +0,0 @@
|
|||||||
zlib License
|
|
||||||
|
|
||||||
Copyright (c) 2011-2016 Stephan Brumme
|
|
||||||
|
|
||||||
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
|
|
||||||
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
|
||||||
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
|
|
||||||
If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
|
|
||||||
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
|
||||||
3. This notice may not be removed or altered from any source distribution.
|
|
||||||
-665
@@ -1,665 +0,0 @@
|
|||||||
// //////////////////////////////////////////////////////////
|
|
||||||
// toojpeg.cpp
|
|
||||||
// written by Stephan Brumme, 2018-2019
|
|
||||||
// see https://create.stephan-brumme.com/toojpeg/
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "toojpeg.h"
|
|
||||||
|
|
||||||
// - the "official" specifications: https://www.w3.org/Graphics/JPEG/itu-t81.pdf and https://www.w3.org/Graphics/JPEG/jfif3.pdf
|
|
||||||
// - Wikipedia has a short description of the JFIF/JPEG file format: https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
|
|
||||||
// - the popular STB Image library includes Jon's JPEG encoder as well: https://github.com/nothings/stb/blob/master/stb_image_write.h
|
|
||||||
// - the most readable JPEG book (from a developer's perspective) is Miano's "Compressed Image File Formats" (1999, ISBN 0-201-60443-4),
|
|
||||||
// used copies are really cheap nowadays and include a CD with C++ sources as well (plus great format descriptions of GIF & PNG)
|
|
||||||
// - much more detailled is Mitchell/Pennebaker's "JPEG: Still Image Data Compression Standard" (1993, ISBN 0-442-01272-1)
|
|
||||||
// which contains the official JPEG standard, too - fun fact: I bought a signed copy in a second-hand store without noticing
|
|
||||||
|
|
||||||
namespace // anonymous namespace to hide local functions / constants / etc.
|
|
||||||
{
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// data types
|
|
||||||
using uint8_t = unsigned char;
|
|
||||||
using uint16_t = unsigned short;
|
|
||||||
using int16_t = short;
|
|
||||||
using int32_t = int; // at least four bytes
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// constants
|
|
||||||
|
|
||||||
// quantization tables from JPEG Standard, Annex K
|
|
||||||
const uint8_t DefaultQuantLuminance[8*8] =
|
|
||||||
{ 16, 11, 10, 16, 24, 40, 51, 61, // there are a few experts proposing slightly more efficient values,
|
|
||||||
12, 12, 14, 19, 26, 58, 60, 55, // e.g. https://www.imagemagick.org/discourse-server/viewtopic.php?t=20333
|
|
||||||
14, 13, 16, 24, 40, 57, 69, 56, // btw: Google's Guetzli project optimizes the quantization tables per image
|
|
||||||
14, 17, 22, 29, 51, 87, 80, 62,
|
|
||||||
18, 22, 37, 56, 68,109,103, 77,
|
|
||||||
24, 35, 55, 64, 81,104,113, 92,
|
|
||||||
49, 64, 78, 87,103,121,120,101,
|
|
||||||
72, 92, 95, 98,112,100,103, 99 };
|
|
||||||
const uint8_t DefaultQuantChrominance[8*8] =
|
|
||||||
{ 17, 18, 24, 47, 99, 99, 99, 99,
|
|
||||||
18, 21, 26, 66, 99, 99, 99, 99,
|
|
||||||
24, 26, 56, 99, 99, 99, 99, 99,
|
|
||||||
47, 66, 99, 99, 99, 99, 99, 99,
|
|
||||||
99, 99, 99, 99, 99, 99, 99, 99,
|
|
||||||
99, 99, 99, 99, 99, 99, 99, 99,
|
|
||||||
99, 99, 99, 99, 99, 99, 99, 99,
|
|
||||||
99, 99, 99, 99, 99, 99, 99, 99 };
|
|
||||||
|
|
||||||
// 8x8 blocks are processed in zig-zag order
|
|
||||||
// most encoders use a zig-zag "forward" table, I switched to its inverse for performance reasons
|
|
||||||
// note: ZigZagInv[ZigZag[i]] = i
|
|
||||||
const uint8_t ZigZagInv[8*8] =
|
|
||||||
{ 0, 1, 8,16, 9, 2, 3,10, // ZigZag[] = 0, 1, 5, 6,14,15,27,28,
|
|
||||||
17,24,32,25,18,11, 4, 5, // 2, 4, 7,13,16,26,29,42,
|
|
||||||
12,19,26,33,40,48,41,34, // 3, 8,12,17,25,30,41,43,
|
|
||||||
27,20,13, 6, 7,14,21,28, // 9,11,18,24,31,40,44,53,
|
|
||||||
35,42,49,56,57,50,43,36, // 10,19,23,32,39,45,52,54,
|
|
||||||
29,22,15,23,30,37,44,51, // 20,22,33,38,46,51,55,60,
|
|
||||||
58,59,52,45,38,31,39,46, // 21,34,37,47,50,56,59,61,
|
|
||||||
53,60,61,54,47,55,62,63 }; // 35,36,48,49,57,58,62,63
|
|
||||||
|
|
||||||
// static Huffman code tables from JPEG standard Annex K
|
|
||||||
// - CodesPerBitsize tables define how many Huffman codes will have a certain bitsize (plus 1 because there nothing with zero bits),
|
|
||||||
// e.g. DcLuminanceCodesPerBitsize[2] = 5 because there are 5 Huffman codes being 2+1=3 bits long
|
|
||||||
// - Values tables are a list of values ordered by their Huffman code bitsize,
|
|
||||||
// e.g. AcLuminanceValues => Huffman(0x01,0x02 and 0x03) will have 2 bits, Huffman(0x00) will have 3 bits, Huffman(0x04,0x11 and 0x05) will have 4 bits, ...
|
|
||||||
|
|
||||||
// Huffman definitions for first DC/AC tables (luminance / Y channel)
|
|
||||||
const uint8_t DcLuminanceCodesPerBitsize[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; // sum = 12
|
|
||||||
const uint8_t DcLuminanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes
|
|
||||||
const uint8_t AcLuminanceCodesPerBitsize[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 }; // sum = 162
|
|
||||||
const uint8_t AcLuminanceValues [162] = // => 162 codes
|
|
||||||
{ 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, // 16*10+2 symbols because
|
|
||||||
0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, // upper 4 bits can be 0..F
|
|
||||||
0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, // while lower 4 bits can be 1..A
|
|
||||||
0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, // plus two special codes 0x00 and 0xF0
|
|
||||||
0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, // order of these symbols was determined empirically by JPEG committee
|
|
||||||
0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2,
|
|
||||||
0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
|
|
||||||
// Huffman definitions for second DC/AC tables (chrominance / Cb and Cr channels)
|
|
||||||
const uint8_t DcChrominanceCodesPerBitsize[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; // sum = 12
|
|
||||||
const uint8_t DcChrominanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes (identical to DcLuminanceValues)
|
|
||||||
const uint8_t AcChrominanceCodesPerBitsize[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 }; // sum = 162
|
|
||||||
const uint8_t AcChrominanceValues [162] = // => 162 codes
|
|
||||||
{ 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, // same number of symbol, just different order
|
|
||||||
0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, // (which is more efficient for AC coding)
|
|
||||||
0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,
|
|
||||||
0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87,
|
|
||||||
0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,
|
|
||||||
0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,
|
|
||||||
0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
|
|
||||||
const int16_t CodeWordLimit = 2048; // +/-2^11, maximum value after DCT
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// structs
|
|
||||||
|
|
||||||
// represent a single Huffman code
|
|
||||||
struct BitCode
|
|
||||||
{
|
|
||||||
BitCode() = default; // undefined state, must be initialized at a later time
|
|
||||||
BitCode(uint16_t code_, uint8_t numBits_)
|
|
||||||
: code(code_), numBits(numBits_) {}
|
|
||||||
uint16_t code; // JPEG's Huffman codes are limited to 16 bits
|
|
||||||
uint8_t numBits; // number of valid bits
|
|
||||||
};
|
|
||||||
|
|
||||||
// wrapper for bit output operations
|
|
||||||
struct BitWriter
|
|
||||||
{
|
|
||||||
// user-supplied callback that writes/stores one byte
|
|
||||||
TooJpeg::WRITE_ONE_BYTE output;
|
|
||||||
// initialize writer
|
|
||||||
explicit BitWriter(TooJpeg::WRITE_ONE_BYTE output_) : output(output_) {}
|
|
||||||
|
|
||||||
// store the most recently encoded bits that are not written yet
|
|
||||||
struct BitBuffer
|
|
||||||
{
|
|
||||||
int32_t data = 0; // actually only at most 24 bits are used
|
|
||||||
uint8_t numBits = 0; // number of valid bits (the right-most bits)
|
|
||||||
} buffer;
|
|
||||||
|
|
||||||
// write Huffman bits stored in BitCode, keep excess bits in BitBuffer
|
|
||||||
BitWriter& operator<<(const BitCode& data)
|
|
||||||
{
|
|
||||||
// append the new bits to those bits leftover from previous call(s)
|
|
||||||
buffer.numBits += data.numBits;
|
|
||||||
buffer.data <<= data.numBits;
|
|
||||||
buffer.data |= data.code;
|
|
||||||
|
|
||||||
// write all "full" bytes
|
|
||||||
while (buffer.numBits >= 8)
|
|
||||||
{
|
|
||||||
// extract highest 8 bits
|
|
||||||
buffer.numBits -= 8;
|
|
||||||
auto oneByte = uint8_t(buffer.data >> buffer.numBits);
|
|
||||||
output(oneByte);
|
|
||||||
|
|
||||||
if (oneByte == 0xFF) // 0xFF has a special meaning for JPEGs (it's a block marker)
|
|
||||||
output(0); // therefore pad a zero to indicate "nope, this one ain't a marker, it's just a coincidence"
|
|
||||||
|
|
||||||
// note: I don't clear those written bits, therefore buffer.bits may contain garbage in the high bits
|
|
||||||
// if you really want to "clean up" (e.g. for debugging purposes) then uncomment the following line
|
|
||||||
//buffer.bits &= (1 << buffer.numBits) - 1;
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write all non-yet-written bits, fill gaps with 1s (that's a strange JPEG thing)
|
|
||||||
void flush()
|
|
||||||
{
|
|
||||||
// at most seven set bits needed to "fill" the last byte: 0x7F = binary 0111 1111
|
|
||||||
*this << BitCode(0x7F, 7); // I should set buffer.numBits = 0 but since there are no single bits written after flush() I can safely ignore it
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: all the following BitWriter functions IGNORE the BitBuffer and write straight to output !
|
|
||||||
// write a single byte
|
|
||||||
BitWriter& operator<<(uint8_t oneByte)
|
|
||||||
{
|
|
||||||
output(oneByte);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write an array of bytes
|
|
||||||
template <typename T, int Size>
|
|
||||||
BitWriter& operator<<(T (&manyBytes)[Size])
|
|
||||||
{
|
|
||||||
for (auto c : manyBytes)
|
|
||||||
output(c);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// start a new JFIF block
|
|
||||||
void addMarker(uint8_t id, uint16_t length)
|
|
||||||
{
|
|
||||||
output(0xFF); output(id); // ID, always preceded by 0xFF
|
|
||||||
output(uint8_t(length >> 8)); // length of the block (big-endian, includes the 2 length bytes as well)
|
|
||||||
output(uint8_t(length & 0xFF));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// functions / templates
|
|
||||||
|
|
||||||
// same as std::min()
|
|
||||||
template <typename Number>
|
|
||||||
Number minimum(Number value, Number maximum)
|
|
||||||
{
|
|
||||||
return value <= maximum ? value : maximum;
|
|
||||||
}
|
|
||||||
|
|
||||||
// restrict a value to the interval [minimum, maximum]
|
|
||||||
template <typename Number, typename Limit>
|
|
||||||
Number clamp(Number value, Limit minValue, Limit maxValue)
|
|
||||||
{
|
|
||||||
if (value <= minValue) return minValue; // never smaller than the minimum
|
|
||||||
if (value >= maxValue) return maxValue; // never bigger than the maximum
|
|
||||||
return value; // value was inside interval, keep it
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert from RGB to YCbCr, constants are similar to ITU-R, see https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion
|
|
||||||
float rgb2y (float r, float g, float b) { return +0.299f * r +0.587f * g +0.114f * b; }
|
|
||||||
float rgb2cb(float r, float g, float b) { return -0.16874f * r -0.33126f * g +0.5f * b; }
|
|
||||||
float rgb2cr(float r, float g, float b) { return +0.5f * r -0.41869f * g -0.08131f * b; }
|
|
||||||
|
|
||||||
// forward DCT computation "in one dimension" (fast AAN algorithm by Arai, Agui and Nakajima: "A fast DCT-SQ scheme for images")
|
|
||||||
void DCT(float block[8*8], uint8_t stride) // stride must be 1 (=horizontal) or 8 (=vertical)
|
|
||||||
{
|
|
||||||
const auto SqrtHalfSqrt = 1.306562965f; // sqrt((2 + sqrt(2)) / 2) = cos(pi * 1 / 8) * sqrt(2)
|
|
||||||
const auto InvSqrt = 0.707106781f; // 1 / sqrt(2) = cos(pi * 2 / 8)
|
|
||||||
const auto HalfSqrtSqrt = 0.382683432f; // sqrt(2 - sqrt(2)) / 2 = cos(pi * 3 / 8)
|
|
||||||
const auto InvSqrtSqrt = 0.541196100f; // 1 / sqrt(2 - sqrt(2)) = cos(pi * 3 / 8) * sqrt(2)
|
|
||||||
|
|
||||||
// modify in-place
|
|
||||||
auto& block0 = block[0 ];
|
|
||||||
auto& block1 = block[1 * stride];
|
|
||||||
auto& block2 = block[2 * stride];
|
|
||||||
auto& block3 = block[3 * stride];
|
|
||||||
auto& block4 = block[4 * stride];
|
|
||||||
auto& block5 = block[5 * stride];
|
|
||||||
auto& block6 = block[6 * stride];
|
|
||||||
auto& block7 = block[7 * stride];
|
|
||||||
|
|
||||||
// based on https://dev.w3.org/Amaya/libjpeg/jfdctflt.c , the original variable names can be found in my comments
|
|
||||||
auto add07 = block0 + block7; auto sub07 = block0 - block7; // tmp0, tmp7
|
|
||||||
auto add16 = block1 + block6; auto sub16 = block1 - block6; // tmp1, tmp6
|
|
||||||
auto add25 = block2 + block5; auto sub25 = block2 - block5; // tmp2, tmp5
|
|
||||||
auto add34 = block3 + block4; auto sub34 = block3 - block4; // tmp3, tmp4
|
|
||||||
|
|
||||||
auto add0347 = add07 + add34; auto sub07_34 = add07 - add34; // tmp10, tmp13 ("even part" / "phase 2")
|
|
||||||
auto add1256 = add16 + add25; auto sub16_25 = add16 - add25; // tmp11, tmp12
|
|
||||||
|
|
||||||
block0 = add0347 + add1256; block4 = add0347 - add1256; // "phase 3"
|
|
||||||
|
|
||||||
auto z1 = (sub16_25 + sub07_34) * InvSqrt; // all temporary z-variables kept their original names
|
|
||||||
block2 = sub07_34 + z1; block6 = sub07_34 - z1; // "phase 5"
|
|
||||||
|
|
||||||
auto sub23_45 = sub25 + sub34; // tmp10 ("odd part" / "phase 2")
|
|
||||||
auto sub12_56 = sub16 + sub25; // tmp11
|
|
||||||
auto sub01_67 = sub16 + sub07; // tmp12
|
|
||||||
|
|
||||||
auto z5 = (sub23_45 - sub01_67) * HalfSqrtSqrt;
|
|
||||||
auto z2 = sub23_45 * InvSqrtSqrt + z5;
|
|
||||||
auto z3 = sub12_56 * InvSqrt;
|
|
||||||
auto z4 = sub01_67 * SqrtHalfSqrt + z5;
|
|
||||||
auto z6 = sub07 + z3; // z11 ("phase 5")
|
|
||||||
auto z7 = sub07 - z3; // z13
|
|
||||||
block1 = z6 + z4; block7 = z6 - z4; // "phase 6"
|
|
||||||
block5 = z7 + z2; block3 = z7 - z2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// run DCT, quantize and write Huffman bit codes
|
|
||||||
int16_t encodeBlock(BitWriter& writer, float block[8][8], const float scaled[8*8], int16_t lastDC,
|
|
||||||
const BitCode huffmanDC[256], const BitCode huffmanAC[256], const BitCode* codewords)
|
|
||||||
{
|
|
||||||
// "linearize" the 8x8 block, treat it as a flat array of 64 floats
|
|
||||||
auto block64 = (float*) block;
|
|
||||||
|
|
||||||
// DCT: rows
|
|
||||||
for (auto offset = 0; offset < 8; offset++)
|
|
||||||
DCT(block64 + offset*8, 1);
|
|
||||||
// DCT: columns
|
|
||||||
for (auto offset = 0; offset < 8; offset++)
|
|
||||||
DCT(block64 + offset*1, 8);
|
|
||||||
|
|
||||||
// scale
|
|
||||||
for (auto i = 0; i < 8*8; i++)
|
|
||||||
block64[i] *= scaled[i];
|
|
||||||
|
|
||||||
// encode DC (the first coefficient is the "average color" of the 8x8 block)
|
|
||||||
auto DC = int(block64[0] + (block64[0] >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
|
|
||||||
|
|
||||||
// quantize and zigzag the other 63 coefficients
|
|
||||||
auto posNonZero = 0; // find last coefficient which is not zero (because trailing zeros are encoded differently)
|
|
||||||
int16_t quantized[8*8];
|
|
||||||
for (auto i = 1; i < 8*8; i++) // start at 1 because block64[0]=DC was already processed
|
|
||||||
{
|
|
||||||
auto value = block64[ZigZagInv[i]];
|
|
||||||
// round to nearest integer
|
|
||||||
quantized[i] = int(value + (value >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
|
|
||||||
// remember offset of last non-zero coefficient
|
|
||||||
if (quantized[i] != 0)
|
|
||||||
posNonZero = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// same "average color" as previous block ?
|
|
||||||
auto diff = DC - lastDC;
|
|
||||||
if (diff == 0)
|
|
||||||
writer << huffmanDC[0x00]; // yes, write a special short symbol
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auto bits = codewords[diff]; // nope, encode the difference to previous block's average color
|
|
||||||
writer << huffmanDC[bits.numBits] << bits;
|
|
||||||
}
|
|
||||||
|
|
||||||
// encode ACs (quantized[1..63])
|
|
||||||
auto offset = 0; // upper 4 bits count the number of consecutive zeros
|
|
||||||
for (auto i = 1; i <= posNonZero; i++) // quantized[0] was already written, skip all trailing zeros, too
|
|
||||||
{
|
|
||||||
// zeros are encoded in a special way
|
|
||||||
while (quantized[i] == 0) // found another zero ?
|
|
||||||
{
|
|
||||||
offset += 0x10; // add 1 to the upper 4 bits
|
|
||||||
// split into blocks of at most 16 consecutive zeros
|
|
||||||
if (offset > 0xF0) // remember, the counter is in the upper 4 bits, 0xF = 15
|
|
||||||
{
|
|
||||||
writer << huffmanAC[0xF0]; // 0xF0 is a special code for "16 zeros"
|
|
||||||
offset = 0;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto encoded = codewords[quantized[i]];
|
|
||||||
// combine number of zeros with the number of bits of the next non-zero value
|
|
||||||
writer << huffmanAC[offset + encoded.numBits] << encoded; // and the value itself
|
|
||||||
offset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// send end-of-block code (0x00), only needed if there are trailing zeros
|
|
||||||
if (posNonZero < 8*8 - 1) // = 63
|
|
||||||
writer << huffmanAC[0x00];
|
|
||||||
|
|
||||||
return DC;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jon's code includes the pre-generated Huffman codes
|
|
||||||
// I don't like these "magic constants" and compute them on my own :-)
|
|
||||||
void generateHuffmanTable(const uint8_t numCodes[16], const uint8_t* values, BitCode result[256])
|
|
||||||
{
|
|
||||||
// process all bitsizes 1 thru 16, no JPEG Huffman code is allowed to exceed 16 bits
|
|
||||||
auto huffmanCode = 0;
|
|
||||||
for (auto numBits = 1; numBits <= 16; numBits++)
|
|
||||||
{
|
|
||||||
// ... and each code of these bitsizes
|
|
||||||
for (auto i = 0; i < numCodes[numBits - 1]; i++) // note: numCodes array starts at zero, but smallest bitsize is 1
|
|
||||||
result[*values++] = BitCode(huffmanCode++, numBits);
|
|
||||||
|
|
||||||
// next Huffman code needs to be one bit wider
|
|
||||||
huffmanCode <<= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // end of anonymous namespace
|
|
||||||
|
|
||||||
// -------------------- externally visible code --------------------
|
|
||||||
|
|
||||||
namespace TooJpeg
|
|
||||||
{
|
|
||||||
// the only exported function ...
|
|
||||||
bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels_, unsigned short width, unsigned short height,
|
|
||||||
bool isRGB, unsigned char quality_, bool downsample, const char* comment)
|
|
||||||
{
|
|
||||||
// reject invalid pointers
|
|
||||||
if (output == nullptr || pixels_ == nullptr)
|
|
||||||
return false;
|
|
||||||
// check image format
|
|
||||||
if (width == 0 || height == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// number of components
|
|
||||||
const auto numComponents = isRGB ? 3 : 1;
|
|
||||||
// note: if there is just one component (=grayscale), then only luminance needs to be stored in the file
|
|
||||||
// thus everything related to chrominance need not to be written to the JPEG
|
|
||||||
// I still compute a few things, like quantization tables to avoid a complete code mess
|
|
||||||
|
|
||||||
// grayscale images can't be downsampled (because there are no Cb + Cr channels)
|
|
||||||
if (!isRGB)
|
|
||||||
downsample = false;
|
|
||||||
|
|
||||||
// wrapper for all output operations
|
|
||||||
BitWriter bitWriter(output);
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// JFIF headers
|
|
||||||
const uint8_t HeaderJfif[2+2+16] =
|
|
||||||
{ 0xFF,0xD8, // SOI marker (start of image)
|
|
||||||
0xFF,0xE0, // JFIF APP0 tag
|
|
||||||
0,16, // length: 16 bytes (14 bytes payload + 2 bytes for this length field)
|
|
||||||
'J','F','I','F',0, // JFIF identifier, zero-terminated
|
|
||||||
1,1, // JFIF version 1.1
|
|
||||||
0, // no density units specified
|
|
||||||
0,1,0,1, // density: 1 pixel "per pixel" horizontally and vertically
|
|
||||||
0,0 }; // no thumbnail (size 0 x 0)
|
|
||||||
bitWriter << HeaderJfif;
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// comment (optional)
|
|
||||||
if (comment != nullptr)
|
|
||||||
{
|
|
||||||
// look for zero terminator
|
|
||||||
auto length = 0; // = strlen(comment);
|
|
||||||
while (comment[length] != 0)
|
|
||||||
length++;
|
|
||||||
|
|
||||||
// write COM marker
|
|
||||||
bitWriter.addMarker(0xFE, 2+length); // block size is number of bytes (without zero terminator) + 2 bytes for this length field
|
|
||||||
// ... and write the comment itself
|
|
||||||
for (auto i = 0; i < length; i++)
|
|
||||||
bitWriter << comment[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// adjust quantization tables to desired quality
|
|
||||||
|
|
||||||
// quality level must be in 1 ... 100
|
|
||||||
auto quality = clamp<uint16_t>(quality_, 1, 100);
|
|
||||||
// convert to an internal JPEG quality factor, formula taken from libjpeg
|
|
||||||
quality = quality < 50 ? 5000 / quality : 200 - quality * 2;
|
|
||||||
|
|
||||||
uint8_t quantLuminance [8*8];
|
|
||||||
uint8_t quantChrominance[8*8];
|
|
||||||
for (auto i = 0; i < 8*8; i++)
|
|
||||||
{
|
|
||||||
int luminance = (DefaultQuantLuminance [ZigZagInv[i]] * quality + 50) / 100;
|
|
||||||
int chrominance = (DefaultQuantChrominance[ZigZagInv[i]] * quality + 50) / 100;
|
|
||||||
|
|
||||||
// clamp to 1..255
|
|
||||||
quantLuminance [i] = clamp(luminance, 1, 255);
|
|
||||||
quantChrominance[i] = clamp(chrominance, 1, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
// write quantization tables
|
|
||||||
bitWriter.addMarker(0xDB, 2 + (isRGB ? 2 : 1) * (1 + 8*8)); // length: 65 bytes per table + 2 bytes for this length field
|
|
||||||
// each table has 64 entries and is preceded by an ID byte
|
|
||||||
|
|
||||||
bitWriter << 0x00 << quantLuminance; // first quantization table
|
|
||||||
if (isRGB)
|
|
||||||
bitWriter << 0x01 << quantChrominance; // second quantization table, only relevant for color images
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// write image infos (SOF0 - start of frame)
|
|
||||||
bitWriter.addMarker(0xC0, 2+6+3*numComponents); // length: 6 bytes general info + 3 per channel + 2 bytes for this length field
|
|
||||||
|
|
||||||
// 8 bits per channel
|
|
||||||
bitWriter << 0x08
|
|
||||||
// image dimensions (big-endian)
|
|
||||||
<< (height >> 8) << (height & 0xFF)
|
|
||||||
<< (width >> 8) << (width & 0xFF);
|
|
||||||
|
|
||||||
// sampling and quantization tables for each component
|
|
||||||
bitWriter << numComponents; // 1 component (grayscale, Y only) or 3 components (Y,Cb,Cr)
|
|
||||||
for (auto id = 1; id <= numComponents; id++)
|
|
||||||
bitWriter << id // component ID (Y=1, Cb=2, Cr=3)
|
|
||||||
// bitmasks for sampling: highest 4 bits: horizontal, lowest 4 bits: vertical
|
|
||||||
<< (id == 1 && downsample ? 0x22 : 0x11) // 0x11 is default YCbCr 4:4:4 and 0x22 stands for YCbCr 4:2:0
|
|
||||||
<< (id == 1 ? 0 : 1); // use quantization table 0 for Y, table 1 for Cb and Cr
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// Huffman tables
|
|
||||||
// DHT marker - define Huffman tables
|
|
||||||
bitWriter.addMarker(0xC4, isRGB ? (2+208+208) : (2+208));
|
|
||||||
// 2 bytes for the length field, store chrominance only if needed
|
|
||||||
// 1+16+12 for the DC luminance
|
|
||||||
// 1+16+162 for the AC luminance (208 = 1+16+12 + 1+16+162)
|
|
||||||
// 1+16+12 for the DC chrominance
|
|
||||||
// 1+16+162 for the AC chrominance (208 = 1+16+12 + 1+16+162, same as above)
|
|
||||||
|
|
||||||
// store luminance's DC+AC Huffman table definitions
|
|
||||||
bitWriter << 0x00 // highest 4 bits: 0 => DC, lowest 4 bits: 0 => Y (baseline)
|
|
||||||
<< DcLuminanceCodesPerBitsize
|
|
||||||
<< DcLuminanceValues;
|
|
||||||
bitWriter << 0x10 // highest 4 bits: 1 => AC, lowest 4 bits: 0 => Y (baseline)
|
|
||||||
<< AcLuminanceCodesPerBitsize
|
|
||||||
<< AcLuminanceValues;
|
|
||||||
|
|
||||||
// compute actual Huffman code tables (see Jon's code for precalculated tables)
|
|
||||||
BitCode huffmanLuminanceDC[256];
|
|
||||||
BitCode huffmanLuminanceAC[256];
|
|
||||||
generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC);
|
|
||||||
generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC);
|
|
||||||
|
|
||||||
// chrominance is only relevant for color images
|
|
||||||
BitCode huffmanChrominanceDC[256];
|
|
||||||
BitCode huffmanChrominanceAC[256];
|
|
||||||
if (isRGB)
|
|
||||||
{
|
|
||||||
// store luminance's DC+AC Huffman table definitions
|
|
||||||
bitWriter << 0x01 // highest 4 bits: 0 => DC, lowest 4 bits: 1 => Cr,Cb (baseline)
|
|
||||||
<< DcChrominanceCodesPerBitsize
|
|
||||||
<< DcChrominanceValues;
|
|
||||||
bitWriter << 0x11 // highest 4 bits: 1 => AC, lowest 4 bits: 1 => Cr,Cb (baseline)
|
|
||||||
<< AcChrominanceCodesPerBitsize
|
|
||||||
<< AcChrominanceValues;
|
|
||||||
|
|
||||||
// compute actual Huffman code tables (see Jon's code for precalculated tables)
|
|
||||||
generateHuffmanTable(DcChrominanceCodesPerBitsize, DcChrominanceValues, huffmanChrominanceDC);
|
|
||||||
generateHuffmanTable(AcChrominanceCodesPerBitsize, AcChrominanceValues, huffmanChrominanceAC);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// start of scan (there is only a single scan for baseline JPEGs)
|
|
||||||
bitWriter.addMarker(0xDA, 2+1+2*numComponents+3); // 2 bytes for the length field, 1 byte for number of components,
|
|
||||||
// then 2 bytes for each component and 3 bytes for spectral selection
|
|
||||||
|
|
||||||
// assign Huffman tables to each component
|
|
||||||
bitWriter << numComponents;
|
|
||||||
for (auto id = 1; id <= numComponents; id++)
|
|
||||||
// highest 4 bits: DC Huffman table, lowest 4 bits: AC Huffman table
|
|
||||||
bitWriter << id << (id == 1 ? 0x00 : 0x11); // Y: tables 0 for DC and AC; Cb + Cr: tables 1 for DC and AC
|
|
||||||
|
|
||||||
// constant values for our baseline JPEGs (which have a single sequential scan)
|
|
||||||
static const uint8_t Spectral[3] = { 0, 63, 0 }; // spectral selection: must be from 0 to 63; successive approximation must be 0
|
|
||||||
bitWriter << Spectral;
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// adjust quantization tables with AAN scaling factors to simplify DCT
|
|
||||||
float scaledLuminance [8*8];
|
|
||||||
float scaledChrominance[8*8];
|
|
||||||
for (auto i = 0; i < 8*8; i++)
|
|
||||||
{
|
|
||||||
auto row = ZigZagInv[i] / 8; // same as ZigZagInv[i] >> 3
|
|
||||||
auto column = ZigZagInv[i] % 8; // same as ZigZagInv[i] & 7
|
|
||||||
|
|
||||||
// scaling constants for AAN DCT algorithm: AanScaleFactors[0] = 1, AanScaleFactors[k=1..7] = cos(k*PI/16) * sqrt(2)
|
|
||||||
static const float AanScaleFactors[8] = { 1, 1.387039845f, 1.306562965f, 1.175875602f, 1, 0.785694958f, 0.541196100f, 0.275899379f };
|
|
||||||
auto factor = 1 / (AanScaleFactors[row] * AanScaleFactors[column] * 8);
|
|
||||||
scaledLuminance [ZigZagInv[i]] = factor / quantLuminance [i];
|
|
||||||
scaledChrominance[ZigZagInv[i]] = factor / quantChrominance[i];
|
|
||||||
// if you really want JPEGs that are bitwise identical to Jon Olick's code then you need slightly different formulas (note: sqrt(8) = 2.828427125f)
|
|
||||||
//static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; // line 240 of jo_jpeg.cpp
|
|
||||||
//scaledLuminance [ZigZagInv[i]] = 1 / (quantLuminance [i] * aasf[row] * aasf[column]); // lines 266-267 of jo_jpeg.cpp
|
|
||||||
//scaledChrominance[ZigZagInv[i]] = 1 / (quantChrominance[i] * aasf[row] * aasf[column]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// precompute JPEG codewords for quantized DCT
|
|
||||||
BitCode codewordsArray[2 * CodeWordLimit]; // note: quantized[i] is found at codewordsArray[quantized[i] + CodeWordLimit]
|
|
||||||
BitCode* codewords = &codewordsArray[CodeWordLimit]; // allow negative indices, so quantized[i] is at codewords[quantized[i]]
|
|
||||||
uint8_t numBits = 1; // each codeword has at least one bit (value == 0 is undefined)
|
|
||||||
int32_t mask = 1; // mask is always 2^numBits - 1, initial value 2^1-1 = 2-1 = 1
|
|
||||||
for (int16_t value = 1; value < CodeWordLimit; value++)
|
|
||||||
{
|
|
||||||
// numBits = position of highest set bit (ignoring the sign)
|
|
||||||
// mask = (2^numBits) - 1
|
|
||||||
if (value > mask) // one more bit ?
|
|
||||||
{
|
|
||||||
numBits++;
|
|
||||||
mask = (mask << 1) | 1; // append a set bit
|
|
||||||
}
|
|
||||||
codewords[-value] = BitCode(mask - value, numBits); // note that I use a negative index => codewords[-value] = codewordsArray[CodeWordLimit value]
|
|
||||||
codewords[+value] = BitCode( value, numBits);
|
|
||||||
}
|
|
||||||
|
|
||||||
// just convert image data from void*
|
|
||||||
auto pixels = (const uint8_t*)pixels_;
|
|
||||||
|
|
||||||
// the next two variables are frequently used when checking for image borders
|
|
||||||
const auto maxWidth = width - 1; // "last row"
|
|
||||||
const auto maxHeight = height - 1; // "bottom line"
|
|
||||||
|
|
||||||
// process MCUs (minimum codes units) => image is subdivided into a grid of 8x8 or 16x16 tiles
|
|
||||||
const auto sampling = downsample ? 2 : 1; // 1x1 or 2x2 sampling
|
|
||||||
const auto mcuSize = 8 * sampling;
|
|
||||||
|
|
||||||
// average color of the previous MCU
|
|
||||||
int16_t lastYDC = 0, lastCbDC = 0, lastCrDC = 0;
|
|
||||||
// convert from RGB to YCbCr
|
|
||||||
float Y[8][8], Cb[8][8], Cr[8][8];
|
|
||||||
|
|
||||||
for (auto mcuY = 0; mcuY < height; mcuY += mcuSize) // each step is either 8 or 16 (=mcuSize)
|
|
||||||
for (auto mcuX = 0; mcuX < width; mcuX += mcuSize)
|
|
||||||
{
|
|
||||||
// YCbCr 4:4:4 format: each MCU is a 8x8 block - the same applies to grayscale images, too
|
|
||||||
// YCbCr 4:2:0 format: each MCU represents a 16x16 block, stored as 4x 8x8 Y-blocks plus 1x 8x8 Cb and 1x 8x8 Cr block)
|
|
||||||
for (auto blockY = 0; blockY < mcuSize; blockY += 8) // iterate once (YCbCr444 and grayscale) or twice (YCbCr420)
|
|
||||||
for (auto blockX = 0; blockX < mcuSize; blockX += 8)
|
|
||||||
{
|
|
||||||
// now we finally have an 8x8 block ...
|
|
||||||
for (auto deltaY = 0; deltaY < 8; deltaY++)
|
|
||||||
{
|
|
||||||
auto column = minimum(mcuX + blockX , maxWidth); // must not exceed image borders, replicate last row/column if needed
|
|
||||||
auto row = minimum(mcuY + blockY + deltaY, maxHeight);
|
|
||||||
for (auto deltaX = 0; deltaX < 8; deltaX++)
|
|
||||||
{
|
|
||||||
// find actual pixel position within the current image
|
|
||||||
auto pixelPos = row * int(width) + column; // the cast ensures that we don't run into multiplication overflows
|
|
||||||
if (column < maxWidth)
|
|
||||||
column++;
|
|
||||||
|
|
||||||
// grayscale images have solely a Y channel which can be easily derived from the input pixel by shifting it by 128
|
|
||||||
if (!isRGB)
|
|
||||||
{
|
|
||||||
Y[deltaY][deltaX] = pixels[pixelPos] - 128.f;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RGB: 3 bytes per pixel (whereas grayscale images have only 1 byte per pixel)
|
|
||||||
auto r = pixels[3 * pixelPos ];
|
|
||||||
auto g = pixels[3 * pixelPos + 1];
|
|
||||||
auto b = pixels[3 * pixelPos + 2];
|
|
||||||
|
|
||||||
Y [deltaY][deltaX] = rgb2y (r, g, b) - 128; // again, the JPEG standard requires Y to be shifted by 128
|
|
||||||
// YCbCr444 is easy - the more complex YCbCr420 has to be computed about 20 lines below in a second pass
|
|
||||||
if (!downsample)
|
|
||||||
{
|
|
||||||
Cb[deltaY][deltaX] = rgb2cb(r, g, b); // standard RGB-to-YCbCr conversion
|
|
||||||
Cr[deltaY][deltaX] = rgb2cr(r, g, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// encode Y channel
|
|
||||||
lastYDC = encodeBlock(bitWriter, Y, scaledLuminance, lastYDC, huffmanLuminanceDC, huffmanLuminanceAC, codewords);
|
|
||||||
// Cb and Cr are encoded about 50 lines below
|
|
||||||
}
|
|
||||||
|
|
||||||
// grayscale images don't need any Cb and Cr information
|
|
||||||
if (!isRGB)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// ////////////////////////////////////////
|
|
||||||
// the following lines are only relevant for YCbCr420:
|
|
||||||
// average/downsample chrominance of four pixels while respecting the image borders
|
|
||||||
if (downsample)
|
|
||||||
for (short deltaY = 7; downsample && deltaY >= 0; deltaY--) // iterating loop in reverse increases cache read efficiency
|
|
||||||
{
|
|
||||||
auto row = minimum(mcuY + 2*deltaY, maxHeight); // each deltaX/Y step covers a 2x2 area
|
|
||||||
auto column = mcuX; // column is updated inside next loop
|
|
||||||
auto pixelPos = (row * int(width) + column) * 3; // numComponents = 3
|
|
||||||
|
|
||||||
// deltas (in bytes) to next row / column, must not exceed image borders
|
|
||||||
auto rowStep = (row < maxHeight) ? 3 * int(width) : 0; // always numComponents*width except for bottom line
|
|
||||||
auto columnStep = (column < maxWidth ) ? 3 : 0; // always numComponents except for rightmost pixel
|
|
||||||
|
|
||||||
for (short deltaX = 0; deltaX < 8; deltaX++)
|
|
||||||
{
|
|
||||||
// let's add all four samples (2x2 area)
|
|
||||||
auto right = pixelPos + columnStep;
|
|
||||||
auto down = pixelPos + rowStep;
|
|
||||||
auto downRight = pixelPos + columnStep + rowStep;
|
|
||||||
|
|
||||||
// note: cast from 8 bits to >8 bits to avoid overflows when adding
|
|
||||||
auto r = short(pixels[pixelPos ]) + pixels[right ] + pixels[down ] + pixels[downRight ];
|
|
||||||
auto g = short(pixels[pixelPos + 1]) + pixels[right + 1] + pixels[down + 1] + pixels[downRight + 1];
|
|
||||||
auto b = short(pixels[pixelPos + 2]) + pixels[right + 2] + pixels[down + 2] + pixels[downRight + 2];
|
|
||||||
|
|
||||||
// convert to Cb and Cr
|
|
||||||
Cb[deltaY][deltaX] = rgb2cb(r, g, b) / 4; // I still have to divide r,g,b by 4 to get their average values
|
|
||||||
Cr[deltaY][deltaX] = rgb2cr(r, g, b) / 4; // it's a bit faster if done AFTER CbCr conversion
|
|
||||||
|
|
||||||
// step forward to next 2x2 area
|
|
||||||
pixelPos += 2*3; // 2 pixels => 6 bytes (2*numComponents)
|
|
||||||
column += 2;
|
|
||||||
|
|
||||||
// reached right border ?
|
|
||||||
if (column >= maxWidth)
|
|
||||||
{
|
|
||||||
columnStep = 0;
|
|
||||||
pixelPos = ((row + 1) * int(width) - 1) * 3; // same as (row * width + maxWidth) * numComponents => current's row last pixel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // end of YCbCr420 code for Cb and Cr
|
|
||||||
|
|
||||||
// encode Cb and Cr
|
|
||||||
lastCbDC = encodeBlock(bitWriter, Cb, scaledChrominance, lastCbDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords);
|
|
||||||
lastCrDC = encodeBlock(bitWriter, Cr, scaledChrominance, lastCrDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords);
|
|
||||||
}
|
|
||||||
|
|
||||||
bitWriter.flush(); // now image is completely encoded, write any bits still left in the buffer
|
|
||||||
|
|
||||||
// ///////////////////////////
|
|
||||||
// EOI marker
|
|
||||||
bitWriter << 0xFF << 0xD9; // this marker has no length, therefore I can't use addMarker()
|
|
||||||
return true;
|
|
||||||
} // writeJpeg()
|
|
||||||
} // namespace TooJpeg
|
|
||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
// //////////////////////////////////////////////////////////
|
|
||||||
// toojpeg.h
|
|
||||||
// written by Stephan Brumme, 2018-2019
|
|
||||||
// see https://create.stephan-brumme.com/toojpeg/
|
|
||||||
//
|
|
||||||
|
|
||||||
// This is a compact baseline JPEG/JFIF writer, written in C++ (but looks like C for the most part).
|
|
||||||
// Its interface has only one function: writeJpeg() - and that's it !
|
|
||||||
//
|
|
||||||
// basic example:
|
|
||||||
// => create an image with any content you like, e.g. 1024x768, RGB = 3 bytes per pixel
|
|
||||||
// auto pixels = new unsigned char[1024*768*3];
|
|
||||||
// => you need to define a callback that receives the compressed data byte-by-byte from my JPEG writer
|
|
||||||
// void myOutput(unsigned char oneByte) { fputc(oneByte, myFileHandle); } // save byte to file
|
|
||||||
// => let's go !
|
|
||||||
// TooJpeg::writeJpeg(myOutput, mypixels, 1024, 768);
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
namespace TooJpeg
|
|
||||||
{
|
|
||||||
// write one byte (to disk, memory, ...)
|
|
||||||
typedef void (*WRITE_ONE_BYTE)(unsigned char);
|
|
||||||
// this callback is called for every byte generated by the encoder and behaves similar to fputc
|
|
||||||
// if you prefer stylish C++11 syntax then it can be a lambda, too:
|
|
||||||
// auto myOutput = [](unsigned char oneByte) { fputc(oneByte, output); };
|
|
||||||
|
|
||||||
// output - callback that stores a single byte (writes to disk, memory, ...)
|
|
||||||
// pixels - stored in RGB format or grayscale, stored from upper-left to lower-right
|
|
||||||
// width,height - image size
|
|
||||||
// isRGB - true if RGB format (3 bytes per pixel); false if grayscale (1 byte per pixel)
|
|
||||||
// quality - between 1 (worst) and 100 (best)
|
|
||||||
// downsample - if true then YCbCr 4:2:0 format is used (smaller size, minor quality loss) instead of 4:4:4, not relevant for grayscale
|
|
||||||
// comment - optional JPEG comment (0/NULL if no comment), must not contain ASCII code 0xFF
|
|
||||||
bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels, unsigned short width, unsigned short height,
|
|
||||||
bool isRGB = true, unsigned char quality = 90, bool downsample = false, const char* comment = nullptr);
|
|
||||||
} // namespace TooJpeg
|
|
||||||
|
|
||||||
// My main inspiration was Jon Olick's Minimalistic JPEG writer
|
|
||||||
// ( https://www.jonolick.com/code.html => direct link is https://www.jonolick.com/uploads/7/9/2/1/7921194/jo_jpeg.cpp ).
|
|
||||||
// However, his code documentation is quite sparse - probably because it wasn't written from scratch and is (quote:) "based on a javascript jpeg writer",
|
|
||||||
// most likely Andreas Ritter's code: https://github.com/eugeneware/jpeg-js/blob/master/lib/encoder.js
|
|
||||||
//
|
|
||||||
// Therefore I wrote the whole lib from scratch and tried hard to add tons of comments to my code, especially describing where all those magic numbers come from.
|
|
||||||
// And I managed to remove the need for any external includes ...
|
|
||||||
// yes, that's right: my library has no (!) includes at all, not even #include <stdlib.h>
|
|
||||||
// Depending on your callback WRITE_ONE_BYTE, the library writes either to disk, or in-memory, or wherever you wish.
|
|
||||||
// Moreover, no dynamic memory allocations are performed, just a few bytes on the stack.
|
|
||||||
//
|
|
||||||
// In contrast to Jon's code, compression can be significantly improved in many use cases:
|
|
||||||
// a) grayscale JPEG images need just a single Y channel, no need to save the superfluous Cb + Cr channels
|
|
||||||
// b) YCbCr 4:2:0 downsampling is often about 20% more efficient (=smaller) than the default YCbCr 4:4:4 with only little visual loss
|
|
||||||
//
|
|
||||||
// TooJpeg 1.2+ compresses about twice as fast as jo_jpeg (and about half as fast as libjpeg-turbo).
|
|
||||||
// A few benchmark numbers can be found on my website https://create.stephan-brumme.com/toojpeg/#benchmark
|
|
||||||
//
|
|
||||||
// Last but not least you can optionally add a JPEG comment.
|
|
||||||
//
|
|
||||||
// Your C++ compiler needs to support a reasonable subset of C++11 (g++ 4.7 or Visual C++ 2013 are sufficient).
|
|
||||||
// I haven't tested the code on big-endian systems or anything that smells like an apple.
|
|
||||||
//
|
|
||||||
// USE AT YOUR OWN RISK. Because you are a brave soul :-)
|
|
||||||
@@ -2,15 +2,18 @@
|
|||||||
#include "asio.h"
|
#include "asio.h"
|
||||||
#include "handle.h"
|
#include "handle.h"
|
||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
|
#include <span>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "cfg/configurator.h"
|
||||||
|
#include "cfg/screen_resize.h"
|
||||||
|
|
||||||
#include <ks.h>
|
#include <ks.h>
|
||||||
#include <ksmedia.h>
|
#include <ksmedia.h>
|
||||||
|
|
||||||
#include "cfg/configurator.h"
|
|
||||||
#include "hooks/audio/audio.h"
|
#include "hooks/audio/audio.h"
|
||||||
#include "hooks/audio/mme.h"
|
#include "hooks/audio/mme.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "touch/native/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "util/cpuutils.h"
|
#include "util/cpuutils.h"
|
||||||
@@ -28,14 +31,17 @@ namespace games::gitadora {
|
|||||||
|
|
||||||
// settings
|
// settings
|
||||||
bool TWOCHANNEL = false;
|
bool TWOCHANNEL = false;
|
||||||
|
bool DISABLE_FRAME_LIMITER = false;
|
||||||
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
||||||
bool P1_LEFTY = false;
|
bool P1_LEFTY = false;
|
||||||
bool P2_LEFTY = false;
|
bool P2_LEFTY = false;
|
||||||
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||||
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
||||||
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
||||||
|
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
||||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||||
bool ALLOW_REALTEK_AUDIO = false;
|
bool ALLOW_REALTEK_AUDIO = false;
|
||||||
|
bool NATIVE_TOUCH = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Prevent GitaDora from creating folders on F drive
|
* Prevent GitaDora from creating folders on F drive
|
||||||
@@ -61,6 +67,46 @@ namespace games::gitadora {
|
|||||||
|
|
||||||
return CreateDirectoryA(lpPathName, lpSecurityAttributes);
|
return CreateDirectoryA(lpPathName, lpSecurityAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// libshare-pj paces mainloop with separate 12 ms and 16 ms waits. these waits
|
||||||
|
// interfere with the game's normal display synchronization on modern Windows
|
||||||
|
// and can hold a nominal 60 FPS game near 58 FPS. locate the instruction
|
||||||
|
// sequences at runtime so the fix does not depend on per-version file offsets.
|
||||||
|
static void disable_mainloop_frame_limiter(HMODULE sharepj_module) {
|
||||||
|
if (!sharepj_module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// both limiters have the same shape, only the millisecond target xx differs
|
||||||
|
// (0Ch for the 12 ms limiter, 10h for the 16 ms one):
|
||||||
|
//
|
||||||
|
// 48 83 F8 xx: cmp rax, xx; compare elapsed frame time with the target
|
||||||
|
// 73 10: jae +10h; skip the wait once the target has elapsed
|
||||||
|
// B9 xx 00 00 00: mov ecx, xx; load the target
|
||||||
|
// 48 2B C8: sub rcx, rax; calculate the remaining wait time
|
||||||
|
// 74 06: je +6h; skip the following six-byte Sleep call if no wait remains
|
||||||
|
//
|
||||||
|
// changing jae (73h) to jmp (EBh) makes each block always take its existing
|
||||||
|
// skip path, which bypasses only the associated Sleep call and leaves the 10h
|
||||||
|
// branch displacement and every other wait untouched.
|
||||||
|
const auto limiter_12ms_disabled = replace_pattern(
|
||||||
|
sharepj_module,
|
||||||
|
"4883F80C7310B90C000000482BC87406",
|
||||||
|
"????????EB??????????????????????", 0, 0);
|
||||||
|
const auto limiter_16ms_disabled = replace_pattern(
|
||||||
|
sharepj_module,
|
||||||
|
"4883F8107310B910000000482BC87406",
|
||||||
|
"????????EB??????????????????????", 0, 0);
|
||||||
|
|
||||||
|
if (!limiter_12ms_disabled || !limiter_16ms_disabled) {
|
||||||
|
log_fatal(
|
||||||
|
"gitadora",
|
||||||
|
"failed to disable libshare-pj mainloop frame limiter (-gdnoframelimiter), ensure patch is not already applied");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("gitadora", "successfully disabled libshare-pj mainloop frame limiter (-gdnoframelimiter)");
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -270,9 +316,21 @@ namespace games::gitadora {
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
if (!GRAPHICS_WINDOWED) {
|
if (!GRAPHICS_WINDOWED) {
|
||||||
log_fatal(
|
if (D3D9_ADAPTER.has_value()) {
|
||||||
|
log_fatal(
|
||||||
|
"gitadora",
|
||||||
|
"arena model: fullscreen two-window mode cannot use -monitor; "
|
||||||
|
"use -w for borderless windows instead");
|
||||||
|
}
|
||||||
|
ARENA_TWO_HEAD_EXCLUSIVE = true;
|
||||||
|
log_info(
|
||||||
"gitadora",
|
"gitadora",
|
||||||
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4");
|
"arena model: native two-head fullscreen adapter-group mode "
|
||||||
|
"(MAIN + SMALL; LEFT/RIGHT are virtual)");
|
||||||
|
} else {
|
||||||
|
log_info(
|
||||||
|
"gitadora",
|
||||||
|
"arena model: two-window mode uses windowed rendering");
|
||||||
}
|
}
|
||||||
log_info("gitadora", "arena model: two-window mode");
|
log_info("gitadora", "arena model: two-window mode");
|
||||||
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
|
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
|
||||||
@@ -298,10 +356,18 @@ namespace games::gitadora {
|
|||||||
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
|
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
|
||||||
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
|
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
|
||||||
|
|
||||||
// cached primary real monitor: its path + source/target mode entries.
|
// Cached real monitor paths and their source/target mode entries. modeInfoIdx values are
|
||||||
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
|
// renumbered so the cache is self-contained. The normal single-head emulation keeps only
|
||||||
|
// MAIN; the two-head adapter-group mode keeps MAIN and SMALL as real heads.
|
||||||
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
|
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
|
||||||
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
|
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
|
||||||
|
static DISPLAYCONFIG_PATH_INFO real_small_path = {};
|
||||||
|
static DISPLAYCONFIG_MODE_INFO real_small_modes[2] = {}; // [0]=source, [1]=target
|
||||||
|
static bool real_small_path_available = false;
|
||||||
|
|
||||||
|
static bool is_two_head_exclusive() {
|
||||||
|
return ARENA_TWO_HEAD_EXCLUSIVE && !GRAPHICS_WINDOWED;
|
||||||
|
}
|
||||||
|
|
||||||
// fake monitors appended after the real ones. the game classifies monitors
|
// fake monitors appended after the real ones. the game classifies monitors
|
||||||
// by outputTechnology + connectorInstance:
|
// by outputTechnology + connectorInstance:
|
||||||
@@ -325,12 +391,31 @@ namespace games::gitadora {
|
|||||||
// adapter to a swap chain role via its DisplayConfig connector instance,
|
// adapter to a swap chain role via its DisplayConfig connector instance,
|
||||||
// so the entries here must be listed in the same order the wrapper enumerates
|
// so the entries here must be listed in the same order the wrapper enumerates
|
||||||
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
|
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
|
||||||
static constexpr FakeMonitor FAKE_MONITORS[] = {
|
// Normal single-head emulation: every non-MAIN role is fake.
|
||||||
|
static constexpr FakeMonitor FAKE_MONITORS_SINGLE_HEAD[] = {
|
||||||
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
|
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
|
||||||
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
|
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
|
||||||
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
|
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
|
||||||
};
|
};
|
||||||
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
|
|
||||||
|
// Two-head fullscreen: adapters 0/1 are the real MAIN/SMALL group heads.
|
||||||
|
// Keep only LEFT/RIGHT fake, with their adapter ids matching the D3D9 wrapper.
|
||||||
|
static constexpr FakeMonitor FAKE_MONITORS_TWO_HEAD[] = {
|
||||||
|
{ 2, 1080, 1920, -200000, -200000, 0 }, // left (DP connector instance 0)
|
||||||
|
{ 3, 1080, 1920, -300000, -300000, 1 }, // right (DP connector instance 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::span<const FakeMonitor> get_fake_monitors() {
|
||||||
|
if (is_two_head_exclusive()) {
|
||||||
|
return FAKE_MONITORS_TWO_HEAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FAKE_MONITORS_SINGLE_HEAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UINT32 real_monitor_count() {
|
||||||
|
return is_two_head_exclusive() ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
// call QueryDisplayConfig once, keep only the primary monitor's path and its
|
// call QueryDisplayConfig once, keep only the primary monitor's path and its
|
||||||
// two referenced modes (source + target). modeInfoIdx values are rewritten to
|
// two referenced modes (source + target). modeInfoIdx values are rewritten to
|
||||||
@@ -368,13 +453,55 @@ namespace games::gitadora {
|
|||||||
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
|
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (primary->targetInfo.modeInfoIdx >= all_modes.size()) {
|
||||||
|
log_fatal("gitadora", "cache_primary_monitor_info: primary target mode is missing");
|
||||||
|
}
|
||||||
|
|
||||||
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
|
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
|
||||||
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
|
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
|
||||||
real_primary_path = *primary;
|
real_primary_path = *primary;
|
||||||
real_primary_path.sourceInfo.modeInfoIdx = 0;
|
real_primary_path.sourceInfo.modeInfoIdx = 0;
|
||||||
real_primary_path.targetInfo.modeInfoIdx = 1;
|
real_primary_path.targetInfo.modeInfoIdx = 1;
|
||||||
|
|
||||||
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
|
real_small_path_available = false;
|
||||||
|
if (is_two_head_exclusive()) {
|
||||||
|
if (all_paths.size() != 2) {
|
||||||
|
log_fatal(
|
||||||
|
"gitadora",
|
||||||
|
"two-head fullscreen mode requires exactly two active display paths, found {}",
|
||||||
|
all_paths.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto secondary = std::find_if(all_paths.begin(), all_paths.end(),
|
||||||
|
[&](const auto &p) {
|
||||||
|
return &p != &*primary;
|
||||||
|
});
|
||||||
|
if (secondary == all_paths.end() ||
|
||||||
|
secondary->sourceInfo.modeInfoIdx >= all_modes.size() ||
|
||||||
|
secondary->targetInfo.modeInfoIdx >= all_modes.size()) {
|
||||||
|
log_fatal("gitadora", "could not identify the physical SMALL monitor");
|
||||||
|
}
|
||||||
|
const LUID primary_adapter = primary->sourceInfo.adapterId;
|
||||||
|
const LUID secondary_adapter = secondary->sourceInfo.adapterId;
|
||||||
|
const bool same_adapter = primary_adapter.HighPart == secondary_adapter.HighPart &&
|
||||||
|
primary_adapter.LowPart == secondary_adapter.LowPart;
|
||||||
|
if (!same_adapter) {
|
||||||
|
log_fatal(
|
||||||
|
"gitadora",
|
||||||
|
"MAIN and SMALL must use the same display adapter");
|
||||||
|
}
|
||||||
|
|
||||||
|
real_small_modes[0] = all_modes[secondary->sourceInfo.modeInfoIdx];
|
||||||
|
real_small_modes[1] = all_modes[secondary->targetInfo.modeInfoIdx];
|
||||||
|
real_small_path = *secondary;
|
||||||
|
real_small_path.sourceInfo.modeInfoIdx = 2;
|
||||||
|
real_small_path.targetInfo.modeInfoIdx = 3;
|
||||||
|
real_small_path_available = true;
|
||||||
|
|
||||||
|
log_info("gitadora", "cache_primary_monitor_info: cached real MAIN and SMALL monitors");
|
||||||
|
} else {
|
||||||
|
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -389,33 +516,36 @@ namespace games::gitadora {
|
|||||||
static std::once_flag populate_once;
|
static std::once_flag populate_once;
|
||||||
std::call_once(populate_once, cache_primary_monitor_info);
|
std::call_once(populate_once, cache_primary_monitor_info);
|
||||||
|
|
||||||
// always report exactly 1 real + N fake monitors
|
const auto fake_monitors = get_fake_monitors();
|
||||||
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
|
||||||
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
const UINT32 real_count = real_monitor_count();
|
||||||
|
*pNumPathArrayElements = real_count + fake_count;
|
||||||
|
*pNumModeInfoArrayElements = (real_count + fake_count) * 2;
|
||||||
|
|
||||||
log_info(
|
log_info(
|
||||||
"gitadora",
|
"gitadora",
|
||||||
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
|
"GetDisplayConfigBufferSizes: {} real path(s) + {} fake monitor(s)",
|
||||||
FAKE_MONITOR_COUNT);
|
real_count,
|
||||||
|
fake_count);
|
||||||
|
|
||||||
return ERROR_SUCCESS;
|
return ERROR_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
|
// Write a fake monitor after the real paths. The cache is packed as [source, target]
|
||||||
// assumption): index 0 in both arrays holds the cached primary real monitor, so
|
// mode pairs, so each path consumes two mode entries.
|
||||||
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
|
|
||||||
static void insert_fake_monitor(
|
static void insert_fake_monitor(
|
||||||
DISPLAYCONFIG_PATH_INFO *paths,
|
DISPLAYCONFIG_PATH_INFO *paths,
|
||||||
DISPLAYCONFIG_MODE_INFO *modes,
|
DISPLAYCONFIG_MODE_INFO *modes,
|
||||||
UINT32 i)
|
const FakeMonitor &m,
|
||||||
|
UINT32 path_index,
|
||||||
|
UINT32 source_mode_index)
|
||||||
{
|
{
|
||||||
const FakeMonitor &m = FAKE_MONITORS[i];
|
const UINT32 src_idx = source_mode_index;
|
||||||
const UINT32 src_idx = 2 + i * 2;
|
|
||||||
const UINT32 tgt_idx = src_idx + 1;
|
const UINT32 tgt_idx = src_idx + 1;
|
||||||
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
|
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
|
||||||
const UINT32 uid = static_cast<UINT32>(-m.id);
|
const UINT32 uid = static_cast<UINT32>(-m.id);
|
||||||
|
|
||||||
paths[1 + i] = {
|
paths[path_index] = {
|
||||||
.sourceInfo = {
|
.sourceInfo = {
|
||||||
.adapterId = adapter_id,
|
.adapterId = adapter_id,
|
||||||
.id = uid,
|
.id = uid,
|
||||||
@@ -473,21 +603,35 @@ namespace games::gitadora {
|
|||||||
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
|
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
|
||||||
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
|
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
|
||||||
{
|
{
|
||||||
// copy cached primary real monitor into caller buffers at index 0
|
// Copy cached real monitor paths into the caller buffers.
|
||||||
pathArray[0] = real_primary_path;
|
pathArray[0] = real_primary_path;
|
||||||
modeInfoArray[0] = real_primary_modes[0];
|
modeInfoArray[0] = real_primary_modes[0];
|
||||||
modeInfoArray[1] = real_primary_modes[1];
|
modeInfoArray[1] = real_primary_modes[1];
|
||||||
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
if (is_two_head_exclusive() && real_small_path_available) {
|
||||||
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
pathArray[1] = real_small_path;
|
||||||
|
modeInfoArray[2] = real_small_modes[0];
|
||||||
|
modeInfoArray[3] = real_small_modes[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto fake_monitors = get_fake_monitors();
|
||||||
|
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
|
||||||
|
const UINT32 real_count = real_monitor_count();
|
||||||
|
*numPathArrayElements = real_count + fake_count;
|
||||||
|
*numModeInfoArrayElements = (real_count + fake_count) * 2;
|
||||||
if (currentTopologyId != nullptr) {
|
if (currentTopologyId != nullptr) {
|
||||||
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
|
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
|
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
|
||||||
|
|
||||||
// append fake monitors after the real one
|
// Append fake monitors after the real path(s).
|
||||||
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
|
for (UINT32 i = 0; i < fake_count; i++) {
|
||||||
insert_fake_monitor(pathArray, modeInfoArray, i);
|
insert_fake_monitor(
|
||||||
|
pathArray,
|
||||||
|
modeInfoArray,
|
||||||
|
fake_monitors[i],
|
||||||
|
real_count + i,
|
||||||
|
real_count * 2 + i * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ERROR_SUCCESS;
|
return ERROR_SUCCESS;
|
||||||
@@ -518,7 +662,8 @@ namespace games::gitadora {
|
|||||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||||
const LONG fake_id = -id;
|
const LONG fake_id = -id;
|
||||||
UINT32 conn_inst = 0xff;
|
UINT32 conn_inst = 0xff;
|
||||||
for (const auto& f : FAKE_MONITORS) {
|
const auto fake_monitors = get_fake_monitors();
|
||||||
|
for (const auto &f : fake_monitors) {
|
||||||
if (f.id == fake_id) {
|
if (f.id == fake_id) {
|
||||||
conn_inst = f.connector_instance;
|
conn_inst = f.connector_instance;
|
||||||
break;
|
break;
|
||||||
@@ -541,18 +686,27 @@ namespace games::gitadora {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// override the cached primary real monitor target info to look like HDMI/0
|
// Override MAIN to HDMI/0 and retag the real second head as the DP/2 SMALL
|
||||||
|
// display expected by the cabinet software in two-head fullscreen mode.
|
||||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||||
const auto& target = real_primary_path.targetInfo;
|
const auto target_matches = [&](const DISPLAYCONFIG_PATH_TARGET_INFO &target) {
|
||||||
if (target.id == targetName->header.id &&
|
return target.id == targetName->header.id &&
|
||||||
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
||||||
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
|
target.adapterId.LowPart == targetName->header.adapterId.LowPart;
|
||||||
{
|
};
|
||||||
|
if (target_matches(real_primary_path.targetInfo)) {
|
||||||
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
|
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
|
||||||
targetName->connectorInstance = 0;
|
targetName->connectorInstance = 0;
|
||||||
log_info("gitadora",
|
log_info("gitadora",
|
||||||
"overriding primary monitor (id={}) to pretend to be HDMI",
|
"overriding primary monitor (id={}) to pretend to be HDMI",
|
||||||
targetName->header.id);
|
targetName->header.id);
|
||||||
|
} else if (is_two_head_exclusive() && real_small_path_available &&
|
||||||
|
target_matches(real_small_path.targetInfo)) {
|
||||||
|
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
|
||||||
|
targetName->connectorInstance = 2;
|
||||||
|
log_info("gitadora",
|
||||||
|
"overriding secondary monitor (id={}) to pretend to be SMALL DP/2",
|
||||||
|
targetName->header.id);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -575,6 +729,12 @@ namespace games::gitadora {
|
|||||||
HMODULE system_module = libutils::try_module("libsystem.dll");
|
HMODULE system_module = libutils::try_module("libsystem.dll");
|
||||||
|
|
||||||
// patches
|
// patches
|
||||||
|
#ifdef SPICE64
|
||||||
|
if (DISABLE_FRAME_LIMITER && !is_arena_model()) {
|
||||||
|
disable_mainloop_frame_limiter(sharepj_module);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc(
|
detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc(
|
||||||
sharepj_module, "eam_network_detected_ip_change"));
|
sharepj_module, "eam_network_detected_ip_change"));
|
||||||
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
|
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
|
||||||
@@ -650,29 +810,38 @@ namespace games::gitadora {
|
|||||||
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// monitor/touch hooks (windowed or full screen)
|
// touch injection drives mouse-as-touch and API touch for the subscreen,
|
||||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
// no matter whether it is drawn by the overlay (single-window mode) or by
|
||||||
// enable touch hook for subscreen overlay
|
// the dedicated SMALL window
|
||||||
|
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
|
if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
|
// the legacy fallback can only feed the subscreen overlay
|
||||||
|
wintouchemu::FORCE = true;
|
||||||
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
|
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
#if !SPICE_XP
|
#if !SPICE_XP
|
||||||
|
|
||||||
if (!GRAPHICS_WINDOWED) {
|
if (!GRAPHICS_WINDOWED &&
|
||||||
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
|
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || ARENA_TWO_HEAD_EXCLUSIVE)) {
|
||||||
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
|
if (ARENA_TWO_HEAD_EXCLUSIVE) {
|
||||||
GetDisplayConfigBufferSizes_orig =
|
log_info(
|
||||||
detour::iat_try("GetDisplayConfigBufferSizes",
|
"gitadora",
|
||||||
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
"exposing physical MAIN/SMALL and virtual LEFT/RIGHT");
|
||||||
QueryDisplayConfig_orig =
|
|
||||||
detour::iat_try("QueryDisplayConfig",
|
|
||||||
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
|
|
||||||
DisplayConfigGetDeviceInfo_orig =
|
|
||||||
detour::iat_try("DisplayConfigGetDeviceInfo",
|
|
||||||
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
|
||||||
}
|
}
|
||||||
#endif
|
GetDisplayConfigBufferSizes_orig =
|
||||||
|
detour::iat_try("GetDisplayConfigBufferSizes",
|
||||||
|
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
||||||
|
QueryDisplayConfig_orig =
|
||||||
|
detour::iat_try("QueryDisplayConfig",
|
||||||
|
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
|
||||||
|
DisplayConfigGetDeviceInfo_orig =
|
||||||
|
detour::iat_try("DisplayConfigGetDeviceInfo",
|
||||||
|
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// window patch
|
// window patch
|
||||||
|
|||||||
@@ -13,14 +13,21 @@ namespace games::gitadora {
|
|||||||
|
|
||||||
// settings
|
// settings
|
||||||
extern bool TWOCHANNEL;
|
extern bool TWOCHANNEL;
|
||||||
|
extern bool DISABLE_FRAME_LIMITER;
|
||||||
extern std::optional<unsigned int> CAB_TYPE;
|
extern std::optional<unsigned int> CAB_TYPE;
|
||||||
extern bool P1_LEFTY;
|
extern bool P1_LEFTY;
|
||||||
extern bool P2_LEFTY;
|
extern bool P2_LEFTY;
|
||||||
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||||
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
||||||
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
||||||
|
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern bool ALLOW_REALTEK_AUDIO;
|
extern bool ALLOW_REALTEK_AUDIO;
|
||||||
|
extern bool NATIVE_TOUCH;
|
||||||
|
|
||||||
|
// arena SMALL subscreen (touch panel) resolution
|
||||||
|
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
|
||||||
|
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
|
||||||
|
|
||||||
class GitaDoraGame : public games::Game {
|
class GitaDoraGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#if SPICE64
|
#if SPICE64
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include "api/client.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -443,10 +444,12 @@ namespace games::iidx {
|
|||||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
|
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
|
||||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
||||||
|
|
||||||
for (unsigned int i = 0; i < data_size; ++i) {
|
if (api::has_clients()) {
|
||||||
map.data[i].r = data[i * 3];
|
for (size_t i = 0; i < data_size; ++i) {
|
||||||
map.data[i].g = data[i * 3 + 1];
|
map.data[i].r = data[i * 3];
|
||||||
map.data[i].b = data[i * 3 + 2];
|
map.data[i].g = data[i * 3 + 1];
|
||||||
|
map.data[i].b = data[i * 3 + 2];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ namespace games::iidx {
|
|||||||
if (mediaTypePointer && mediaTypePointer->IsString()) {
|
if (mediaTypePointer && mediaTypePointer->IsString()) {
|
||||||
std::string mediaType = mediaTypePointer->GetString();
|
std::string mediaType = mediaTypePointer->GetString();
|
||||||
if (mediaType.length() > 0) {
|
if (mediaType.length() > 0) {
|
||||||
camera->m_selectedMediaTypeDescription = mediaType;
|
camera->SetSelectedMediaTypeDescription(mediaType);
|
||||||
camera->m_useAutoMediaType = false;
|
camera->m_useAutoMediaType = false;
|
||||||
} else {
|
} else {
|
||||||
camera->m_useAutoMediaType = true;
|
camera->m_useAutoMediaType = true;
|
||||||
@@ -547,7 +547,7 @@ namespace games::iidx {
|
|||||||
std::string drawModeString = drawModePointer->GetString();
|
std::string drawModeString = drawModePointer->GetString();
|
||||||
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
|
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
|
||||||
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
|
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
|
||||||
camera->m_drawMode = (LocalCameraDrawMode) j;
|
camera->m_drawMode.store((LocalCameraDrawMode) j);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,12 +556,12 @@ namespace games::iidx {
|
|||||||
// Flip
|
// Flip
|
||||||
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
|
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
|
||||||
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
|
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
|
||||||
camera->m_flipHorizontal = flipHorizontalPointer->GetBool();
|
camera->m_flipHorizontal.store(flipHorizontalPointer->GetBool());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
|
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
|
||||||
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
|
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
|
||||||
camera->m_flipVertical = flipVerticalPointer->GetBool();
|
camera->m_flipVertical.store(flipVerticalPointer->GetBool());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow manual control
|
// Allow manual control
|
||||||
@@ -625,15 +625,17 @@ namespace games::iidx {
|
|||||||
if (camera->m_useAutoMediaType) {
|
if (camera->m_useAutoMediaType) {
|
||||||
rapidjson::Pointer(root + "MediaType").Set(doc, "");
|
rapidjson::Pointer(root + "MediaType").Set(doc, "");
|
||||||
} else {
|
} else {
|
||||||
rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription);
|
const auto mediaTypeDescription = camera->GetSelectedMediaTypeDescription();
|
||||||
|
rapidjson::Pointer(root + "MediaType").Set(doc, mediaTypeDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Mode
|
// Draw Mode
|
||||||
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]);
|
const auto drawMode = camera->m_drawMode.load();
|
||||||
|
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[drawMode]);
|
||||||
|
|
||||||
// Flip
|
// Flip
|
||||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal);
|
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal.load());
|
||||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical);
|
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical.load());
|
||||||
|
|
||||||
// Manual control
|
// Manual control
|
||||||
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
|
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "touch/native/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "misc/wintouchemu.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
#include "ezusb.h"
|
#include "ezusb.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
|
#include "poke.h"
|
||||||
|
|
||||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||||
@@ -56,12 +58,14 @@ namespace games::iidx {
|
|||||||
|
|
||||||
// settings
|
// settings
|
||||||
bool FLIP_CAMS = false;
|
bool FLIP_CAMS = false;
|
||||||
std::optional<bool> DISABLE_CAMS;
|
cab_camera_access_mode CAB_CAMERA_ACCESS = cab_camera_access_mode::automatic;
|
||||||
bool TDJ_CAMERA = false;
|
bool TDJ_CAMERA = false;
|
||||||
bool TDJ_CAMERA_PREFER_16_9 = true;
|
bool TDJ_CAMERA_PREFER_16_9 = true;
|
||||||
bool TDJ_MODE = false;
|
bool TDJ_MODE = false;
|
||||||
bool FORCE_720P = false;
|
bool FORCE_720P = false;
|
||||||
bool DISABLE_ESPEC_IO = false;
|
bool DISABLE_ESPEC_IO = false;
|
||||||
|
bool NATIVE_TOUCH = true;
|
||||||
|
bool ENABLE_POKE = false;
|
||||||
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
||||||
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
||||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||||
@@ -275,6 +279,26 @@ namespace games::iidx {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// best-effort read of a TDJ ROM file to determine if the game is running in TDJ mode
|
||||||
|
//
|
||||||
|
// these paths are not emulated - they hit whatever is actually mounted at that drive letter.
|
||||||
|
// an empty optical or removable drive raises the modal "insert a disk" error (the launcher
|
||||||
|
// clears SEM_FAILCRITICALERRORS process-wide) and a downed network drive stalls on redirector
|
||||||
|
// timeouts, so only probe what a TDJ cabinet would actually be laid out on.
|
||||||
|
// drive_path must be absolute and start with a drive letter.
|
||||||
|
static bool tdj_rom_matches(const char *drive_path, const char *expected) {
|
||||||
|
const wchar_t root[] = { (wchar_t) drive_path[0], L':', L'\\', L'\0' };
|
||||||
|
const auto drive_type = GetDriveTypeW(root);
|
||||||
|
|
||||||
|
if (drive_type != DRIVE_FIXED && drive_type != DRIVE_RAMDISK) {
|
||||||
|
log_misc("iidx", "not probing '{}' for TDJ, not a local disk (drive type {})",
|
||||||
|
drive_path, drive_type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileutils::text_read(drive_path) == expected;
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
IIDXGame::IIDXGame() : Game("Beatmania IIDX") {
|
IIDXGame::IIDXGame() : Game("Beatmania IIDX") {
|
||||||
@@ -335,8 +359,10 @@ namespace games::iidx {
|
|||||||
HAS_LIBAIO = true;
|
HAS_LIBAIO = true;
|
||||||
|
|
||||||
// check TDJ mode
|
// check TDJ mode
|
||||||
TDJ_MODE |= fileutils::text_read("C:\\000rom.txt") == "TDJ-JA";
|
if (!TDJ_MODE) {
|
||||||
TDJ_MODE |= fileutils::text_read("D:\\001rom.txt") == "TDJ";
|
TDJ_MODE = tdj_rom_matches("C:\\000rom.txt", "TDJ-JA")
|
||||||
|
|| tdj_rom_matches("D:\\001rom.txt", "TDJ");
|
||||||
|
}
|
||||||
|
|
||||||
// force TDJ mode
|
// force TDJ mode
|
||||||
if (TDJ_MODE) {
|
if (TDJ_MODE) {
|
||||||
@@ -348,7 +374,16 @@ namespace games::iidx {
|
|||||||
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
||||||
devicehook_init(avs::core::DLL_INSTANCE);
|
devicehook_init(avs::core::DLL_INSTANCE);
|
||||||
|
|
||||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||||
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
|
if (!NATIVE_TOUCH) {
|
||||||
|
wintouchemu::FORCE = true;
|
||||||
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
|
wintouchemu::hook_title_ends(
|
||||||
|
"beatmania IIDX",
|
||||||
|
"main",
|
||||||
|
avs::game::DLL_INSTANCE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert BI2X hooks
|
// insert BI2X hooks
|
||||||
@@ -411,10 +446,7 @@ namespace games::iidx {
|
|||||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||||
|
|
||||||
// check if cam hook should be enabled
|
// check if cam hook should be enabled
|
||||||
if (!DISABLE_CAMS.has_value()) {
|
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::legacy) {
|
||||||
log_fatal("iidx", "assertion failure - DISABLE_CAMS not set during attach");
|
|
||||||
}
|
|
||||||
if (!DISABLE_CAMS.value()) {
|
|
||||||
init_legacy_camera_hook(FLIP_CAMS);
|
init_legacy_camera_hook(FLIP_CAMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,19 +473,10 @@ namespace games::iidx {
|
|||||||
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
|
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for cab camera access for the second time (first time was in launcher.cpp)
|
// auto with iidx module means turn off camera (non-cab use)
|
||||||
// this time, we are inside -iidx module hook, which means the user is likely NOT on a cab
|
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::automatic) {
|
||||||
// therefore, start with cams OFF by default, and allow user to forcibly override to ON
|
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
|
||||||
if (!games::iidx::DISABLE_CAMS.has_value()) {
|
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
||||||
games::iidx::DISABLE_CAMS = true;
|
|
||||||
if (options->at(launcher::Options::IIDXCabCamAccess).is_active() &&
|
|
||||||
options->at(launcher::Options::IIDXCabCamAccess).value_text() == "on") {
|
|
||||||
games::iidx::DISABLE_CAMS = false;
|
|
||||||
}
|
|
||||||
if (games::iidx::DISABLE_CAMS.value()) {
|
|
||||||
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
|
|
||||||
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// windowed subscreen, enabled by default, unless turned off by user
|
// windowed subscreen, enabled by default, unless turned off by user
|
||||||
@@ -525,6 +548,12 @@ namespace games::iidx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IIDXGame::post_attach() {
|
||||||
|
if (ENABLE_POKE) {
|
||||||
|
poke::enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void IIDXGame::detach() {
|
void IIDXGame::detach() {
|
||||||
Game::detach();
|
Game::detach();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
|
|
||||||
namespace games::iidx {
|
namespace games::iidx {
|
||||||
|
|
||||||
|
enum class cab_camera_access_mode {
|
||||||
|
automatic,
|
||||||
|
off,
|
||||||
|
on,
|
||||||
|
legacy,
|
||||||
|
};
|
||||||
|
|
||||||
enum class iidx_aio_emulation_state {
|
enum class iidx_aio_emulation_state {
|
||||||
unknown,
|
unknown,
|
||||||
bi2a_com2,
|
bi2a_com2,
|
||||||
@@ -20,7 +27,7 @@ namespace games::iidx {
|
|||||||
// settings
|
// settings
|
||||||
|
|
||||||
extern bool FLIP_CAMS;
|
extern bool FLIP_CAMS;
|
||||||
extern std::optional<bool> DISABLE_CAMS;
|
extern cab_camera_access_mode CAB_CAMERA_ACCESS;
|
||||||
extern bool TDJ_CAMERA;
|
extern bool TDJ_CAMERA;
|
||||||
extern bool TDJ_CAMERA_PREFER_16_9;
|
extern bool TDJ_CAMERA_PREFER_16_9;
|
||||||
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
|
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
|
||||||
@@ -28,6 +35,8 @@ namespace games::iidx {
|
|||||||
extern bool TDJ_MODE;
|
extern bool TDJ_MODE;
|
||||||
extern bool FORCE_720P;
|
extern bool FORCE_720P;
|
||||||
extern bool DISABLE_ESPEC_IO;
|
extern bool DISABLE_ESPEC_IO;
|
||||||
|
extern bool NATIVE_TOUCH;
|
||||||
|
extern bool ENABLE_POKE;
|
||||||
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern uint8_t DIGITAL_TT_SENS;
|
extern uint8_t DIGITAL_TT_SENS;
|
||||||
@@ -52,6 +61,7 @@ namespace games::iidx {
|
|||||||
|
|
||||||
virtual void attach() override;
|
virtual void attach() override;
|
||||||
virtual void pre_attach() override;
|
virtual void pre_attach() override;
|
||||||
|
virtual void post_attach() override;
|
||||||
virtual void detach() override;
|
virtual void detach() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
#include <dxva2api.h>
|
#include <dxva2api.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -66,6 +67,8 @@ extern std::string CAMERA_CONTROL_LABELS[];
|
|||||||
extern std::string DRAW_MODE_LABELS[];
|
extern std::string DRAW_MODE_LABELS[];
|
||||||
|
|
||||||
namespace games::iidx {
|
namespace games::iidx {
|
||||||
|
class IIDXCameraSourceReaderCallback;
|
||||||
|
|
||||||
namespace Camera {
|
namespace Camera {
|
||||||
struct PlayVideoCamera {
|
struct PlayVideoCamera {
|
||||||
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
|
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
|
||||||
@@ -87,7 +90,7 @@ namespace games::iidx {
|
|||||||
|
|
||||||
class IIDXLocalCamera {
|
class IIDXLocalCamera {
|
||||||
protected:
|
protected:
|
||||||
virtual ~IIDXLocalCamera() {};
|
virtual ~IIDXLocalCamera();
|
||||||
|
|
||||||
LONG m_nRefCount;
|
LONG m_nRefCount;
|
||||||
CRITICAL_SECTION m_critsec;
|
CRITICAL_SECTION m_critsec;
|
||||||
@@ -101,9 +104,14 @@ namespace games::iidx {
|
|||||||
// For reading frames from Camera
|
// For reading frames from Camera
|
||||||
IMFMediaSource *m_pSource = nullptr;
|
IMFMediaSource *m_pSource = nullptr;
|
||||||
IMFSourceReader *m_pSourceReader = nullptr;
|
IMFSourceReader *m_pSourceReader = nullptr;
|
||||||
|
IMFSourceReaderEx *m_pSourceReaderEx = nullptr;
|
||||||
|
IIDXCameraSourceReaderCallback *m_pSourceReaderCallback = nullptr;
|
||||||
|
std::mutex m_mediaTypeMutex;
|
||||||
|
IMFMediaType *m_pendingMediaType = nullptr;
|
||||||
|
int m_selectedMediaTypeIndex = 0;
|
||||||
|
std::string m_selectedMediaTypeDescription = "";
|
||||||
|
|
||||||
// Camera Format information
|
// Camera Format information
|
||||||
double m_frameRate = 0;
|
|
||||||
LONG m_cameraWidth;
|
LONG m_cameraWidth;
|
||||||
LONG m_cameraHeight;
|
LONG m_cameraHeight;
|
||||||
|
|
||||||
@@ -129,6 +137,12 @@ namespace games::iidx {
|
|||||||
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
|
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
|
||||||
IDirect3DSurface9 *m_pConversionSurf = nullptr;
|
IDirect3DSurface9 *m_pConversionSurf = nullptr;
|
||||||
|
|
||||||
|
// upload surface for decoded camera frames returned in system memory
|
||||||
|
IDirect3DSurface9 *m_pDecodedSurf = nullptr;
|
||||||
|
GUID m_decodedSubtype = GUID_NULL;
|
||||||
|
GUID m_outputSubtype = GUID_NULL;
|
||||||
|
bool m_drawErrorLogged = false;
|
||||||
|
|
||||||
// Texture for custom transform (e.g. horizontal flip)
|
// Texture for custom transform (e.g. horizontal flip)
|
||||||
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
|
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
|
||||||
IDirect3DSurface9 *m_pTransformSurf = nullptr;
|
IDirect3DSurface9 *m_pTransformSurf = nullptr;
|
||||||
@@ -153,21 +167,19 @@ namespace games::iidx {
|
|||||||
BOOL m_initialized = false;
|
BOOL m_initialized = false;
|
||||||
|
|
||||||
// True if all the setup steps succeeded
|
// True if all the setup steps succeeded
|
||||||
BOOL m_active = false;
|
std::atomic_bool m_active = false;
|
||||||
|
|
||||||
// Media type select
|
// Media type select
|
||||||
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
|
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
|
||||||
int m_selectedMediaTypeIndex = 0;
|
|
||||||
bool m_useAutoMediaType = true;
|
bool m_useAutoMediaType = true;
|
||||||
IMFMediaType *m_pAutoMediaType = nullptr;
|
IMFMediaType *m_pAutoMediaType = nullptr;
|
||||||
std::string m_selectedMediaTypeDescription = "";
|
|
||||||
bool m_allowManualControl = false;
|
bool m_allowManualControl = false;
|
||||||
|
|
||||||
LocalCameraDrawMode m_drawMode = DrawModeCrop4_3;
|
std::atomic<LocalCameraDrawMode> m_drawMode = DrawModeCrop4_3;
|
||||||
|
|
||||||
// Render processing
|
// Render processing
|
||||||
bool m_flipHorizontal = false;
|
std::atomic_bool m_flipHorizontal = false;
|
||||||
bool m_flipVertical = false;
|
std::atomic_bool m_flipVertical = false;
|
||||||
|
|
||||||
IIDXLocalCamera(
|
IIDXLocalCamera(
|
||||||
std::string name,
|
std::string name,
|
||||||
@@ -184,11 +196,14 @@ namespace games::iidx {
|
|||||||
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
|
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
|
||||||
HRESULT SetCameraControlProp(int index, long value, long flags);
|
HRESULT SetCameraControlProp(int index, long value, long flags);
|
||||||
HRESULT ResetCameraControlProps();
|
HRESULT ResetCameraControlProps();
|
||||||
HRESULT FlushDrawCommands();
|
|
||||||
std::string GetName();
|
std::string GetName();
|
||||||
std::string GetFriendlyName();
|
std::string GetFriendlyName();
|
||||||
std::string GetSymLink();
|
std::string GetSymLink();
|
||||||
|
int GetSelectedMediaTypeIndex();
|
||||||
|
std::string GetSelectedMediaTypeDescription();
|
||||||
|
void SetSelectedMediaTypeDescription(const std::string &description);
|
||||||
HRESULT ChangeMediaType(IMFMediaType *pType);
|
HRESULT ChangeMediaType(IMFMediaType *pType);
|
||||||
|
void RequestMediaType(IMFMediaType *pType);
|
||||||
HRESULT StartCapture();
|
HRESULT StartCapture();
|
||||||
void UpdateDrawRect();
|
void UpdateDrawRect();
|
||||||
|
|
||||||
@@ -197,12 +212,22 @@ namespace games::iidx {
|
|||||||
void CreateThread();
|
void CreateThread();
|
||||||
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
|
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
|
||||||
std::string GetVideoFormatName(GUID subtype);
|
std::string GetVideoFormatName(GUID subtype);
|
||||||
HRESULT TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate);
|
static bool CompareMediaTypes(const MediaTypeInfo &a, const MediaTypeInfo &b);
|
||||||
|
bool MatchesPreferredAspect(const MediaTypeInfo &info) const;
|
||||||
|
static bool IsBetterAutoType(const MediaTypeInfo &candidate, const MediaTypeInfo ¤t);
|
||||||
|
IMFMediaType *FindBestNativeAutoType(bool requirePreferredAspect) const;
|
||||||
|
IMFMediaType *FindBestAutoType(const GUID &subtype, bool requirePreferredAspect) const;
|
||||||
|
HRESULT ValidateMediaType(IMFMediaType *pType);
|
||||||
HRESULT InitTargetTexture();
|
HRESULT InitTargetTexture();
|
||||||
|
HRESULT EnsureTransformTextures();
|
||||||
HRESULT InitCameraControl();
|
HRESULT InitCameraControl();
|
||||||
|
void SetSelectedMediaType(int index, const std::string &description);
|
||||||
|
bool HasPendingMediaType();
|
||||||
|
HRESULT ApplyPendingMediaType();
|
||||||
|
HRESULT UploadDecodedSample(IMFMediaBuffer *pSrcBuffer);
|
||||||
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
|
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
|
||||||
HRESULT ReadSample();
|
HRESULT ReadSample();
|
||||||
LPDIRECT3DTEXTURE9 Render();
|
HRESULT Render();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ namespace games::iidx {
|
|||||||
_In_ UINT32 cInitialSize
|
_In_ UINT32 cInitialSize
|
||||||
);
|
);
|
||||||
|
|
||||||
|
typedef HRESULT (__stdcall * MFCreateMediaType_t)(
|
||||||
|
_Out_ IMFMediaType** ppMFType
|
||||||
|
);
|
||||||
|
|
||||||
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
|
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
|
||||||
_In_ IMFAttributes* pAttributes,
|
_In_ IMFAttributes* pAttributes,
|
||||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||||
@@ -35,6 +39,7 @@ namespace games::iidx {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static MFCreateAttributes_t MFCreateAttributes = nullptr;
|
static MFCreateAttributes_t MFCreateAttributes = nullptr;
|
||||||
|
static MFCreateMediaType_t MFCreateMediaType = nullptr;
|
||||||
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
|
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
|
||||||
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
|
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
|
||||||
static MFGetService_t MFGetService = nullptr;
|
static MFGetService_t MFGetService = nullptr;
|
||||||
@@ -66,6 +71,12 @@ namespace games::iidx {
|
|||||||
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
|
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MFCreateMediaType = (MFCreateMediaType_t)
|
||||||
|
libutils::get_proc(mfplat_dll, "MFCreateMediaType");
|
||||||
|
if (!MFCreateMediaType) {
|
||||||
|
log_fatal("mf_wrappers", "MFCreateMediaType failed to hook");
|
||||||
|
}
|
||||||
|
|
||||||
MFEnumDeviceSources = (MFEnumDeviceSources_t)
|
MFEnumDeviceSources = (MFEnumDeviceSources_t)
|
||||||
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
|
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
|
||||||
if (!MFEnumDeviceSources) {
|
if (!MFEnumDeviceSources) {
|
||||||
@@ -92,6 +103,11 @@ namespace games::iidx {
|
|||||||
return MFCreateAttributes(ppMFAttributes, cInitialSize);
|
return MFCreateAttributes(ppMFAttributes, cInitialSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HRESULT WrappedMFCreateMediaType (
|
||||||
|
_Out_ IMFMediaType** ppMFType) {
|
||||||
|
return MFCreateMediaType(ppMFType);
|
||||||
|
}
|
||||||
|
|
||||||
HRESULT WrappedMFEnumDeviceSources (
|
HRESULT WrappedMFEnumDeviceSources (
|
||||||
_In_ IMFAttributes* pAttributes,
|
_In_ IMFAttributes* pAttributes,
|
||||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ namespace games::iidx {
|
|||||||
_Out_ IMFAttributes** ppMFAttributes,
|
_Out_ IMFAttributes** ppMFAttributes,
|
||||||
_In_ UINT32 cInitialSize);
|
_In_ UINT32 cInitialSize);
|
||||||
|
|
||||||
|
HRESULT WrappedMFCreateMediaType (
|
||||||
|
_Out_ IMFMediaType** ppMFType);
|
||||||
|
|
||||||
HRESULT WrappedMFEnumDeviceSources (
|
HRESULT WrappedMFEnumDeviceSources (
|
||||||
_In_ IMFAttributes* pAttributes,
|
_In_ IMFAttributes* pAttributes,
|
||||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
#include "poke.h"
|
#include "poke.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include "windows.h"
|
#include "windows.h"
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
|
#include "games/iidx/iidx.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "touch/native/inject.h"
|
#include "touch/native/inject.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
|
|
||||||
namespace games::iidx::poke {
|
namespace games::iidx::poke {
|
||||||
|
|
||||||
@@ -116,11 +117,14 @@ namespace games::iidx::poke {
|
|||||||
if (THREAD)
|
if (THREAD)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!games::iidx::NATIVE_TOUCH) {
|
||||||
|
log_warning("poke", "keypad touch emulation requires native touch; disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// create new thread
|
// create new thread
|
||||||
THREAD_RUNNING = true;
|
THREAD_RUNNING = true;
|
||||||
THREAD = new std::thread([] {
|
THREAD = new std::thread([] {
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
// log
|
// log
|
||||||
log_info("poke", "enabled");
|
log_info("poke", "enabled");
|
||||||
|
|
||||||
@@ -192,7 +196,7 @@ namespace games::iidx::poke {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// slow down
|
// slow down
|
||||||
timer.sleep(50);
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!touch_points.empty()) {
|
if (!touch_points.empty()) {
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ namespace games {
|
|||||||
buttons_help.insert({ jb, jb::get_buttons_help() });
|
buttons_help.insert({ jb, jb::get_buttons_help() });
|
||||||
lights.insert({ jb, jb::get_lights() });
|
lights.insert({ jb, jb::get_lights() });
|
||||||
file_hints[jb].push_back({"jubeat.dll"});
|
file_hints[jb].push_back({"jubeat.dll"});
|
||||||
|
file_hints[jb].push_back({"jubeat2019.dll"});
|
||||||
|
|
||||||
// mga
|
// mga
|
||||||
const std::string mga("Metal Gear");
|
const std::string mga("Metal Gear");
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
#include "bi2x_hook.h"
|
||||||
|
|
||||||
|
#if SPICE64
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "games/io.h"
|
||||||
|
#include "games/jb/jb_touch.h"
|
||||||
|
#include "io.h"
|
||||||
|
#include "misc/eamuse.h"
|
||||||
|
#include "rawinput/rawinput.h"
|
||||||
|
#include "util/detour.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace games::jb {
|
||||||
|
|
||||||
|
struct AIO_SCI_COMM {
|
||||||
|
std::array<uint8_t, 0x100> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_NMGR_IOB2;
|
||||||
|
|
||||||
|
struct AIO_NMGR_IOB2_VTABLE {
|
||||||
|
std::array<void *, 10> unused;
|
||||||
|
void (__fastcall *begin_manage)(AIO_NMGR_IOB2 *node_mgr);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_NMGR_IOB2 {
|
||||||
|
AIO_NMGR_IOB2_VTABLE *vtable;
|
||||||
|
std::array<uint8_t, 0x78> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_IOB2_BI2X_T44 {
|
||||||
|
std::array<uint8_t, 0x80> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_IOB2_BI2X_T44_DEVSTATUS {
|
||||||
|
std::array<uint8_t, 0x140> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_IOB2_BI2X_WRFIRM {
|
||||||
|
uint8_t data;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(AIO_NMGR_IOB2) == 0x80);
|
||||||
|
static_assert(sizeof(AIO_IOB2_BI2X_T44_DEVSTATUS) == 0x140);
|
||||||
|
|
||||||
|
using aioIob2Bi2xT44_Create_t = AIO_IOB2_BI2X_T44 *(__fastcall *)(AIO_NMGR_IOB2 *node_mgr,
|
||||||
|
uint32_t device_id);
|
||||||
|
using aioIob2Bi2xT44_GetDeviceStatus_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
|
||||||
|
AIO_IOB2_BI2X_T44_DEVSTATUS *status);
|
||||||
|
using aioIob2Bi2xT44_IoReset_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t reset);
|
||||||
|
using aioIob2Bi2xT44_SetWatchDogTimer_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint8_t count);
|
||||||
|
using aioIob2Bi2xT44_ControlCoinBlocker_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
|
||||||
|
uint32_t slot, bool open);
|
||||||
|
using aioIob2Bi2xT44_AddCounter_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
|
||||||
|
uint32_t counter, uint32_t count);
|
||||||
|
using aioIob2Bi2xT44_SetIccrLed_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t color);
|
||||||
|
using aioIob2Bi2xT44_SetTapeLedData_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
|
||||||
|
uint32_t tape, const void *data);
|
||||||
|
using aioIob2Bi2x_OpenSciUsbCdc_t = AIO_SCI_COMM *(__fastcall *)(uint32_t serial_number);
|
||||||
|
using aioIob2Bi2x_CreateWriteFirmContext_t = AIO_IOB2_BI2X_WRFIRM *(__fastcall *)(
|
||||||
|
uint32_t serial_number, uint32_t iob_mask);
|
||||||
|
using aioIob2Bi2x_DestroyWriteFirmContext_t = void (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context);
|
||||||
|
using aioIob2Bi2x_WriteFirmGetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context);
|
||||||
|
using aioIob2Bi2x_WriteFirmIsCompleted_t = bool (__fastcall *)(int32_t state);
|
||||||
|
using aioIob2Bi2x_WriteFirmIsError_t = bool (__fastcall *)(int32_t state);
|
||||||
|
using aioNMgrIob2_Create_t = AIO_NMGR_IOB2 *(__fastcall *)(AIO_SCI_COMM *sci, uint32_t mode);
|
||||||
|
using aioSci_Destroy_t = void (__fastcall *)(AIO_SCI_COMM *sci);
|
||||||
|
using aioNodeMgr_Destroy_t = void (__fastcall *)(AIO_NMGR_IOB2 *node_mgr);
|
||||||
|
using aioNodeCtl_Destroy_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node);
|
||||||
|
using aioNodeCtl_UpdateDevicesStatus_t = void (__fastcall *)();
|
||||||
|
|
||||||
|
static aioIob2Bi2xT44_Create_t aioIob2Bi2xT44_Create_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_GetDeviceStatus_t aioIob2Bi2xT44_GetDeviceStatus_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_IoReset_t aioIob2Bi2xT44_IoReset_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_SetWatchDogTimer_t aioIob2Bi2xT44_SetWatchDogTimer_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_ControlCoinBlocker_t aioIob2Bi2xT44_ControlCoinBlocker_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_AddCounter_t aioIob2Bi2xT44_AddCounter_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_SetIccrLed_t aioIob2Bi2xT44_SetIccrLed_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_SetTapeLedData_t aioIob2Bi2xT44_SetTapeLedData_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_OpenSciUsbCdc_t aioIob2Bi2x_OpenSciUsbCdc_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_CreateWriteFirmContext_t aioIob2Bi2x_CreateWriteFirmContext_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_DestroyWriteFirmContext_t aioIob2Bi2x_DestroyWriteFirmContext_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_WriteFirmGetState_t aioIob2Bi2x_WriteFirmGetState_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_WriteFirmIsCompleted_t aioIob2Bi2x_WriteFirmIsCompleted_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_WriteFirmIsError_t aioIob2Bi2x_WriteFirmIsError_orig = nullptr;
|
||||||
|
static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr;
|
||||||
|
static aioSci_Destroy_t aioSci_Destroy_orig = nullptr;
|
||||||
|
static aioNodeMgr_Destroy_t aioNodeMgr_Destroy_orig = nullptr;
|
||||||
|
static aioNodeCtl_Destroy_t aioNodeCtl_Destroy_orig = nullptr;
|
||||||
|
static aioNodeCtl_UpdateDevicesStatus_t aioNodeCtl_UpdateDevicesStatus_orig = nullptr;
|
||||||
|
|
||||||
|
static AIO_SCI_COMM *aio_sci_comm = nullptr;
|
||||||
|
static AIO_NMGR_IOB2 *aio_node_mgr = nullptr;
|
||||||
|
static AIO_IOB2_BI2X_T44 *aio_t44 = nullptr;
|
||||||
|
static AIO_IOB2_BI2X_WRFIRM *aio_write_firm = nullptr;
|
||||||
|
static uint8_t input_counter = 0;
|
||||||
|
|
||||||
|
static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *) {
|
||||||
|
}
|
||||||
|
|
||||||
|
static AIO_NMGR_IOB2_VTABLE aio_node_mgr_vtable {
|
||||||
|
{},
|
||||||
|
aioNMgrIob_BeginManage,
|
||||||
|
};
|
||||||
|
|
||||||
|
static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t) {
|
||||||
|
aio_sci_comm = new AIO_SCI_COMM {};
|
||||||
|
return aio_sci_comm;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) {
|
||||||
|
if (sci != aio_sci_comm) {
|
||||||
|
return aioNMgrIob2_Create_orig(sci, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
aio_node_mgr = new AIO_NMGR_IOB2 {};
|
||||||
|
aio_node_mgr->vtable = &aio_node_mgr_vtable;
|
||||||
|
return aio_node_mgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AIO_IOB2_BI2X_T44 *__fastcall aioIob2Bi2xT44_Create(
|
||||||
|
AIO_NMGR_IOB2 *node_mgr, uint32_t device_id) {
|
||||||
|
if (node_mgr != aio_node_mgr) {
|
||||||
|
return aioIob2Bi2xT44_Create_orig(node_mgr, device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("jb::bi2x", "T44 node created");
|
||||||
|
aio_t44 = new AIO_IOB2_BI2X_T44 {};
|
||||||
|
return aio_t44;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_GetDeviceStatus(
|
||||||
|
AIO_IOB2_BI2X_T44 *node, AIO_IOB2_BI2X_T44_DEVSTATUS *status) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_GetDeviceStatus_orig(node, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
RI_MGR->devices_flush_output();
|
||||||
|
std::memset(status, 0, sizeof(*status));
|
||||||
|
|
||||||
|
status->data[0x00] = input_counter;
|
||||||
|
status->data[0x03] = input_counter++;
|
||||||
|
status->data[0x0A] = static_cast<uint8_t>(eamuse_coin_get_stock());
|
||||||
|
|
||||||
|
games::jb::touch_update();
|
||||||
|
const auto touched = games::jb::touch_state();
|
||||||
|
auto &buttons = get_buttons();
|
||||||
|
|
||||||
|
status->data[0x04] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 0xFF : 0;
|
||||||
|
status->data[0x05] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 0xFF : 0;
|
||||||
|
status->data[0x06] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]) ? 0xFF : 0;
|
||||||
|
|
||||||
|
static constexpr size_t PANEL_ORDER[16] = {
|
||||||
|
3, 7, 11, 15,
|
||||||
|
2, 6, 10, 14,
|
||||||
|
1, 5, 9, 13,
|
||||||
|
0, 4, 8, 12,
|
||||||
|
};
|
||||||
|
for (size_t status_index = 0; status_index < std::size(PANEL_ORDER); status_index++) {
|
||||||
|
const auto panel_index = PANEL_ORDER[status_index];
|
||||||
|
if (touched[panel_index] || GameAPI::Buttons::getState(
|
||||||
|
RI_MGR, buttons[Buttons::Button1 + panel_index])) {
|
||||||
|
status->data[0x0F + status_index] = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_IoReset(AIO_IOB2_BI2X_T44 *node, uint32_t reset) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_IoReset_orig(node, reset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_SetWatchDogTimer(AIO_IOB2_BI2X_T44 *node, uint8_t count) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_SetWatchDogTimer_orig(node, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_ControlCoinBlocker(
|
||||||
|
AIO_IOB2_BI2X_T44 *node, uint32_t slot, bool open) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_ControlCoinBlocker_orig(node, slot, open);
|
||||||
|
}
|
||||||
|
|
||||||
|
eamuse_coin_set_block(!open);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_AddCounter(
|
||||||
|
AIO_IOB2_BI2X_T44 *node, uint32_t counter, uint32_t count) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_AddCounter_orig(node, counter, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_SetIccrLed(AIO_IOB2_BI2X_T44 *node, uint32_t color) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_SetIccrLed_orig(node, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_SetTapeLedData(
|
||||||
|
AIO_IOB2_BI2X_T44 *node, uint32_t tape, const void *data) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_SetTapeLedData_orig(node, tape, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext(uint32_t, uint32_t) {
|
||||||
|
aio_write_firm = new AIO_IOB2_BI2X_WRFIRM {};
|
||||||
|
return aio_write_firm;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) {
|
||||||
|
if (context != aio_write_firm) {
|
||||||
|
return aioIob2Bi2x_DestroyWriteFirmContext_orig(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete aio_write_firm;
|
||||||
|
aio_write_firm = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) {
|
||||||
|
if (context != aio_write_firm) {
|
||||||
|
return aioIob2Bi2x_WriteFirmGetState_orig(context);
|
||||||
|
}
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool __fastcall aioIob2Bi2x_WriteFirmIsCompleted(int32_t state) {
|
||||||
|
if (aio_write_firm) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return aioIob2Bi2x_WriteFirmIsCompleted_orig(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool __fastcall aioIob2Bi2x_WriteFirmIsError(int32_t state) {
|
||||||
|
if (aio_write_firm) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return aioIob2Bi2x_WriteFirmIsError_orig(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioSci_Destroy(AIO_SCI_COMM *sci) {
|
||||||
|
if (sci != aio_sci_comm) {
|
||||||
|
return aioSci_Destroy_orig(sci);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete aio_sci_comm;
|
||||||
|
aio_sci_comm = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *node_mgr) {
|
||||||
|
if (node_mgr != aio_node_mgr) {
|
||||||
|
return aioNodeMgr_Destroy_orig(node_mgr);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete aio_node_mgr;
|
||||||
|
aio_node_mgr = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_T44 *node) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioNodeCtl_Destroy_orig(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete aio_t44;
|
||||||
|
aio_t44 = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioNodeCtl_UpdateDevicesStatus() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void bi2x_hook_init() {
|
||||||
|
static bool initialized = false;
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
log_info("jb::bi2x", "initializing T44 hooks");
|
||||||
|
|
||||||
|
const auto libaio_iob2_video = "libaio-iob2_video.dll";
|
||||||
|
detour::trampoline_try(libaio_iob2_video, "aioIob2Bi2xT44_Create",
|
||||||
|
aioIob2Bi2xT44_Create, &aioIob2Bi2xT44_Create_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video,
|
||||||
|
"?GetDeviceStatus@AIO_IOB2_BI2X_T44@@QEBAXAEAUDEVSTATUS@1@@Z",
|
||||||
|
aioIob2Bi2xT44_GetDeviceStatus, &aioIob2Bi2xT44_GetDeviceStatus_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video, "?IoReset@AIO_IOB2_BI2X_T44@@QEAAXI@Z",
|
||||||
|
aioIob2Bi2xT44_IoReset, &aioIob2Bi2xT44_IoReset_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video,
|
||||||
|
"?SetWatchDogTimer@AIO_IOB2_BI2X_T44@@QEAAXE@Z",
|
||||||
|
aioIob2Bi2xT44_SetWatchDogTimer, &aioIob2Bi2xT44_SetWatchDogTimer_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video,
|
||||||
|
"?ControlCoinBlocker@AIO_IOB2_BI2X_T44@@QEAAXI_N@Z",
|
||||||
|
aioIob2Bi2xT44_ControlCoinBlocker, &aioIob2Bi2xT44_ControlCoinBlocker_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video, "?AddCounter@AIO_IOB2_BI2X_T44@@QEAAXII@Z",
|
||||||
|
aioIob2Bi2xT44_AddCounter, &aioIob2Bi2xT44_AddCounter_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video, "?SetIccrLed@AIO_IOB2_BI2X_T44@@QEAAXI@Z",
|
||||||
|
aioIob2Bi2xT44_SetIccrLed, &aioIob2Bi2xT44_SetIccrLed_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video,
|
||||||
|
"?SetTapeLedData@AIO_IOB2_BI2X_T44@@QEAAXIPEBX@Z",
|
||||||
|
aioIob2Bi2xT44_SetTapeLedData, &aioIob2Bi2xT44_SetTapeLedData_orig);
|
||||||
|
|
||||||
|
const auto libaio_iob = "libaio-iob.dll";
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_OpenSciUsbCdc",
|
||||||
|
aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_CreateWriteFirmContext",
|
||||||
|
aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_DestroyWriteFirmContext",
|
||||||
|
aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmGetState",
|
||||||
|
aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsCompleted",
|
||||||
|
aioIob2Bi2x_WriteFirmIsCompleted, &aioIob2Bi2x_WriteFirmIsCompleted_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsError",
|
||||||
|
aioIob2Bi2x_WriteFirmIsError, &aioIob2Bi2x_WriteFirmIsError_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioNMgrIob2_Create",
|
||||||
|
aioNMgrIob2_Create, &aioNMgrIob2_Create_orig);
|
||||||
|
|
||||||
|
const auto libaio = "libaio.dll";
|
||||||
|
detour::trampoline_try(libaio, "aioSci_Destroy", aioSci_Destroy, &aioSci_Destroy_orig);
|
||||||
|
detour::trampoline_try(libaio, "aioNodeMgr_Destroy",
|
||||||
|
aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig);
|
||||||
|
detour::trampoline_try(libaio, "aioNodeCtl_Destroy",
|
||||||
|
aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig);
|
||||||
|
detour::trampoline_try(libaio, "aioNodeCtl_UpdateDevicesStatus",
|
||||||
|
aioNodeCtl_UpdateDevicesStatus, &aioNodeCtl_UpdateDevicesStatus_orig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if SPICE64
|
||||||
|
|
||||||
|
namespace games::jb {
|
||||||
|
void bi2x_hook_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
|
#include "bi2x_hook.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
@@ -10,6 +12,8 @@
|
|||||||
|
|
||||||
namespace games::jb {
|
namespace games::jb {
|
||||||
|
|
||||||
|
#if !SPICE64
|
||||||
|
|
||||||
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
|
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
|
||||||
static BOOL __stdcall network_addr_is_changed() {
|
static BOOL __stdcall network_addr_is_changed() {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -31,6 +35,8 @@ namespace games::jb {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
JBGame::JBGame() : Game("Jubeat") {
|
JBGame::JBGame() : Game("Jubeat") {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +66,18 @@ namespace games::jb {
|
|||||||
void JBGame::attach() {
|
void JBGame::attach() {
|
||||||
Game::attach();
|
Game::attach();
|
||||||
|
|
||||||
|
#if SPICE64
|
||||||
|
if (avs::game::DLL_NAME == "jubeat2019.dll") {
|
||||||
|
libutils::load_library("libaio.dll");
|
||||||
|
libutils::load_library("libaio-iob.dll");
|
||||||
|
libutils::load_library("libaio-iob2_video.dll");
|
||||||
|
bi2x_hook_init();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
touch_attach();
|
touch_attach();
|
||||||
|
|
||||||
|
#if !SPICE64
|
||||||
// enable debug logging of gftools
|
// enable debug logging of gftools
|
||||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||||
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
|
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
|
||||||
@@ -75,6 +91,8 @@ namespace games::jb {
|
|||||||
network, "network_get_network_check_info"));
|
network, "network_get_network_check_info"));
|
||||||
detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc(
|
detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc(
|
||||||
network, "network_get_dhcp_result"));
|
network, "network_get_dhcp_result"));
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void JBGame::detach() {
|
void JBGame::detach() {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
// (6, 8) in landscape.
|
// (6, 8) in landscape.
|
||||||
#define JB_BUTTON_SIZE 160
|
#define JB_BUTTON_SIZE 160
|
||||||
#define JB_MAX_BUTTON_GAP 38
|
#define JB_MAX_BUTTON_GAP 38
|
||||||
|
#define JB_T44_BUTTON_SIZE 224
|
||||||
|
#define JB_T44_BUTTON_GAP 33
|
||||||
|
|
||||||
// improved and plus modes use this reach around each button. must be >= the
|
// improved and plus modes use this reach around each button. must be >= the
|
||||||
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
|
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
|
||||||
@@ -41,6 +43,7 @@ namespace games::jb {
|
|||||||
static std::atomic_bool TOUCH_ENABLE = false;
|
static std::atomic_bool TOUCH_ENABLE = false;
|
||||||
static bool TOUCH_ATTACHED = false;
|
static bool TOUCH_ATTACHED = false;
|
||||||
static bool IS_PORTRAIT = true;
|
static bool IS_PORTRAIT = true;
|
||||||
|
static bool IS_T44 = false;
|
||||||
static std::atomic_uint16_t TOUCH_STATE = 0;
|
static std::atomic_uint16_t TOUCH_STATE = 0;
|
||||||
|
|
||||||
// fixed-size contact view used by the debug overlay
|
// fixed-size contact view used by the debug overlay
|
||||||
@@ -85,35 +88,45 @@ namespace games::jb {
|
|||||||
// --- touch geometry ------------------------------------------------------
|
// --- touch geometry ------------------------------------------------------
|
||||||
// gaps between the four buttons along one axis (the middle gap is 1px wider)
|
// gaps between the four buttons along one axis (the middle gap is 1px wider)
|
||||||
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
|
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
|
||||||
|
static const int JB_T44_BUTTON_GAPS[3] = {
|
||||||
|
JB_T44_BUTTON_GAP,
|
||||||
|
JB_T44_BUTTON_GAP,
|
||||||
|
JB_T44_BUTTON_GAP,
|
||||||
|
};
|
||||||
|
|
||||||
struct AxisGeometry {
|
struct AxisGeometry {
|
||||||
|
int size;
|
||||||
int button[4]; // left/top edge of each button
|
int button[4]; // left/top edge of each button
|
||||||
};
|
};
|
||||||
|
|
||||||
// left/top edges of the four buttons along one axis, starting at `first`
|
// left/top edges of the four buttons along one axis, starting at `first`
|
||||||
static AxisGeometry axis_geometry(int first) {
|
static AxisGeometry axis_geometry(int first, int size, const int gaps[3]) {
|
||||||
AxisGeometry g {};
|
AxisGeometry g {};
|
||||||
|
g.size = size;
|
||||||
g.button[0] = first;
|
g.button[0] = first;
|
||||||
for (int i = 1; i < 4; i++) {
|
for (int i = 1; i < 4; i++) {
|
||||||
g.button[i] = g.button[i - 1] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1];
|
g.button[i] = g.button[i - 1] + size + gaps[i - 1];
|
||||||
}
|
}
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
// button edges for the current orientation
|
// button edges for the current orientation
|
||||||
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
|
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
|
||||||
if (IS_PORTRAIT) {
|
if (IS_T44) {
|
||||||
gx = axis_geometry(8);
|
gx = axis_geometry(37, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
|
||||||
gy = axis_geometry(602);
|
gy = axis_geometry(864, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
|
||||||
|
} else if (IS_PORTRAIT) {
|
||||||
|
gx = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
|
||||||
|
gy = axis_geometry(602, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
|
||||||
} else {
|
} else {
|
||||||
gx = axis_geometry(6);
|
gx = axis_geometry(6, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
|
||||||
gy = axis_geometry(8);
|
gy = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// distance from `p` to a button along one axis (0 when inside)
|
// distance from `p` to a button along one axis (0 when inside)
|
||||||
static int axis_distance(int p, int button) {
|
static int axis_distance(int p, int button, int size) {
|
||||||
int end = button + JB_BUTTON_SIZE - 1;
|
int end = button + size - 1;
|
||||||
if (p < button) {
|
if (p < button) {
|
||||||
return button - p;
|
return button - p;
|
||||||
}
|
}
|
||||||
@@ -131,8 +144,8 @@ namespace games::jb {
|
|||||||
int best_dist = 0;
|
int best_dist = 0;
|
||||||
for (int r = 0; r < 4; r++) {
|
for (int r = 0; r < 4; r++) {
|
||||||
for (int c = 0; c < 4; c++) {
|
for (int c = 0; c < 4; c++) {
|
||||||
int dx = axis_distance(px, gx.button[c]);
|
int dx = axis_distance(px, gx.button[c], gx.size);
|
||||||
int dy = axis_distance(py, gy.button[r]);
|
int dy = axis_distance(py, gy.button[r], gy.size);
|
||||||
int dist = dx * dx + dy * dy;
|
int dist = dx * dx + dy * dy;
|
||||||
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
|
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
|
||||||
best_dist = dist;
|
best_dist = dist;
|
||||||
@@ -145,7 +158,8 @@ namespace games::jb {
|
|||||||
|
|
||||||
// detection reach for the current algorithm (0 = register only inside a button)
|
// detection reach for the current algorithm (0 = register only inside a button)
|
||||||
static int touch_radius() {
|
static int touch_radius() {
|
||||||
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS;
|
return TOUCH_ALGORITHM == AcAccurate || (IS_T44 && TOUCH_ALGORITHM == Legacy) ?
|
||||||
|
0 : JB_TOUCH_RADIUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
|
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
|
||||||
@@ -162,8 +176,8 @@ namespace games::jb {
|
|||||||
}
|
}
|
||||||
for (int r = 0; r < 4; r++) {
|
for (int r = 0; r < 4; r++) {
|
||||||
for (int c = 0; c < 4; c++) {
|
for (int c = 0; c < 4; c++) {
|
||||||
int dx = axis_distance(px, gx.button[c]);
|
int dx = axis_distance(px, gx.button[c], gx.size);
|
||||||
int dy = axis_distance(py, gy.button[r]);
|
int dy = axis_distance(py, gy.button[r], gy.size);
|
||||||
if (dx * dx + dy * dy <= radius * radius) {
|
if (dx * dx + dy * dy <= radius * radius) {
|
||||||
state |= uint16_t(1) << (r * 4 + c);
|
state |= uint16_t(1) << (r * 4 + c);
|
||||||
}
|
}
|
||||||
@@ -184,11 +198,15 @@ namespace games::jb {
|
|||||||
// one-time touch window attach
|
// one-time touch window attach
|
||||||
if (!TOUCH_ATTACHED) {
|
if (!TOUCH_ATTACHED) {
|
||||||
|
|
||||||
|
IS_T44 = avs::game::is_model("T44");
|
||||||
|
IS_PORTRAIT = IS_T44 || avs::game::is_model("L44");
|
||||||
|
|
||||||
// find the game window: prefer the foreground window, else search by
|
// find the game window: prefer the foreground window, else search by
|
||||||
// title (the model name prefixes the window title in every version)
|
// title (T44 uses a fixed title instead of the model prefix)
|
||||||
|
const char *window_title = IS_T44 ? "jubeat 10 main" : avs::game::MODEL;
|
||||||
HWND wnd = GetForegroundWindow();
|
HWND wnd = GetForegroundWindow();
|
||||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
if (!string_begins_with(GetActiveWindowTitle(), window_title)) {
|
||||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
wnd = FindWindowBeginsWith(window_title);
|
||||||
}
|
}
|
||||||
if (!wnd) {
|
if (!wnd) {
|
||||||
log_warning("jubeat", "could not find window handle for touch");
|
log_warning("jubeat", "could not find window handle for touch");
|
||||||
@@ -197,10 +215,6 @@ namespace games::jb {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only the L44 model runs in portrait; set this before starting the
|
|
||||||
// touch-window thread so the renderer only observes the final value
|
|
||||||
IS_PORTRAIT = avs::game::is_model("L44");
|
|
||||||
|
|
||||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||||
|
|
||||||
// let the rawinput stack correct the aspect ratio
|
// let the rawinput stack correct the aspect ratio
|
||||||
@@ -232,7 +246,7 @@ namespace games::jb {
|
|||||||
return !touch_matured(tp, now_ms, threshold_ms);
|
return !touch_matured(tp, now_ms, threshold_ms);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (TOUCH_ALGORITHM == Legacy) {
|
if (TOUCH_ALGORITHM == Legacy && !IS_T44) {
|
||||||
|
|
||||||
// legacy: evenly divide the play area into a 4x4 grid
|
// legacy: evenly divide the play area into a 4x4 grid
|
||||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||||
@@ -311,7 +325,7 @@ namespace games::jb {
|
|||||||
for (int c = 0; c < 4; c++) {
|
for (int c = 0; c < 4; c++) {
|
||||||
cells[r * 4 + c] = {
|
cells[r * 4 + c] = {
|
||||||
gx.button[c], gy.button[r],
|
gx.button[c], gy.button[r],
|
||||||
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE
|
gx.button[c] + gx.size, gy.button[r] + gy.size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,8 +366,8 @@ namespace games::jb {
|
|||||||
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
|
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
|
||||||
int c = index % 4;
|
int c = index % 4;
|
||||||
int r = index / 4;
|
int r = index / 4;
|
||||||
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
|
double mid = std::atan2((gy.button[r] + gy.size / 2) - py,
|
||||||
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
|
(gx.button[c] + gx.size / 2) - px);
|
||||||
const double quarter = 3.14159265358979323846 / 2.0;
|
const double quarter = 3.14159265358979323846 / 2.0;
|
||||||
const int segments = 16;
|
const int segments = 16;
|
||||||
POINT arc[segments + 1];
|
POINT arc[segments + 1];
|
||||||
@@ -425,7 +439,7 @@ namespace games::jb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// legacy divides the field evenly; the other algorithms use button squares
|
// legacy divides the field evenly; the other algorithms use button squares
|
||||||
bool legacy = (TOUCH_ALGORITHM == Legacy);
|
bool legacy = TOUCH_ALGORITHM == Legacy && !IS_T44;
|
||||||
AxisGeometry gx {}, gy {};
|
AxisGeometry gx {}, gy {};
|
||||||
if (!legacy) {
|
if (!legacy) {
|
||||||
touch_geometry(gx, gy);
|
touch_geometry(gx, gy);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "poke.h"
|
#include "poke.h"
|
||||||
#include "touch_mode.h"
|
#include "touch_mode.h"
|
||||||
#include "hooks/setupapihook.h"
|
#include "hooks/setupapihook.h"
|
||||||
|
#include "misc/wintouchemu.h"
|
||||||
#include "touch/native/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
|
|
||||||
@@ -41,9 +42,15 @@ namespace games::nost {
|
|||||||
setupapihook_init(avs::game::DLL_INSTANCE);
|
setupapihook_init(avs::game::DLL_INSTANCE);
|
||||||
setupapihook_add(touch_settings);
|
setupapihook_add(touch_settings);
|
||||||
|
|
||||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||||
if (ENABLE_TOUCH_MODE) {
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
touch_mode::enable();
|
if (!native_touch_ready) {
|
||||||
|
wintouchemu::FORCE = true;
|
||||||
|
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||||
|
} else {
|
||||||
|
if (ENABLE_TOUCH_MODE) {
|
||||||
|
touch_mode::enable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "poke.h"
|
#include "poke.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
@@ -9,8 +10,8 @@
|
|||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "touch/native/inject.h"
|
#include "touch/native/inject.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
|
|
||||||
namespace games::nost::poke {
|
namespace games::nost::poke {
|
||||||
|
|
||||||
@@ -59,11 +60,14 @@ namespace games::nost::poke {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nativetouch::is_hooked()) {
|
||||||
|
log_warning("poke", "Nostalgia poke requires native touch; disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// create new thread
|
// create new thread
|
||||||
THREAD_RUNNING = true;
|
THREAD_RUNNING = true;
|
||||||
THREAD = new std::thread([] {
|
THREAD = new std::thread([] {
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
const int swipe_anim_total_frames = 6;
|
const int swipe_anim_total_frames = 6;
|
||||||
const int swipe_anim_y = 300;
|
const int swipe_anim_y = 300;
|
||||||
|
|
||||||
@@ -188,7 +192,7 @@ namespace games::nost::poke {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// slow down
|
// slow down
|
||||||
timer.sleep(30);
|
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contact_active) {
|
if (contact_active) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "launcher/logger.h"
|
#include "launcher/logger.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "misc/wintouchemu.h"
|
||||||
#include "util/sysutils.h"
|
#include "util/sysutils.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
namespace games::popn {
|
namespace games::popn {
|
||||||
|
|
||||||
bool SHOW_PIKA_MONITOR_WARNING = false;
|
bool SHOW_PIKA_MONITOR_WARNING = false;
|
||||||
|
bool NATIVE_TOUCH = true;
|
||||||
|
|
||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
@@ -713,7 +715,18 @@ namespace games::popn {
|
|||||||
// 00000100 0000000B 00000001 (button 9)
|
// 00000100 0000000B 00000001 (button 9)
|
||||||
// set third column to 0 and it will work with BIO2
|
// set third column to 0 and it will work with BIO2
|
||||||
|
|
||||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||||
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
|
if (!NATIVE_TOUCH) {
|
||||||
|
wintouchemu::FORCE = true;
|
||||||
|
if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
|
wintouchemu::hook_title_ends(
|
||||||
|
"",
|
||||||
|
"Main Screen",
|
||||||
|
avs::game::DLL_INSTANCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sysutils::hook_EnumDisplayDevicesA();
|
sysutils::hook_EnumDisplayDevicesA();
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace games::popn {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
extern bool SHOW_PIKA_MONITOR_WARNING;
|
extern bool SHOW_PIKA_MONITOR_WARNING;
|
||||||
|
extern bool NATIVE_TOUCH;
|
||||||
|
|
||||||
class POPNGame : public games::Game {
|
class POPNGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#if SPICE64
|
#if SPICE64
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include "api/client.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -349,43 +350,29 @@ namespace games::sdvx {
|
|||||||
* 9 - v unit - 258 bytes - 86 colors
|
* 9 - v unit - 258 bytes - 86 colors
|
||||||
*
|
*
|
||||||
* data is stored in RGB order, 3 bytes per color
|
* data is stored in RGB order, 3 bytes per color
|
||||||
*
|
|
||||||
* TODO: expose this data via API
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// data mapping
|
|
||||||
static struct TapeLedMapping {
|
|
||||||
size_t data_size;
|
|
||||||
int index_r, index_g, index_b;
|
|
||||||
|
|
||||||
TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b)
|
|
||||||
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {}
|
|
||||||
|
|
||||||
} mapping[] = {
|
|
||||||
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B },
|
|
||||||
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B },
|
|
||||||
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B },
|
|
||||||
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B },
|
|
||||||
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B },
|
|
||||||
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B },
|
|
||||||
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B },
|
|
||||||
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B },
|
|
||||||
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B },
|
|
||||||
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B },
|
|
||||||
};
|
|
||||||
|
|
||||||
// check index bounds
|
// check index bounds
|
||||||
if (tapeledutils::is_enabled() && index < std::size(mapping)) {
|
if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) {
|
||||||
auto &map = mapping[index];
|
auto &map = TAPELED_MAPPING[index];
|
||||||
|
const auto data_size = map.data.size();
|
||||||
|
|
||||||
// pick a color to use
|
// pick a color to use
|
||||||
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size);
|
const auto rgb = tapeledutils::pick_color_from_led_tape(data, data_size);
|
||||||
|
|
||||||
// program the lights into API
|
// program the lights into API
|
||||||
auto &lights = get_lights();
|
auto &lights = get_lights();
|
||||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
|
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
|
||||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
|
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
|
||||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
||||||
|
|
||||||
|
if (api::has_clients()) {
|
||||||
|
for (size_t i = 0; i < data_size; ++i) {
|
||||||
|
map.data[i].r = data[i * 3];
|
||||||
|
map.data[i].g = data[i * 3 + 1];
|
||||||
|
map.data[i].b = data[i * 3 + 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (This != custom_node) {
|
if (This != custom_node) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/sysutils.h"
|
#include "util/sysutils.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "misc/wintouchemu.h"
|
||||||
#include "touch/native/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
#include "camera.h"
|
#include "camera.h"
|
||||||
@@ -60,6 +61,19 @@ namespace games::sdvx {
|
|||||||
static HKEY real_asio_reg_handle = nullptr;
|
static HKEY real_asio_reg_handle = nullptr;
|
||||||
static HKEY real_asio_device_reg_handle = nullptr;
|
static HKEY real_asio_device_reg_handle = nullptr;
|
||||||
|
|
||||||
|
tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL] = {
|
||||||
|
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B, "Title" },
|
||||||
|
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B, "Upper Left Speaker" },
|
||||||
|
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B, "Upper Right Speaker" },
|
||||||
|
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B, "Left Wing" },
|
||||||
|
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B, "Right Wing" },
|
||||||
|
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B, "Control Panel" },
|
||||||
|
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B, "Lower Left Speaker" },
|
||||||
|
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B, "Lower Right Speaker" },
|
||||||
|
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B, "Woofer" },
|
||||||
|
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B, "V Unit" },
|
||||||
|
};
|
||||||
|
|
||||||
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||||
if (lpSubKey != nullptr &&
|
if (lpSubKey != nullptr &&
|
||||||
phkResult != nullptr &&
|
phkResult != nullptr &&
|
||||||
@@ -453,7 +467,18 @@ namespace games::sdvx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is_valkyrie_model()) {
|
if (is_valkyrie_model()) {
|
||||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||||
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
|
if (!native_touch_ready) {
|
||||||
|
wintouchemu::FORCE = true;
|
||||||
|
if (!GRAPHICS_WINDOWED) {
|
||||||
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
|
wintouchemu::hook_title_ends(
|
||||||
|
"SOUND VOLTEX",
|
||||||
|
"Main Screen",
|
||||||
|
avs::game::DLL_INSTANCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// insert BI2X hooks
|
// insert BI2X hooks
|
||||||
bi2x_hook_init();
|
bi2x_hook_init();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/game.h"
|
#include "games/game.h"
|
||||||
|
#include "util/tapeled.h"
|
||||||
|
|
||||||
namespace games::sdvx {
|
namespace games::sdvx {
|
||||||
|
|
||||||
@@ -25,6 +26,9 @@ namespace games::sdvx {
|
|||||||
// states
|
// states
|
||||||
extern bool SHOW_VM_MONITOR_WARNING;
|
extern bool SHOW_VM_MONITOR_WARNING;
|
||||||
|
|
||||||
|
constexpr int SDVX_TAPELED_TOTAL = 10;
|
||||||
|
extern tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL];
|
||||||
|
|
||||||
static inline bool is_valkyrie_model() {
|
static inline bool is_valkyrie_model() {
|
||||||
return (
|
return (
|
||||||
avs::game::is_model("KFC") &&
|
avs::game::is_model("KFC") &&
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "hooks/audio/util.h"
|
#include "hooks/audio/util.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
|
|
||||||
NullDiscardBackend::~NullDiscardBackend() {
|
NullDiscardBackend::~NullDiscardBackend() {
|
||||||
this->running = false;
|
this->running = false;
|
||||||
@@ -125,8 +125,6 @@ HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
|
|||||||
void NullDiscardBackend::pace_loop() {
|
void NullDiscardBackend::pace_loop() {
|
||||||
using namespace std::chrono;
|
using namespace std::chrono;
|
||||||
|
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
// audio is discarded, so timing precision and drift do not matter; just wake the
|
// audio is discarded, so timing precision and drift do not matter; just wake the
|
||||||
// game once per buffer period to keep its render thread from blocking on the event.
|
// game once per buffer period to keep its render thread from blocking on the event.
|
||||||
const auto period = duration_cast<steady_clock::duration>(
|
const auto period = duration_cast<steady_clock::duration>(
|
||||||
@@ -136,6 +134,6 @@ void NullDiscardBackend::pace_loop() {
|
|||||||
if (this->relay_handle) {
|
if (this->relay_handle) {
|
||||||
SetEvent(this->relay_handle);
|
SetEvent(this->relay_handle);
|
||||||
}
|
}
|
||||||
timer.sleep(period);
|
std::this_thread::sleep_for(period);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -574,7 +574,7 @@ void devicehook_init(HMODULE module) {
|
|||||||
STORE(EscapeCommFunction_orig, detour::iat_try("EscapeCommFunction", EscapeCommFunction_hook, module));
|
STORE(EscapeCommFunction_orig, detour::iat_try("EscapeCommFunction", EscapeCommFunction_hook, module));
|
||||||
STORE(GetCommState_orig, detour::iat_try("GetCommState", GetCommState_hook, module));
|
STORE(GetCommState_orig, detour::iat_try("GetCommState", GetCommState_hook, module));
|
||||||
STORE(GetFileSize_orig, detour::iat_try("GetFileSize", GetFileSize_hook, module));
|
STORE(GetFileSize_orig, detour::iat_try("GetFileSize", GetFileSize_hook, module));
|
||||||
STORE(GetFileSizeEx_orig, detour::iat_try("GetFileSize", GetFileSizeEx_hook, module));
|
STORE(GetFileSizeEx_orig, detour::iat_try("GetFileSizeEx", GetFileSizeEx_hook, module));
|
||||||
STORE(GetFileInformationByHandle_orig, detour::iat_try(
|
STORE(GetFileInformationByHandle_orig, detour::iat_try(
|
||||||
"GetFileInformationByHandle", GetFileInformationByHandle_hook, module));
|
"GetFileInformationByHandle", GetFileInformationByHandle_hook, module));
|
||||||
STORE(PurgeComm_orig, detour::iat_try("PurgeComm", PurgeComm_hook, module));
|
STORE(PurgeComm_orig, detour::iat_try("PurgeComm", PurgeComm_hook, module));
|
||||||
|
|||||||
@@ -23,10 +23,7 @@
|
|||||||
#include "external/imgui/backends/imgui_impl_dx11.h"
|
#include "external/imgui/backends/imgui_impl_dx11.h"
|
||||||
#include "overlay/imgui/impl_spice.h"
|
#include "overlay/imgui/impl_spice.h"
|
||||||
|
|
||||||
#include "games/io.h"
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "launcher/launcher.h"
|
|
||||||
#include "misc/eamuse.h"
|
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -108,8 +105,62 @@ Present1_t Present1_orig = nullptr;
|
|||||||
bool g_swapchain_hooked = false;
|
bool g_swapchain_hooked = false;
|
||||||
bool g_swapchain1_hooked = false;
|
bool g_swapchain1_hooked = false;
|
||||||
|
|
||||||
|
// sub-screens / IME helpers are usually child or zero-sized windows.
|
||||||
|
// visibility isn't checked - the game may present before showing the window.
|
||||||
|
bool looks_like_game_window(HWND hwnd) {
|
||||||
|
RECT client {};
|
||||||
|
return GetAncestor(hwnd, GA_ROOT) == hwnd
|
||||||
|
&& GetClientRect(hwnd, &client)
|
||||||
|
&& client.right > client.left
|
||||||
|
&& client.bottom > client.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only the main game window; ignore sub-screens / IME helpers.
|
||||||
|
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
|
||||||
|
DXGI_SWAP_CHAIN_DESC desc {};
|
||||||
|
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND main = d3d11_hooks::main_hwnd();
|
||||||
|
if (!main) {
|
||||||
|
// no creation hook recorded a window, so fall back to the presenting one;
|
||||||
|
// the choice is permanent, so require a plausible game window
|
||||||
|
if (!looks_like_game_window(desc.OutputWindow)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"graphics::d3d11",
|
||||||
|
"try to notemain hwnd from swapchain present: 0x{:x}",
|
||||||
|
(uintptr_t)desc.OutputWindow);
|
||||||
|
|
||||||
|
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
|
||||||
|
|
||||||
|
// it may have been ignored, or another thread may have won the slot
|
||||||
|
main = d3d11_hooks::main_hwnd();
|
||||||
|
}
|
||||||
|
return desc.OutputWindow == main;
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks are ordered cheapest first, since this runs on every present
|
||||||
void try_create_overlay(IDXGISwapChain *swapchain) {
|
void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||||
if (!swapchain || overlay::OVERLAY) {
|
if (!swapchain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// overlay is disabled by user
|
||||||
|
if (!overlay::ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// overlay is already enabled and attached
|
||||||
|
if (overlay::OVERLAY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore sub windows
|
||||||
|
if (!is_main_game_swapchain(swapchain)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +169,6 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only attach to the main game window; ignore sub-screens / IME helpers.
|
|
||||||
HWND main = d3d11_hooks::main_hwnd();
|
|
||||||
if (main && desc.OutputWindow != main) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// theme the native title bar; first present is the only reliable point for
|
// theme the native title bar; first present is the only reliable point for
|
||||||
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
|
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
|
||||||
set_window_dark_titlebar(desc.OutputWindow);
|
set_window_dark_titlebar(desc.OutputWindow);
|
||||||
@@ -149,43 +194,42 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
|||||||
device->Release();
|
device->Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour).
|
// screenshots have to keep working with the overlay disabled, so they are not gated on it
|
||||||
void poll_screenshot_hotkey() {
|
void pump_frame(IDXGISwapChain *swapchain) {
|
||||||
static bool s_down = false;
|
const bool has_overlay =
|
||||||
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
||||||
const bool pressed = buttons
|
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
|
||||||
&& (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered())
|
|
||||||
&& GameAPI::Buttons::getState(RI_MGR,
|
|
||||||
buttons->at(games::OverlayButtons::Screenshot));
|
|
||||||
if (pressed && !s_down) {
|
|
||||||
graphics_screenshot_trigger();
|
|
||||||
}
|
|
||||||
s_down = pressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void pump_overlay(IDXGISwapChain *swapchain) {
|
|
||||||
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
poll_screenshot_hotkey();
|
graphics_poll_screenshot_hotkey();
|
||||||
|
|
||||||
// size imgui to the backbuffer (not window client). dxgi may upscale
|
// before the overlay render so the screenshot excludes it
|
||||||
// a small backbuffer into a larger client rect; without this override
|
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||||
// imgui would draw past the RTV and the mouse mapping would be off.
|
d3d11_hooks::try_screenshot(swapchain);
|
||||||
DXGI_SWAP_CHAIN_DESC desc {};
|
|
||||||
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
|
|
||||||
ImGui_ImplSpice_SetDisplaySizeOverride(
|
|
||||||
(float) desc.BufferDesc.Width,
|
|
||||||
(float) desc.BufferDesc.Height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay::OVERLAY->update();
|
if (has_overlay) {
|
||||||
overlay::OVERLAY->new_frame();
|
|
||||||
overlay::OVERLAY->render();
|
|
||||||
|
|
||||||
// after overlay render so toasts/menus end up in the saved image.
|
// size imgui to the backbuffer (not window client). dxgi may upscale
|
||||||
d3d11_hooks::try_screenshot(swapchain);
|
// a small backbuffer into a larger client rect; without this override
|
||||||
|
// imgui would draw past the RTV and the mouse mapping would be off.
|
||||||
|
DXGI_SWAP_CHAIN_DESC desc {};
|
||||||
|
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
|
||||||
|
ImGui_ImplSpice_SetDisplaySizeOverride(
|
||||||
|
(float) desc.BufferDesc.Width,
|
||||||
|
(float) desc.BufferDesc.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay::OVERLAY->update();
|
||||||
|
overlay::OVERLAY->new_frame();
|
||||||
|
overlay::OVERLAY->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// after the overlay render so the screenshot includes toasts / menus
|
||||||
|
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||||
|
d3d11_hooks::try_screenshot(swapchain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@@ -194,8 +238,11 @@ void pump_overlay(IDXGISwapChain *swapchain) {
|
|||||||
HRESULT STDMETHODCALLTYPE Present_hook(
|
HRESULT STDMETHODCALLTYPE Present_hook(
|
||||||
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
|
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
|
||||||
{
|
{
|
||||||
try_create_overlay(swapchain);
|
// a test present doesn't display anything; don't pick a window or take a screenshot off it
|
||||||
pump_overlay(swapchain);
|
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||||
|
try_create_overlay(swapchain);
|
||||||
|
pump_frame(swapchain);
|
||||||
|
}
|
||||||
return Present_orig(swapchain, SyncInterval, Flags);
|
return Present_orig(swapchain, SyncInterval, Flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +250,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
|
|||||||
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
|
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
|
||||||
const DXGI_PRESENT_PARAMETERS *pParams)
|
const DXGI_PRESENT_PARAMETERS *pParams)
|
||||||
{
|
{
|
||||||
try_create_overlay(swapchain);
|
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||||
pump_overlay(swapchain);
|
try_create_overlay(swapchain);
|
||||||
|
pump_frame(swapchain);
|
||||||
|
}
|
||||||
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
#include "d3d9_backend.h"
|
#include "d3d9_backend.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <external/robin_hood.h>
|
|
||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
#ifdef __GNUC__
|
|
||||||
#include <d3dx9tex.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "cfg/screen_resize.h"
|
#include "cfg/screen_resize.h"
|
||||||
@@ -18,28 +15,23 @@
|
|||||||
#include "games/popn/popn.h"
|
#include "games/popn/popn.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
#include "games/mfc/mfc.h"
|
#include "games/mfc/mfc.h"
|
||||||
#include "games/io.h"
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "launcher/options.h"
|
#include "launcher/options.h"
|
||||||
#include "launcher/signal.h"
|
#include "launcher/signal.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "misc/clipboard.h"
|
|
||||||
#include "misc/eamuse.h"
|
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "overlay/notifications.h"
|
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "util/fileutils.h"
|
|
||||||
#include "util/flags_helper.h"
|
#include "util/flags_helper.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "util/memutils.h"
|
#include "util/memutils.h"
|
||||||
#include "util/threadpool.h"
|
|
||||||
|
|
||||||
#include "d3d9_device.h"
|
#include "d3d9_device.h"
|
||||||
|
#include "d3d9_screenshot.h"
|
||||||
|
|
||||||
#ifdef min
|
#ifdef min
|
||||||
#undef min
|
#undef min
|
||||||
@@ -63,19 +55,6 @@
|
|||||||
return __ret; \
|
return __ret; \
|
||||||
} while (0)
|
} while (0)
|
||||||
|
|
||||||
#ifdef __GNUC__
|
|
||||||
typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t;
|
|
||||||
#else
|
|
||||||
#define D3DXIFF_PNG ((DWORD) 3)
|
|
||||||
|
|
||||||
typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)(
|
|
||||||
LPCSTR pDestFile,
|
|
||||||
DWORD DestFormat,
|
|
||||||
LPDIRECT3DSURFACE9 pSrcSurface,
|
|
||||||
CONST PALETTEENTRY *pSrcPalette,
|
|
||||||
CONST RECT *pSrcRect);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 9 on 12
|
* 9 on 12
|
||||||
*/
|
*/
|
||||||
@@ -99,8 +78,6 @@ typedef IDirect3D9* (WINAPI *Direct3DCreate9On12_t)(
|
|||||||
static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr;
|
static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr;
|
||||||
static char D3D9_DIRECT3D_CREATE9_CONTENTS[16];
|
static char D3D9_DIRECT3D_CREATE9_CONTENTS[16];
|
||||||
|
|
||||||
static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false;
|
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
std::optional<UINT> D3D9_ADAPTER = std::nullopt;
|
std::optional<UINT> D3D9_ADAPTER = std::nullopt;
|
||||||
DWORD D3D9_BEHAVIOR_DISABLE = 0;
|
DWORD D3D9_BEHAVIOR_DISABLE = 0;
|
||||||
@@ -113,7 +90,16 @@ static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr;
|
|||||||
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
|
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
|
||||||
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
|
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
|
||||||
|
|
||||||
static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params);
|
// main and subscreen presents may occur on different threads.
|
||||||
|
static std::atomic_bool SUBSCREEN_PRESENTED_SINCE_LAST_MAIN = false;
|
||||||
|
|
||||||
|
// do not mistake the fallback Present below for a game-originated Present.
|
||||||
|
static thread_local bool SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = false;
|
||||||
|
|
||||||
|
static void graphics_d3d9_ldj_init_sub_screen(
|
||||||
|
IDirect3DDevice9Ex *device,
|
||||||
|
D3DPRESENT_PARAMETERS *present_params,
|
||||||
|
UINT gfdm_small_swapchain);
|
||||||
|
|
||||||
static std::string behavior2s(DWORD behavior_flags) {
|
static std::string behavior2s(DWORD behavior_flags) {
|
||||||
FLAGS_START(behavior_flags);
|
FLAGS_START(behavior_flags);
|
||||||
@@ -409,7 +395,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::QueryInterface(
|
|||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (riid == IID_WrappedIDirect3D9 ||
|
if ((riid == IID_IUnknown && gfdm_two_head_exclusive()) ||
|
||||||
|
riid == IID_WrappedIDirect3D9 ||
|
||||||
riid == IID_IDirect3D9 ||
|
riid == IID_IDirect3D9 ||
|
||||||
riid == IID_IDirect3D9Ex)
|
riid == IID_IDirect3D9Ex)
|
||||||
{
|
{
|
||||||
@@ -467,6 +454,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
|
|||||||
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
|
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
|
||||||
UINT result = pReal->GetAdapterCount();
|
UINT result = pReal->GetAdapterCount();
|
||||||
|
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
if (result == 2) {
|
||||||
|
FAKE_SUBSCREEN_ADAPTER = true;
|
||||||
|
return GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
}
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode requires two native adapters; found {}",
|
||||||
|
result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (!FAKE_SUBSCREEN_ADAPTER) {
|
if (!FAKE_SUBSCREEN_ADAPTER) {
|
||||||
if (games::popn::is_pikapika_model() && result == 1) {
|
if (games::popn::is_pikapika_model() && result == 1) {
|
||||||
FAKE_SUBSCREEN_ADAPTER = true;
|
FAKE_SUBSCREEN_ADAPTER = true;
|
||||||
@@ -493,7 +492,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
|
|||||||
D3DADAPTER_IDENTIFIER9 *pIdentifier)
|
D3DADAPTER_IDENTIFIER9 *pIdentifier)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) {
|
if (is_fake_subscreen_adapter(Adapter) && pIdentifier) {
|
||||||
*pIdentifier = {};
|
*pIdentifier = {};
|
||||||
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
|
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
|
||||||
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
|
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
|
||||||
@@ -509,7 +508,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
|
|||||||
}
|
}
|
||||||
|
|
||||||
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
|
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
if (is_fake_subscreen_adapter(Adapter)) {
|
||||||
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
|
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -523,13 +522,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
|
|||||||
UINT Mode,
|
UINT Mode,
|
||||||
D3DDISPLAYMODE *pMode)
|
D3DDISPLAYMODE *pMode)
|
||||||
{
|
{
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
if (is_fake_subscreen_adapter(Adapter)) {
|
||||||
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
|
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
|
||||||
if (Mode == 0 && pMode) {
|
if (Mode == 0 && pMode) {
|
||||||
pMode->Width = 1280;
|
get_fake_subscreen_display_mode(Adapter, pMode);
|
||||||
pMode->Height = 800;
|
|
||||||
pMode->RefreshRate = 60;
|
|
||||||
pMode->Format = D3DFMT_X8R8G8B8;
|
|
||||||
return S_OK;
|
return S_OK;
|
||||||
} else {
|
} else {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
@@ -611,6 +607,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE *pMode) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE *pMode) {
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
get_fake_subscreen_display_mode(Adapter, pMode);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetAdapterDisplayMode(Adapter, pMode));
|
CHECK_RESULT(pReal->GetAdapterDisplayMode(Adapter, pMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,6 +625,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceType(
|
|||||||
D3DFORMAT BackBufferFormat,
|
D3DFORMAT BackBufferFormat,
|
||||||
BOOL bWindowed)
|
BOOL bWindowed)
|
||||||
{
|
{
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(iAdapter)) {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed));
|
CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,7 +640,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
|
|||||||
D3DRESOURCETYPE RType,
|
D3DRESOURCETYPE RType,
|
||||||
D3DFORMAT CheckFormat)
|
D3DFORMAT CheckFormat)
|
||||||
{
|
{
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
if (is_fake_subscreen_adapter(Adapter)) {
|
||||||
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
|
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
@@ -648,6 +656,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceMultiSampleType(
|
|||||||
D3DMULTISAMPLE_TYPE MultiSampleType,
|
D3DMULTISAMPLE_TYPE MultiSampleType,
|
||||||
DWORD *pQualityLevels)
|
DWORD *pQualityLevels)
|
||||||
{
|
{
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (pQualityLevels != nullptr) {
|
||||||
|
*pQualityLevels = 1;
|
||||||
|
}
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->CheckDeviceMultiSampleType(
|
CHECK_RESULT(pReal->CheckDeviceMultiSampleType(
|
||||||
Adapter,
|
Adapter,
|
||||||
DeviceType,
|
DeviceType,
|
||||||
@@ -664,6 +679,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDepthStencilMatch(
|
|||||||
D3DFORMAT RenderTargetFormat,
|
D3DFORMAT RenderTargetFormat,
|
||||||
D3DFORMAT DepthStencilFormat)
|
D3DFORMAT DepthStencilFormat)
|
||||||
{
|
{
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->CheckDepthStencilMatch(
|
CHECK_RESULT(pReal->CheckDepthStencilMatch(
|
||||||
Adapter,
|
Adapter,
|
||||||
DeviceType,
|
DeviceType,
|
||||||
@@ -678,6 +697,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormatConversion(
|
|||||||
D3DFORMAT SourceFormat,
|
D3DFORMAT SourceFormat,
|
||||||
D3DFORMAT TargetFormat)
|
D3DFORMAT TargetFormat)
|
||||||
{
|
{
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat));
|
CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,6 +712,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
|
|||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gfdm_two_head_exclusive() && Adapter < GFDM_LOGICAL_HEAD_COUNT) {
|
||||||
|
HRESULT ret = this->pReal->GetDeviceCaps(
|
||||||
|
Adapter >= 2 ? 0 : Adapter,
|
||||||
|
DeviceType,
|
||||||
|
pCaps);
|
||||||
|
if (SUCCEEDED(ret)) {
|
||||||
|
pCaps->NumberOfAdaptersInGroup =
|
||||||
|
Adapter == 0 ? GFDM_LOGICAL_HEAD_COUNT : 0;
|
||||||
|
pCaps->MasterAdapterOrdinal = 0;
|
||||||
|
pCaps->AdapterOrdinalInGroup = Adapter;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
// SDVX uses `NumberOfAdaptersInGroup` to allocate a vector and the Microsoft documentation states:
|
// SDVX uses `NumberOfAdaptersInGroup` to allocate a vector and the Microsoft documentation states:
|
||||||
// "The value will be 0 for a subordinate adapter of a multihead card. Each card can have at most one
|
// "The value will be 0 for a subordinate adapter of a multihead card. Each card can have at most one
|
||||||
// master, but may have many subordinates." Therefore, this must point to the master adapter.
|
// master, but may have many subordinates." Therefore, this must point to the master adapter.
|
||||||
@@ -741,6 +778,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
|
|||||||
}
|
}
|
||||||
|
|
||||||
HMONITOR STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterMonitor(UINT Adapter) {
|
HMONITOR STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterMonitor(UINT Adapter) {
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
return pReal->GetAdapterMonitor(0);
|
||||||
|
}
|
||||||
|
|
||||||
return pReal->GetAdapterMonitor(Adapter);
|
return pReal->GetAdapterMonitor(Adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,7 +902,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
|||||||
"either use -graphics-force-refresh option or change the desktop resolution beforehand.");
|
"either use -graphics-force-refresh option or change the desktop resolution beforehand.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
if (!GRAPHICS_WINDOWED && num_adapters >= 2
|
||||||
|
&& GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
||||||
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
|
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
|
||||||
pPresentationParameters[1].FullScreen_RefreshRateInHz,
|
pPresentationParameters[1].FullScreen_RefreshRateInHz,
|
||||||
GRAPHICS_FORCE_REFRESH_SUB.value());
|
GRAPHICS_FORCE_REFRESH_SUB.value());
|
||||||
@@ -902,7 +944,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
|||||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
|
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
||||||
|
hFocusWindow,
|
||||||
|
*ppReturnedDeviceInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return result
|
// return result
|
||||||
@@ -916,6 +960,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
|||||||
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) {
|
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) {
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
return static_cast<IDirect3D9Ex *>(pReal)->GetAdapterModeCountEx(Adapter, pFilter);
|
return static_cast<IDirect3D9Ex *>(pReal)->GetAdapterModeCountEx(Adapter, pFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,6 +974,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModesEx(
|
|||||||
D3DDISPLAYMODEEX *pMode)
|
D3DDISPLAYMODEEX *pMode)
|
||||||
{
|
{
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (Mode != 0 || pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
get_fake_subscreen_display_mode_ex(Adapter, pMode);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode));
|
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +991,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayModeEx(
|
|||||||
D3DDISPLAYROTATION *pRotation)
|
D3DDISPLAYROTATION *pRotation)
|
||||||
{
|
{
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
get_fake_subscreen_display_mode_ex(Adapter, pMode);
|
||||||
|
if (pRotation != nullptr) {
|
||||||
|
*pRotation = D3DDISPLAYROTATION_IDENTITY;
|
||||||
|
}
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation));
|
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,6 +1030,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
|
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
}
|
}
|
||||||
|
if (gfdm_two_head_exclusive() && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"-forceressub is unavailable; SMALL must remain {}x{}",
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
DWORD orig_behavior_flags = BehaviorFlags;
|
DWORD orig_behavior_flags = BehaviorFlags;
|
||||||
size_t num_adapters = 1;
|
size_t num_adapters = 1;
|
||||||
@@ -989,6 +1064,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) {
|
if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) {
|
||||||
num_adapters = device_caps.NumberOfAdaptersInGroup;
|
num_adapters = device_caps.NumberOfAdaptersInGroup;
|
||||||
}
|
}
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
num_adapters = GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < num_adapters; i++) {
|
for (size_t i = 0; i < num_adapters; i++) {
|
||||||
@@ -1105,7 +1183,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
if (!GRAPHICS_WINDOWED && !gfdm_two_head_exclusive()
|
||||||
|
&& num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
||||||
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
|
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
|
||||||
pPresentationParameters[1].FullScreen_RefreshRateInHz,
|
pPresentationParameters[1].FullScreen_RefreshRateInHz,
|
||||||
GRAPHICS_FORCE_REFRESH_SUB.value());
|
GRAPHICS_FORCE_REFRESH_SUB.value());
|
||||||
@@ -1131,16 +1210,93 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value();
|
pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GfdmTwoHeadDeviceState gfdm_parameters(
|
||||||
|
pPresentationParameters,
|
||||||
|
pFullscreenDisplayMode);
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
if (!(BehaviorFlags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"CreateDeviceEx did not request an adapter-group device");
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
HRESULT select = graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
|
pPresentationParameters,
|
||||||
|
pFullscreenDisplayMode,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
gfdm_parameters.native_fullscreen_display_modes.data(),
|
||||||
|
&gfdm_parameters.logical_small_swapchain,
|
||||||
|
"CreateDeviceEx");
|
||||||
|
if (FAILED(select)) {
|
||||||
|
return select;
|
||||||
|
}
|
||||||
|
if (GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
||||||
|
gfdm_parameters.native_presentation_parameters[1]
|
||||||
|
.FullScreen_RefreshRateInHz =
|
||||||
|
GRAPHICS_FORCE_REFRESH_SUB.value();
|
||||||
|
if (pFullscreenDisplayMode != nullptr) {
|
||||||
|
gfdm_parameters.native_fullscreen_display_modes[1].RefreshRate =
|
||||||
|
GRAPHICS_FORCE_REFRESH_SUB.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HRESULT remap = graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
pFullscreenDisplayMode != nullptr
|
||||||
|
? gfdm_parameters.native_fullscreen_display_modes.data()
|
||||||
|
: nullptr,
|
||||||
|
"CreateDeviceEx");
|
||||||
|
if (FAILED(remap)) {
|
||||||
|
return remap;
|
||||||
|
}
|
||||||
|
if (pFullscreenDisplayMode != nullptr) {
|
||||||
|
graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
|
||||||
|
static_cast<IDirect3D9Ex *>(this->pReal),
|
||||||
|
Adapter,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
gfdm_parameters.native_fullscreen_display_modes.data(),
|
||||||
|
"CreateDeviceEx");
|
||||||
|
}
|
||||||
|
if (!graphics_gitadora_prepare_two_head_device_window(
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].hDeviceWindow,
|
||||||
|
this->pReal->GetAdapterMonitor(Adapter + 1),
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].BackBufferWidth,
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].BackBufferHeight))
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
HRESULT validation = validate_gfdm_two_head_exclusive(
|
||||||
|
this->pReal,
|
||||||
|
Adapter,
|
||||||
|
DeviceType,
|
||||||
|
BehaviorFlags,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data());
|
||||||
|
if (FAILED(validation)) {
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
gfdm_parameters.use_native_parameters();
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
HRESULT result = static_cast<IDirect3D9Ex *>(this->pReal)->CreateDeviceEx(
|
HRESULT result = static_cast<IDirect3D9Ex *>(this->pReal)->CreateDeviceEx(
|
||||||
Adapter,
|
Adapter,
|
||||||
DeviceType,
|
DeviceType,
|
||||||
hFocusWindow,
|
hFocusWindow,
|
||||||
BehaviorFlags,
|
BehaviorFlags,
|
||||||
pPresentationParameters,
|
gfdm_parameters.presentation_parameters,
|
||||||
pFullscreenDisplayMode,
|
gfdm_parameters.fullscreen_display_modes,
|
||||||
ppReturnedDeviceInterface);
|
ppReturnedDeviceInterface);
|
||||||
|
|
||||||
|
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
|
||||||
|
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
|
||||||
|
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
|
||||||
|
gfdm_parameters.presentation_parameters[1];
|
||||||
|
if (pFullscreenDisplayMode != nullptr) {
|
||||||
|
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
|
||||||
|
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
|
||||||
|
gfdm_parameters.fullscreen_display_modes[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check for error
|
// check for error
|
||||||
if (result != D3D_OK) {
|
if (result != D3D_OK) {
|
||||||
|
|
||||||
@@ -1151,17 +1307,28 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
|
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
||||||
|
hFocusWindow,
|
||||||
|
*ppReturnedDeviceInterface,
|
||||||
|
gfdm_parameters.logical_small_swapchain,
|
||||||
|
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
|
||||||
|
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
|
||||||
|
|
||||||
// initialize sub screen if the game requested a multi-head context
|
// initialize sub screen if the game requested a multi-head context
|
||||||
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
||||||
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
|
|
||||||
UINT i = 1;
|
UINT i = games::gitadora::is_arena_model()
|
||||||
if (games::gitadora::is_arena_model()) {
|
? gfdm_parameters.logical_small_swapchain
|
||||||
i = 2;
|
: 1;
|
||||||
}
|
D3DPRESENT_PARAMETERS *subscreen_parameters =
|
||||||
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]);
|
gfdm_two_head_exclusive()
|
||||||
|
? &gfdm_parameters.presentation_parameters[1]
|
||||||
|
: &pPresentationParameters[i];
|
||||||
|
graphics_d3d9_ldj_init_sub_screen(
|
||||||
|
*ppReturnedDeviceInterface,
|
||||||
|
subscreen_parameters,
|
||||||
|
i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1170,6 +1337,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *pLUID) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *pLUID) {
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (pLUID == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
pLUID->LowPart = static_cast<DWORD>(-static_cast<LONG>(Adapter));
|
||||||
|
pLUID->HighPart = -static_cast<LONG>(Adapter);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterLUID(Adapter, pLUID));
|
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterLUID(Adapter, pLUID));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,7 +1353,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *
|
|||||||
// The sub screen swap chain should be created if:
|
// The sub screen swap chain should be created if:
|
||||||
// - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist)
|
// - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist)
|
||||||
// - Running fullscreen with `NumberOfAdaptersInGroup < 2` (overridden `GetDeviceCaps` structure)
|
// - Running fullscreen with `NumberOfAdaptersInGroup < 2` (overridden `GetDeviceCaps` structure)
|
||||||
static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params) {
|
static void graphics_d3d9_ldj_init_sub_screen(
|
||||||
|
IDirect3DDevice9Ex *device,
|
||||||
|
D3DPRESENT_PARAMETERS *present_params,
|
||||||
|
UINT gfdm_small_swapchain)
|
||||||
|
{
|
||||||
D3DCAPS9 caps {};
|
D3DCAPS9 caps {};
|
||||||
HRESULT hr = device->GetDeviceCaps(&caps);
|
HRESULT hr = device->GetDeviceCaps(&caps);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
@@ -1212,10 +1391,9 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
int swapchain = 1;
|
const int swapchain = games::gitadora::is_arena_model()
|
||||||
if (games::gitadora::is_arena_model()) {
|
? static_cast<int>(gfdm_small_swapchain)
|
||||||
swapchain = 2;
|
: 1;
|
||||||
}
|
|
||||||
|
|
||||||
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
|
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
@@ -1250,6 +1428,12 @@ IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen() {
|
|||||||
return surface;
|
return surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void graphics_d3d9_notify_subscreen_present() {
|
||||||
|
if (SUBSCREEN_FORCE_REDRAW && !SUBSCREEN_FORCE_REDRAW_IN_PROGRESS) {
|
||||||
|
SUBSCREEN_PRESENTED_SINCE_LAST_MAIN.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
|
static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
|
||||||
// iidx/sdvx
|
// iidx/sdvx
|
||||||
int swapchain = 1;
|
int swapchain = 1;
|
||||||
@@ -1277,188 +1461,24 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
|
|||||||
//
|
//
|
||||||
// early versions of popn HC needs this as well, but not on by default as it can cause
|
// early versions of popn HC needs this as well, but not on by default as it can cause
|
||||||
// graphical glitches on some GPUs
|
// graphical glitches on some GPUs
|
||||||
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
|
//
|
||||||
|
// treat forced redraw as a fallback so it does not duplicate a successful game present.
|
||||||
|
|
||||||
|
const bool force_redraw = SUBSCREEN_FORCE_REDRAW &&
|
||||||
|
!SUBSCREEN_PRESENTED_SINCE_LAST_MAIN.exchange(false, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (GRAPHICS_WINDOWED || force_redraw) {
|
||||||
|
SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = true;
|
||||||
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
|
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
|
||||||
|
SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void save_capture(
|
|
||||||
int screen,
|
|
||||||
D3DFORMAT format,
|
|
||||||
UINT width,
|
|
||||||
UINT height,
|
|
||||||
IDirect3DSurface9 *surface) {
|
|
||||||
HRESULT hr;
|
|
||||||
|
|
||||||
// lock surface to be able to access the data
|
|
||||||
D3DLOCKED_RECT finished_copy {};
|
|
||||||
hr = surface->LockRect(&finished_copy, nullptr, 0);
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
|
|
||||||
graphics_capture_skip(screen);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy pixel data
|
|
||||||
size_t pitch = finished_copy.Pitch;
|
|
||||||
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
|
|
||||||
auto pixels = new uint8_t[width * height * 3];
|
|
||||||
for (size_t row = 0; row < height; row++) {
|
|
||||||
size_t offset_pixels = 0;
|
|
||||||
size_t offset_row = row * width * 3;
|
|
||||||
switch (format) {
|
|
||||||
case D3DFMT_R8G8B8: {
|
|
||||||
for (size_t offset = 0; offset < pitch; offset += 3) {
|
|
||||||
auto cell = data + row * pitch + offset;
|
|
||||||
auto pixel = &pixels[offset_row + offset_pixels];
|
|
||||||
pixel[0] = cell[0];
|
|
||||||
pixel[1] = cell[1];
|
|
||||||
pixel[2] = cell[2];
|
|
||||||
offset_pixels += 3;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case D3DFMT_X8R8G8B8:
|
|
||||||
case D3DFMT_A8R8G8B8: {
|
|
||||||
for (size_t offset = 0; offset < pitch; offset += 4) {
|
|
||||||
auto cell = data + row * pitch + offset;
|
|
||||||
auto pixel = &pixels[offset_row + offset_pixels];
|
|
||||||
pixel[0] = cell[2];
|
|
||||||
pixel[1] = cell[1];
|
|
||||||
pixel[2] = cell[0];
|
|
||||||
offset_pixels += 3;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case D3DFMT_X8B8G8R8:
|
|
||||||
case D3DFMT_A8B8G8R8: {
|
|
||||||
for (size_t offset = 0; offset < pitch; offset += 4) {
|
|
||||||
auto cell = data + row * pitch + offset;
|
|
||||||
auto pixel = &pixels[offset_row + offset_pixels];
|
|
||||||
pixel[0] = cell[0];
|
|
||||||
pixel[1] = cell[1];
|
|
||||||
pixel[2] = cell[2];
|
|
||||||
offset_pixels += 3;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
for (size_t offset = 0; offset < width; offset++) {
|
|
||||||
auto pixel = &pixels[offset_row + offset_pixels];
|
|
||||||
pixel[0] = 0;
|
|
||||||
pixel[1] = 0;
|
|
||||||
pixel[2] = 0;
|
|
||||||
offset_pixels += 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// unlock surface
|
|
||||||
hr = surface->UnlockRect();
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
|
|
||||||
graphics_capture_skip(screen);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// enqueue
|
|
||||||
graphics_capture_enqueue(screen, pixels, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void save_screenshot(const std::string &file_path, UINT height, IDirect3DSurface9 *surface) {
|
|
||||||
HRESULT hr;
|
|
||||||
|
|
||||||
D3DLOCKED_RECT finished_copy {};
|
|
||||||
hr = surface->LockRect(&finished_copy, nullptr, 0);
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set alpha channel to 255
|
|
||||||
{
|
|
||||||
auto pitch = finished_copy.Pitch;
|
|
||||||
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < height; i++) {
|
|
||||||
for (int j = 3; j < pitch; j += 4) {
|
|
||||||
data[i * pitch + j] = 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr = surface->UnlockRect();
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazy load function
|
|
||||||
static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr;
|
|
||||||
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
|
||||||
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>("D3DXSaveSurfaceToFileA");
|
|
||||||
|
|
||||||
// check if function was not found, likely because d3dx9 is not loaded
|
|
||||||
if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
|
||||||
ATTEMPTED_D3DX9_LOAD_LIBRARY = true;
|
|
||||||
|
|
||||||
for (size_t i = 43; i >= 24; i--) {
|
|
||||||
auto lib_name = fmt::format("d3dx9_{}.dll", i);
|
|
||||||
auto d3dx9 = libutils::try_library(lib_name);
|
|
||||||
|
|
||||||
// Check if library was not found
|
|
||||||
if (d3dx9 == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>(
|
|
||||||
d3dx9, "D3DXSaveSurfaceToFileA");
|
|
||||||
|
|
||||||
// Check if function was not found
|
|
||||||
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
|
||||||
FreeLibrary(d3dx9);
|
|
||||||
d3dx9 = nullptr;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
log_info("graphics::d3d9", "found surface save function in '{}'", lib_name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (D3DXSaveSurfaceToFileA_ptr != nullptr) {
|
|
||||||
|
|
||||||
// save to file
|
|
||||||
log_info("graphics::d3d9", "saving screenshot to {}", file_path);
|
|
||||||
auto hr = D3DXSaveSurfaceToFileA_ptr(file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr);
|
|
||||||
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
log_warning("graphics::d3d9", "Failed to save screenshot");
|
|
||||||
overlay::notifications::add(
|
|
||||||
overlay::notifications::Severity::Error,
|
|
||||||
"Screenshot failed to save");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save to clipboard
|
|
||||||
clipboard::copy_image(file_path);
|
|
||||||
|
|
||||||
overlay::notifications::add(
|
|
||||||
overlay::notifications::Severity::Success,
|
|
||||||
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
|
|
||||||
} else {
|
|
||||||
log_warning("graphics::d3d9", "Direct3D save helper function not available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void graphics_d3d9_on_present(
|
void graphics_d3d9_on_present(
|
||||||
HWND hFocusWindow,
|
HWND hFocusWindow,
|
||||||
IDirect3DDevice9 *device,
|
IDirect3DDevice9 *device,
|
||||||
IDirect3DDevice9 *wrapped_device) {
|
WrappedIDirect3DDevice9 *wrapped_device) {
|
||||||
|
|
||||||
// image resize / orientation swap. run here (the present path) rather than from `EndScene`,
|
// image resize / orientation swap. run here (the present path) rather than from `EndScene`,
|
||||||
// which may fire several times per frame on multi-pass / render-to-texture games. this is the
|
// which may fire several times per frame on multi-pass / render-to-texture games. this is the
|
||||||
@@ -1469,6 +1489,13 @@ void graphics_d3d9_on_present(
|
|||||||
SurfaceHook(device);
|
SurfaceHook(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
graphics_poll_screenshot_hotkey();
|
||||||
|
|
||||||
|
// before the overlay render so the screenshot excludes it
|
||||||
|
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||||
|
graphics_d3d9_process_screenshot(device, wrapped_device);
|
||||||
|
}
|
||||||
|
|
||||||
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
|
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
|
||||||
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
|
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
|
||||||
// `dx9osd.dll` for pfreepanic.
|
// `dx9osd.dll` for pfreepanic.
|
||||||
@@ -1488,6 +1515,15 @@ void graphics_d3d9_on_present(
|
|||||||
device->EndScene();
|
device->EndScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// after the overlay render so the screenshot includes toasts / menus
|
||||||
|
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||||
|
graphics_d3d9_process_screenshot(device, wrapped_device);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API capture always includes the overlay; it must run before the subscreen present
|
||||||
|
// below, which leaves the arena SMALL back buffer black
|
||||||
|
graphics_d3d9_process_capture(device, wrapped_device);
|
||||||
|
|
||||||
// for IIDX TDJ / SDVX UFC, handle subscreen
|
// for IIDX TDJ / SDVX UFC, handle subscreen
|
||||||
const bool is_vm = games::sdvx::is_valkyrie_model();
|
const bool is_vm = games::sdvx::is_valkyrie_model();
|
||||||
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
|
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
|
||||||
@@ -1501,136 +1537,6 @@ void graphics_d3d9_on_present(
|
|||||||
if (is_mfc) {
|
if (is_mfc) {
|
||||||
wintouchemu::update();
|
wintouchemu::update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// check screenshot key
|
|
||||||
static bool trigger_last = false;
|
|
||||||
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
|
||||||
if (buttons && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
|
|
||||||
GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::Screenshot)))
|
|
||||||
{
|
|
||||||
if (!trigger_last) {
|
|
||||||
graphics_screenshot_trigger();
|
|
||||||
}
|
|
||||||
trigger_last = true;
|
|
||||||
} else {
|
|
||||||
trigger_last = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// process pending screenshot
|
|
||||||
bool screenshot = false;
|
|
||||||
bool capture = false;
|
|
||||||
int capture_screen = 0;
|
|
||||||
if ((screenshot = graphics_screenshot_consume())
|
|
||||||
|| ((capture = graphics_capture_consume(&capture_screen)))) {
|
|
||||||
HRESULT hr = S_OK;
|
|
||||||
|
|
||||||
// TODO: verify capture_screen is a valid swapchain
|
|
||||||
|
|
||||||
// get back buffer
|
|
||||||
IDirect3DSurface9 *buffer = nullptr;
|
|
||||||
if (SUB_SWAP_CHAIN != nullptr && capture_screen & 1) {
|
|
||||||
hr = SUB_SWAP_CHAIN->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
|
||||||
} else {
|
|
||||||
hr = device->GetBackBuffer(capture_screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
|
||||||
}
|
|
||||||
if (FAILED(hr) || buffer == nullptr) {
|
|
||||||
log_warning("graphics::d3d9",
|
|
||||||
"failed to get back buffer, hr={}",
|
|
||||||
FMT_HRESULT(hr));
|
|
||||||
if (capture) {
|
|
||||||
graphics_capture_skip(capture_screen);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
D3DSURFACE_DESC desc {};
|
|
||||||
hr = buffer->GetDesc(&desc);
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
log_warning("graphics::d3d9",
|
|
||||||
"failed to acquire back buffer descriptor, hr={}",
|
|
||||||
FMT_HRESULT(hr));
|
|
||||||
buffer->Release();
|
|
||||||
if (capture) {
|
|
||||||
graphics_capture_skip(capture_screen);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: cache render targets
|
|
||||||
IDirect3DSurface9 *temp_surface = nullptr;
|
|
||||||
hr = device->CreateRenderTarget(
|
|
||||||
desc.Width, desc.Height, desc.Format, desc.MultiSampleType,
|
|
||||||
desc.MultiSampleQuality, TRUE, &temp_surface, nullptr);
|
|
||||||
if (FAILED(hr) || temp_surface == nullptr) {
|
|
||||||
log_warning("graphics::d3d9",
|
|
||||||
"failed to acquire temporary surface, hr={}",
|
|
||||||
FMT_HRESULT(hr));
|
|
||||||
buffer->Release();
|
|
||||||
if (capture) {
|
|
||||||
graphics_capture_skip(capture_screen);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE);
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
log_warning("graphics::d3d9",
|
|
||||||
"failed to copy back buffer contents, hr={}",
|
|
||||||
FMT_HRESULT(hr));
|
|
||||||
temp_surface->Release();
|
|
||||||
buffer->Release();
|
|
||||||
if (capture) {
|
|
||||||
graphics_capture_skip(capture_screen);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// release original back buffer reference
|
|
||||||
buffer->Release();
|
|
||||||
|
|
||||||
// function for storing the surface
|
|
||||||
auto surface_process = [=]() {
|
|
||||||
|
|
||||||
// capture
|
|
||||||
if (capture) {
|
|
||||||
save_capture(capture_screen, desc.Format, desc.Width, desc.Height, temp_surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
// screenshot
|
|
||||||
if (screenshot) {
|
|
||||||
|
|
||||||
// check where we can save it
|
|
||||||
auto file_path = graphics_screenshot_genpath();
|
|
||||||
if (!file_path.empty()) {
|
|
||||||
|
|
||||||
// write to file
|
|
||||||
save_screenshot(file_path, desc.Height, temp_surface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// release surface
|
|
||||||
temp_surface->Release();
|
|
||||||
};
|
|
||||||
|
|
||||||
// list of games that crash when running the screenshot processor on another thread
|
|
||||||
static const robin_hood::unordered_set<std::string> THREAD_BAN {
|
|
||||||
"JMA",
|
|
||||||
#ifndef SPICE64
|
|
||||||
"KFC",
|
|
||||||
#endif
|
|
||||||
"KMA",
|
|
||||||
"KLP",
|
|
||||||
"LMA",
|
|
||||||
};
|
|
||||||
|
|
||||||
// run the save operation on another thread for supported games
|
|
||||||
if (THREAD_BAN.contains(avs::game::MODEL)) {
|
|
||||||
surface_process();
|
|
||||||
} else {
|
|
||||||
static auto pool = ThreadPool(2);
|
|
||||||
pool.add(surface_process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
|
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
#include "d3d9_gfdm.h"
|
||||||
|
|
||||||
|
struct WrappedIDirect3DDevice9;
|
||||||
|
|
||||||
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
|
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
|
||||||
static const GUID IID_WrappedIDirect3D9 = {
|
static const GUID IID_WrappedIDirect3D9 = {
|
||||||
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
|
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
|
||||||
@@ -11,7 +15,9 @@ void graphics_d3d9_init();
|
|||||||
void graphics_d3d9_on_present(
|
void graphics_d3d9_on_present(
|
||||||
HWND hFocusWindow,
|
HWND hFocusWindow,
|
||||||
IDirect3DDevice9 *device,
|
IDirect3DDevice9 *device,
|
||||||
IDirect3DDevice9 *wrapped_device);
|
WrappedIDirect3DDevice9 *wrapped_device);
|
||||||
|
|
||||||
|
void graphics_d3d9_notify_subscreen_present();
|
||||||
|
|
||||||
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen();
|
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen();
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,18 @@
|
|||||||
|
|
||||||
#include "d3d9_backend.h"
|
#include "d3d9_backend.h"
|
||||||
#include "d3d9_live2d.h"
|
#include "d3d9_live2d.h"
|
||||||
|
#include "d3d9_readback.h"
|
||||||
#include "d3d9_texture.h"
|
#include "d3d9_texture.h"
|
||||||
|
|
||||||
#ifndef SPICE64
|
#ifndef SPICE64
|
||||||
#include "shaders/vertex_shader.h"
|
#include "shaders/vertex_shader.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// maps arena's cached additional swap chains (SMALL, LEFT, RIGHT) to screen numbers.
|
||||||
|
// MAIN is the implicit swap chain, is not in those slots, and is always screen 0.
|
||||||
|
// screen 1 is the subscreen for every other game, so SMALL takes that number here too.
|
||||||
|
static constexpr int GFDM_ARENA_SLOT_SCREENS[] { 1, 2, 3 };
|
||||||
|
|
||||||
#define CHECK_RESULT_FMT(x, fmt, ...) \
|
#define CHECK_RESULT_FMT(x, fmt, ...) \
|
||||||
HRESULT __ret = (x); \
|
HRESULT __ret = (x); \
|
||||||
if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \
|
if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \
|
||||||
@@ -41,7 +47,6 @@
|
|||||||
constexpr bool CUSTOM_RESET = false;
|
constexpr bool CUSTOM_RESET = false;
|
||||||
|
|
||||||
constexpr D3DFORMAT D3DFMT_DF24 = static_cast<D3DFORMAT>(MAKEFOURCC('D', 'F', '2', '4'));
|
constexpr D3DFORMAT D3DFMT_DF24 = static_cast<D3DFORMAT>(MAKEFOURCC('D', 'F', '2', '4'));
|
||||||
|
|
||||||
auto format_as(D3DPOOL f) {
|
auto format_as(D3DPOOL f) {
|
||||||
return fmt::underlying(f);
|
return fmt::underlying(f);
|
||||||
}
|
}
|
||||||
@@ -85,7 +90,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::QueryInterface(
|
|||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (riid == IID_WrappedIDirect3DDevice9 ||
|
if ((riid == IID_IUnknown && is_gfdm_two_head_exclusive()) ||
|
||||||
|
riid == IID_WrappedIDirect3DDevice9 ||
|
||||||
riid == IID_IDirect3DDevice9 ||
|
riid == IID_IDirect3DDevice9 ||
|
||||||
riid == IID_IDirect3DDevice9Ex)
|
riid == IID_IDirect3DDevice9Ex)
|
||||||
{
|
{
|
||||||
@@ -134,6 +140,10 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
|
|||||||
this->main_swapchain->Release();
|
this->main_swapchain->Release();
|
||||||
this->main_swapchain = nullptr;
|
this->main_swapchain = nullptr;
|
||||||
}
|
}
|
||||||
|
if (this->implicit_sub_swapchain) {
|
||||||
|
this->implicit_sub_swapchain->Release();
|
||||||
|
this->implicit_sub_swapchain = nullptr;
|
||||||
|
}
|
||||||
for (auto &sc : this->sub_swapchain) {
|
for (auto &sc : this->sub_swapchain) {
|
||||||
if (sc) {
|
if (sc) {
|
||||||
sc->Release();
|
sc->Release();
|
||||||
@@ -147,6 +157,8 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d3d9_readback::release_device_resources(this->pReal);
|
||||||
|
|
||||||
if (overlay::ENABLED) {
|
if (overlay::ENABLED) {
|
||||||
const std::lock_guard<std::mutex> lock(overlay::OVERLAY_MUTEX);
|
const std::lock_guard<std::mutex> lock(overlay::OVERLAY_MUTEX);
|
||||||
|
|
||||||
@@ -202,6 +214,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDirect3D(
|
|||||||
IDirect3D9 **ppD3D9)
|
IDirect3D9 **ppD3D9)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (is_gfdm_two_head_exclusive() && gfdm_parent_d3d != nullptr) {
|
||||||
|
if (ppD3D9 == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
gfdm_parent_d3d->AddRef();
|
||||||
|
*ppD3D9 = gfdm_parent_d3d;
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetDirect3D(ppD3D9));
|
CHECK_RESULT(pReal->GetDirect3D(ppD3D9));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +230,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDeviceCaps(
|
|||||||
D3DCAPS9 *pCaps)
|
D3DCAPS9 *pCaps)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
CHECK_RESULT(pReal->GetDeviceCaps(pCaps));
|
HRESULT result = pReal->GetDeviceCaps(pCaps);
|
||||||
|
if (SUCCEEDED(result) && is_gfdm_two_head_exclusive() && pCaps != nullptr) {
|
||||||
|
pCaps->NumberOfAdaptersInGroup = GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
pCaps->MasterAdapterOrdinal = 0;
|
||||||
|
pCaps->AdapterOrdinalInGroup = 0;
|
||||||
|
}
|
||||||
|
CHECK_RESULT(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
|
||||||
@@ -217,6 +244,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
|
|||||||
D3DDISPLAYMODE *pMode)
|
D3DDISPLAYMODE *pMode)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetDisplayMode(pMode);
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(pReal->GetDisplayMode(GFDM_NATIVE_SMALL_SWAPCHAIN, pMode));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetDisplayMode(iSwapChain, pMode));
|
CHECK_RESULT(pReal->GetDisplayMode(iSwapChain, pMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,22 +296,73 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
|
|||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
|
||||||
|
if (pPresentationParameters == nullptr || ppSwapChain == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
if (pPresentationParameters->BackBufferWidth == 800) {
|
||||||
|
return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain);
|
||||||
|
}
|
||||||
|
if (pPresentationParameters->BackBufferWidth != 1080) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: rejecting unexpected additional swap chain {}x{}",
|
||||||
|
pPresentationParameters->BackBufferWidth,
|
||||||
|
pPresentationParameters->BackBufferHeight);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t index = 3;
|
||||||
|
for (UINT logical_swapchain = 1;
|
||||||
|
logical_swapchain < GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
logical_swapchain++)
|
||||||
|
{
|
||||||
|
if (!is_gfdm_logical_side_swapchain(logical_swapchain)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const size_t candidate =
|
||||||
|
gfdm_hidden_side_swapchain_slot(logical_swapchain);
|
||||||
|
if (fake_sub_swapchain[candidate] == nullptr) {
|
||||||
|
index = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index == 3) {
|
||||||
|
return D3DERR_OUTOFVIDEOMEMORY;
|
||||||
|
}
|
||||||
|
fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(
|
||||||
|
this,
|
||||||
|
pPresentationParameters,
|
||||||
|
is_d3d9ex,
|
||||||
|
true);
|
||||||
|
fake_sub_swapchain[index]->AddRef();
|
||||||
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
bool create_swap_chain = false;
|
bool create_swap_chain = false;
|
||||||
bool create_fake_swap_chain = false;
|
bool create_fake_swap_chain = false;
|
||||||
|
bool arena_slot = false;
|
||||||
if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
|
|
||||||
} else if (games::gitadora::is_arena_model() &&
|
} else if (games::gitadora::is_arena_model() &&
|
||||||
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
|
(GRAPHICS_SCREENSHOT_SUBSCREENS ||
|
||||||
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS ||
|
||||||
|
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
|
||||||
|
|
||||||
if (pPresentationParameters->BackBufferWidth == 800) {
|
if (pPresentationParameters->BackBufferWidth == 800) {
|
||||||
// SMALL (subscreen)
|
// SMALL (subscreen)
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
|
arena_slot = true;
|
||||||
index = 0;
|
index = 0;
|
||||||
|
|
||||||
} else if (pPresentationParameters->BackBufferWidth == 1080) {
|
} else if (pPresentationParameters->BackBufferWidth == 1080) {
|
||||||
// LEFT/RIGHT
|
// LEFT/RIGHT
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
|
arena_slot = true;
|
||||||
index = 1;
|
index = 1;
|
||||||
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
|
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
|
||||||
index = 2;
|
index = 2;
|
||||||
@@ -285,6 +374,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the api lists screens from this registry, so arena heads need their logical numbers in it
|
||||||
|
if (arena_slot) {
|
||||||
|
graphics_screens_register(GFDM_ARENA_SLOT_SCREENS[index]);
|
||||||
|
}
|
||||||
|
|
||||||
if (create_fake_swap_chain) {
|
if (create_fake_swap_chain) {
|
||||||
if (!fake_sub_swapchain[index]) {
|
if (!fake_sub_swapchain[index]) {
|
||||||
log_info(
|
log_info(
|
||||||
@@ -335,6 +429,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
{
|
{
|
||||||
WRAP_VERBOSE_FMT("GetSwapChain({})", iSwapChain);
|
WRAP_VERBOSE_FMT("GetSwapChain({})", iSwapChain);
|
||||||
|
|
||||||
|
if (ppSwapChain == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
if (iSwapChain == 0) {
|
if (iSwapChain == 0) {
|
||||||
if (!main_swapchain) {
|
if (!main_swapchain) {
|
||||||
HRESULT ret = pReal->GetSwapChain(iSwapChain, ppSwapChain);
|
HRESULT ret = pReal->GetSwapChain(iSwapChain, ppSwapChain);
|
||||||
@@ -344,7 +442,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
main_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
|
main_swapchain = new WrappedIDirect3DSwapChain9(
|
||||||
|
this,
|
||||||
|
*ppSwapChain,
|
||||||
|
is_gfdm_two_head_exclusive()
|
||||||
|
? WrappedIDirect3DSwapChain9::NativeGroupHead::Main
|
||||||
|
: WrappedIDirect3DSwapChain9::NativeGroupHead::Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
main_swapchain->AddRef();
|
main_swapchain->AddRef();
|
||||||
@@ -354,6 +457,39 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
if (is_gfdm_logical_small_swapchain(iSwapChain)) {
|
||||||
|
if (!sub_swapchain[0]) {
|
||||||
|
IDirect3DSwapChain9 *real_swapchain = nullptr;
|
||||||
|
HRESULT ret = pReal->GetSwapChain(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
&real_swapchain);
|
||||||
|
if (FAILED(ret)) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
sub_swapchain[0] = new WrappedIDirect3DSwapChain9(
|
||||||
|
this,
|
||||||
|
real_swapchain,
|
||||||
|
WrappedIDirect3DSwapChain9::NativeGroupHead::Small);
|
||||||
|
sub_swapchain[0]->should_run_hooks = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_swapchain[0]->AddRef();
|
||||||
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain[0]);
|
||||||
|
graphics_screens_register(iSwapChain);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
if (is_gfdm_logical_side_swapchain(iSwapChain)) {
|
||||||
|
FakeIDirect3DSwapChain9 *swapchain =
|
||||||
|
ensure_gfdm_hidden_side_swapchain(iSwapChain);
|
||||||
|
swapchain->AddRef();
|
||||||
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(swapchain);
|
||||||
|
graphics_screens_register(iSwapChain);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
bool swap = false;
|
bool swap = false;
|
||||||
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
swap = true;
|
swap = true;
|
||||||
@@ -373,6 +509,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
fake_sub_swapchain[0]->AddRef();
|
fake_sub_swapchain[0]->AddRef();
|
||||||
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
|
||||||
|
|
||||||
|
graphics_screens_register(iSwapChain);
|
||||||
|
return D3D_OK;
|
||||||
|
} else if (SUBSCREEN_FORCE_REDRAW) {
|
||||||
|
// store implicit sub swap chain
|
||||||
|
if (!implicit_sub_swapchain) {
|
||||||
|
IDirect3DSwapChain9 *real_swapchain = nullptr;
|
||||||
|
HRESULT ret = pReal->GetSwapChain(iSwapChain, &real_swapchain);
|
||||||
|
if (FAILED(ret)) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit_sub_swapchain = new WrappedIDirect3DSwapChain9(this, real_swapchain);
|
||||||
|
implicit_sub_swapchain->should_run_hooks = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit_sub_swapchain->AddRef();
|
||||||
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(implicit_sub_swapchain);
|
||||||
|
|
||||||
graphics_screens_register(iSwapChain);
|
graphics_screens_register(iSwapChain);
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
@@ -390,6 +544,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
|
UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
return GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
UINT n = pReal->GetNumberOfSwapChains();
|
UINT n = pReal->GetNumberOfSwapChains();
|
||||||
|
|
||||||
if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
@@ -399,6 +557,64 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::get_screenshot_screens(std::vector<int> &screens) const {
|
||||||
|
if (games::gitadora::is_arena_model()) {
|
||||||
|
screens.push_back(0);
|
||||||
|
|
||||||
|
// every head the game renders into, whether or not it reaches a display
|
||||||
|
for (int slot = 0; slot < 3; slot++) {
|
||||||
|
if (sub_swapchain[slot] != nullptr || fake_sub_swapchain[slot] != nullptr) {
|
||||||
|
screens.push_back(GFDM_ARENA_SLOT_SCREENS[slot]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
graphics_screens_get(screens);
|
||||||
|
|
||||||
|
// the sub screen is only registered once the game asks for it by index
|
||||||
|
if (sub_swapchain[0] != nullptr &&
|
||||||
|
avs::game::is_model({"LDJ", "KFC", "M39"}) &&
|
||||||
|
std::find(screens.begin(), screens.end(), 1) == screens.end())
|
||||||
|
{
|
||||||
|
screens.push_back(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT WrappedIDirect3DDevice9::get_screenshot_swap_chain(
|
||||||
|
UINT iSwapChain,
|
||||||
|
IDirect3DSwapChain9 **ppSwapChain)
|
||||||
|
{
|
||||||
|
if (ppSwapChain == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the game numbers the two-head SMALL head itself; keep screen 1 meaning SMALL
|
||||||
|
if (games::gitadora::is_arena_model() && is_gfdm_two_head_exclusive() && iSwapChain == 1) {
|
||||||
|
return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (games::gitadora::is_arena_model()) {
|
||||||
|
for (int slot = 0; slot < 3; slot++) {
|
||||||
|
if (GFDM_ARENA_SLOT_SCREENS[slot] != (int) iSwapChain) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sub_swapchain[slot] != nullptr) {
|
||||||
|
sub_swapchain[slot]->AddRef();
|
||||||
|
*ppSwapChain = sub_swapchain[slot];
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
if (fake_sub_swapchain[slot] != nullptr) {
|
||||||
|
fake_sub_swapchain[slot]->AddRef();
|
||||||
|
*ppSwapChain = fake_sub_swapchain[slot];
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetSwapChain(iSwapChain, ppSwapChain);
|
||||||
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
||||||
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
||||||
{
|
{
|
||||||
@@ -453,6 +669,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
|
|||||||
IDirect3DSurface9 **ppBackBuffer)
|
IDirect3DSurface9 **ppBackBuffer)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetBackBuffer(
|
||||||
|
iBackBuffer,
|
||||||
|
Type,
|
||||||
|
ppBackBuffer);
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(pReal->GetBackBuffer(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
iBackBuffer,
|
||||||
|
Type,
|
||||||
|
ppBackBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetBackBuffer(iSwapChain, iBackBuffer, Type, ppBackBuffer));
|
CHECK_RESULT(pReal->GetBackBuffer(iSwapChain, iBackBuffer, Type, ppBackBuffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +695,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetRasterStatus(
|
|||||||
D3DRASTER_STATUS *pRasterStatus)
|
D3DRASTER_STATUS *pRasterStatus)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetRasterStatus(
|
||||||
|
pRasterStatus);
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(pReal->GetRasterStatus(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
pRasterStatus));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetRasterStatus(iSwapChain, pRasterStatus));
|
CHECK_RESULT(pReal->GetRasterStatus(iSwapChain, pRasterStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,6 +725,18 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetGammaRamp(
|
|||||||
const D3DGAMMARAMP *pRamp)
|
const D3DGAMMARAMP *pRamp)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
pReal->SetGammaRamp(GFDM_NATIVE_SMALL_SWAPCHAIN, Flags, pRamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pReal->SetGammaRamp(iSwapChain, Flags, pRamp);
|
pReal->SetGammaRamp(iSwapChain, Flags, pRamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +745,21 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGammaRamp(
|
|||||||
D3DGAMMARAMP *pRamp)
|
D3DGAMMARAMP *pRamp)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
if (pRamp != nullptr) {
|
||||||
|
*pRamp = {};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
pReal->GetGammaRamp(GFDM_NATIVE_SMALL_SWAPCHAIN, pRamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pReal->GetGammaRamp(iSwapChain, pRamp);
|
pReal->GetGammaRamp(iSwapChain, pRamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,6 +960,19 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetFrontBufferData(
|
|||||||
IDirect3DSurface9 *pDestSurface)
|
IDirect3DSurface9 *pDestSurface)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(pReal->GetFrontBufferData(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
pDestSurface));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetFrontBufferData(iSwapChain, pDestSurface));
|
CHECK_RESULT(pReal->GetFrontBufferData(iSwapChain, pDestSurface));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1049,7 +1337,36 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetViewport(
|
|||||||
D3DVIEWPORT9 *pViewport)
|
D3DVIEWPORT9 *pViewport)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
CHECK_RESULT(pReal->GetViewport(pViewport));
|
const auto result = pReal->GetViewport(pViewport);
|
||||||
|
|
||||||
|
// SDVX landscape mode
|
||||||
|
// without this, the game calculates the camera angle incorrectly and
|
||||||
|
// ends up with zoomed out view of the lanes
|
||||||
|
const auto landscape_width = GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||||
|
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT;
|
||||||
|
const auto landscape_height = GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||||
|
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH;
|
||||||
|
if (SUCCEEDED(result) &&
|
||||||
|
GRAPHICS_FS_ORIENTATION_SWAP &&
|
||||||
|
avs::game::is_model("KFC") &&
|
||||||
|
pViewport != nullptr &&
|
||||||
|
pViewport->Width == landscape_width &&
|
||||||
|
pViewport->Height == landscape_height) {
|
||||||
|
|
||||||
|
static std::once_flag log_once;
|
||||||
|
std::call_once(log_once, [pViewport]() {
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"SDVX landscape viewport fix: {}x{} => {}x{}",
|
||||||
|
pViewport->Width,
|
||||||
|
pViewport->Height,
|
||||||
|
pViewport->Height,
|
||||||
|
pViewport->Width);
|
||||||
|
});
|
||||||
|
std::swap(pViewport->Width, pViewport->Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetMaterial(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetMaterial(
|
||||||
@@ -1779,6 +2096,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::WaitForVBlank(
|
|||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(iSwapChain));
|
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(iSwapChain));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1882,6 +2211,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
|||||||
|
|
||||||
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::ResetEx");
|
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::ResetEx");
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
gfdm_disarm_present_mode_recovery();
|
||||||
|
if (pPresentationParameters == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
release_gfdm_hidden_side_swapchains();
|
||||||
|
}
|
||||||
|
|
||||||
if (GRAPHICS_WINDOWED) {
|
if (GRAPHICS_WINDOWED) {
|
||||||
if (pPresentationParameters) {
|
if (pPresentationParameters) {
|
||||||
pPresentationParameters->Windowed = true;
|
pPresentationParameters->Windowed = true;
|
||||||
@@ -1898,13 +2235,115 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GfdmTwoHeadDeviceState gfdm_parameters(
|
||||||
|
pPresentationParameters,
|
||||||
|
pFullscreenDisplayMode,
|
||||||
|
gfdm_logical_small_swapchain);
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
HRESULT result = graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
|
pPresentationParameters,
|
||||||
|
pFullscreenDisplayMode,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
pFullscreenDisplayMode != nullptr
|
||||||
|
? gfdm_parameters.native_fullscreen_display_modes.data()
|
||||||
|
: nullptr,
|
||||||
|
&gfdm_parameters.logical_small_swapchain,
|
||||||
|
"ResetEx");
|
||||||
|
if (FAILED(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
set_gfdm_logical_group_parameters(pPresentationParameters);
|
||||||
|
gfdm_parameters.use_native_parameters();
|
||||||
|
if (gfdm_parameters.native_presentation_parameters[0].Windowed
|
||||||
|
|| gfdm_parameters.native_presentation_parameters[1].Windowed)
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
||||||
|
gfdm_parameters.native_presentation_parameters[1]
|
||||||
|
.FullScreen_RefreshRateInHz =
|
||||||
|
GRAPHICS_FORCE_REFRESH_SUB.value();
|
||||||
|
if (gfdm_parameters.fullscreen_display_modes != nullptr) {
|
||||||
|
gfdm_parameters.fullscreen_display_modes[1].RefreshRate =
|
||||||
|
GRAPHICS_FORCE_REFRESH_SUB.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
gfdm_parameters.fullscreen_display_modes,
|
||||||
|
"ResetEx");
|
||||||
|
if (FAILED(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!graphics_gitadora_prepare_two_head_device_window(
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].hDeviceWindow,
|
||||||
|
nullptr,
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].BackBufferWidth,
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].BackBufferHeight))
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (gfdm_parameters.fullscreen_display_modes != nullptr) {
|
||||||
|
IDirect3D9 *raw_d3d = nullptr;
|
||||||
|
IDirect3D9Ex *raw_d3d_ex = nullptr;
|
||||||
|
D3DDEVICE_CREATION_PARAMETERS creation {};
|
||||||
|
HRESULT raw_result = pReal->GetDirect3D(&raw_d3d);
|
||||||
|
if (SUCCEEDED(raw_result) && raw_d3d != nullptr) {
|
||||||
|
raw_result = raw_d3d->QueryInterface(IID_PPV_ARGS(&raw_d3d_ex));
|
||||||
|
}
|
||||||
|
if (SUCCEEDED(raw_result)) {
|
||||||
|
raw_result = pReal->GetCreationParameters(&creation);
|
||||||
|
}
|
||||||
|
if (SUCCEEDED(raw_result) && raw_d3d_ex != nullptr) {
|
||||||
|
graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
|
||||||
|
raw_d3d_ex,
|
||||||
|
creation.AdapterOrdinal,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
gfdm_parameters.fullscreen_display_modes,
|
||||||
|
"ResetEx");
|
||||||
|
}
|
||||||
|
if (raw_d3d_ex != nullptr) {
|
||||||
|
raw_d3d_ex->Release();
|
||||||
|
}
|
||||||
|
if (raw_d3d != nullptr) {
|
||||||
|
raw_d3d->Release();
|
||||||
|
}
|
||||||
|
gfdm_parameters.recovery_parameters =
|
||||||
|
gfdm_parameters.native_presentation_parameters;
|
||||||
|
gfdm_parameters.recovery_modes =
|
||||||
|
gfdm_parameters.native_fullscreen_display_modes;
|
||||||
|
gfdm_parameters.recovery_candidate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reset overlay
|
// reset overlay
|
||||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
||||||
overlay::OVERLAY->reset_invalidate();
|
overlay::OVERLAY->reset_invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
|
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
|
||||||
pPresentationParameters, pFullscreenDisplayMode);
|
gfdm_parameters.presentation_parameters,
|
||||||
|
gfdm_parameters.fullscreen_display_modes);
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& SUCCEEDED(res)
|
||||||
|
&& gfdm_parameters.recovery_candidate)
|
||||||
|
{
|
||||||
|
gfdm_arm_present_mode_recovery(
|
||||||
|
gfdm_parameters.recovery_parameters.data(),
|
||||||
|
gfdm_parameters.recovery_modes.data());
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
|
||||||
|
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
|
||||||
|
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
|
||||||
|
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
|
||||||
|
gfdm_parameters.presentation_parameters[1];
|
||||||
|
if (pFullscreenDisplayMode != nullptr) {
|
||||||
|
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
|
||||||
|
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
|
||||||
|
gfdm_parameters.fullscreen_display_modes[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// recreate overlay
|
// recreate overlay
|
||||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
|
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
|
||||||
@@ -1922,6 +2361,22 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayModeEx(
|
|||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetDisplayModeEx(
|
||||||
|
pMode,
|
||||||
|
pRotation);
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
pMode,
|
||||||
|
pRotation));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
|
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
|
||||||
iSwapChain, pMode, pRotation));
|
iSwapChain, pMode, pRotation));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <initguid.h>
|
#include <initguid.h>
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
@@ -40,23 +43,54 @@ static const GUID IID_WrappedIDirect3DDevice9 = {
|
|||||||
void SurfaceHook(IDirect3DDevice9 *pReal);
|
void SurfaceHook(IDirect3DDevice9 *pReal);
|
||||||
|
|
||||||
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
||||||
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig)
|
explicit WrappedIDirect3DDevice9(
|
||||||
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) {
|
HWND hFocusWindow,
|
||||||
|
IDirect3DDevice9 *orig,
|
||||||
|
UINT gfdm_logical_small_swapchain = 2,
|
||||||
|
IDirect3D9 *gfdm_parent_d3d = nullptr,
|
||||||
|
const D3DPRESENT_PARAMETERS *gfdm_logical_group_parameters = nullptr)
|
||||||
|
: hFocusWindow(hFocusWindow),
|
||||||
|
pReal(orig),
|
||||||
|
is_d3d9ex(false),
|
||||||
|
gfdm_logical_small_swapchain(gfdm_logical_small_swapchain),
|
||||||
|
gfdm_parent_d3d(gfdm_parent_d3d) {
|
||||||
|
if (this->gfdm_parent_d3d != nullptr) {
|
||||||
|
this->gfdm_parent_d3d->AddRef();
|
||||||
|
}
|
||||||
IDirect3DDevice9Ex *device = nullptr;
|
IDirect3DDevice9Ex *device = nullptr;
|
||||||
|
|
||||||
// attempt to upgrade handle
|
// attempt to upgrade handle
|
||||||
if (SUCCEEDED(this->QueryInterface(IID_PPV_ARGS(&device))) && device != nullptr) {
|
if (SUCCEEDED(this->QueryInterface(IID_PPV_ARGS(&device))) && device != nullptr) {
|
||||||
device->Release();
|
device->Release();
|
||||||
}
|
}
|
||||||
|
set_gfdm_logical_group_parameters(gfdm_logical_group_parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9Ex *orig)
|
explicit WrappedIDirect3DDevice9(
|
||||||
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(true) {}
|
HWND hFocusWindow,
|
||||||
|
IDirect3DDevice9Ex *orig,
|
||||||
|
UINT gfdm_logical_small_swapchain = 2,
|
||||||
|
IDirect3D9 *gfdm_parent_d3d = nullptr,
|
||||||
|
const D3DPRESENT_PARAMETERS *gfdm_logical_group_parameters = nullptr)
|
||||||
|
: hFocusWindow(hFocusWindow),
|
||||||
|
pReal(orig),
|
||||||
|
is_d3d9ex(true),
|
||||||
|
gfdm_logical_small_swapchain(gfdm_logical_small_swapchain),
|
||||||
|
gfdm_parent_d3d(gfdm_parent_d3d) {
|
||||||
|
if (this->gfdm_parent_d3d != nullptr) {
|
||||||
|
this->gfdm_parent_d3d->AddRef();
|
||||||
|
}
|
||||||
|
set_gfdm_logical_group_parameters(gfdm_logical_group_parameters);
|
||||||
|
}
|
||||||
|
|
||||||
WrappedIDirect3DDevice9(const WrappedIDirect3DDevice9 &) = delete;
|
WrappedIDirect3DDevice9(const WrappedIDirect3DDevice9 &) = delete;
|
||||||
WrappedIDirect3DDevice9 &operator=(const WrappedIDirect3DDevice9 &) = delete;
|
WrappedIDirect3DDevice9 &operator=(const WrappedIDirect3DDevice9 &) = delete;
|
||||||
|
|
||||||
virtual ~WrappedIDirect3DDevice9() = default;
|
virtual ~WrappedIDirect3DDevice9() {
|
||||||
|
if (gfdm_parent_d3d != nullptr) {
|
||||||
|
gfdm_parent_d3d->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma region IUnknown
|
#pragma region IUnknown
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||||
@@ -201,6 +235,31 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
|
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
|
||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
|
// logical screens the game draws, and the swap chain each one lives on
|
||||||
|
void get_screenshot_screens(std::vector<int> &screens) const;
|
||||||
|
HRESULT get_screenshot_swap_chain(UINT iSwapChain, IDirect3DSwapChain9 **ppSwapChain);
|
||||||
|
|
||||||
|
bool is_gfdm_two_head_exclusive() const;
|
||||||
|
bool is_gfdm_logical_small_swapchain(UINT swapchain) const;
|
||||||
|
bool is_gfdm_logical_side_swapchain(UINT swapchain) const;
|
||||||
|
size_t gfdm_hidden_side_swapchain_slot(UINT swapchain) const;
|
||||||
|
void set_gfdm_logical_group_parameters(
|
||||||
|
const D3DPRESENT_PARAMETERS *presentation_parameters);
|
||||||
|
FakeIDirect3DSwapChain9 *ensure_gfdm_hidden_side_swapchain(UINT iSwapChain);
|
||||||
|
void release_gfdm_hidden_side_swapchains();
|
||||||
|
|
||||||
|
enum class GfdmPresentModeRecoveryResult {
|
||||||
|
NotAttempted,
|
||||||
|
ReadyToRetry,
|
||||||
|
Failed,
|
||||||
|
};
|
||||||
|
void gfdm_disarm_present_mode_recovery();
|
||||||
|
void gfdm_arm_present_mode_recovery(
|
||||||
|
const D3DPRESENT_PARAMETERS *native_presentation_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *native_fullscreen_display_modes);
|
||||||
|
GfdmPresentModeRecoveryResult gfdm_recover_present_mode_change(
|
||||||
|
HRESULT *failure_result);
|
||||||
|
|
||||||
HWND const hFocusWindow;
|
HWND const hFocusWindow;
|
||||||
IDirect3DDevice9 *pReal;
|
IDirect3DDevice9 *pReal;
|
||||||
bool is_d3d9ex = false;
|
bool is_d3d9ex = false;
|
||||||
@@ -208,8 +267,21 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
std::atomic_ulong refs = 1;
|
std::atomic_ulong refs = 1;
|
||||||
|
|
||||||
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
||||||
|
WrappedIDirect3DSwapChain9 *implicit_sub_swapchain = nullptr;
|
||||||
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
||||||
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
||||||
|
|
||||||
|
UINT gfdm_logical_small_swapchain = 2;
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
|
||||||
|
bool gfdm_logical_group_parameters_valid = false;
|
||||||
|
IDirect3D9 *gfdm_parent_d3d = nullptr;
|
||||||
|
|
||||||
|
std::mutex gfdm_recovery_mutex;
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
|
||||||
|
std::array<D3DDISPLAYMODEEX, 2> gfdm_recovery_modes {};
|
||||||
|
bool gfdm_recovery_armed = false;
|
||||||
|
std::atomic_bool gfdm_recovery_consumed {false};
|
||||||
|
const DWORD gfdm_device_creation_thread_id = GetCurrentThreadId();
|
||||||
|
|
||||||
IDirect3DVertexShader9 *vertex_shader = nullptr;
|
IDirect3DVertexShader9 *vertex_shader = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,7 +59,12 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::Present(const RECT *pSourceRe
|
|||||||
log_misc("graphics::d3d9", "FakeIDirect3DSwapChain9::Present");
|
log_misc("graphics::d3d9", "FakeIDirect3DSwapChain9::Present");
|
||||||
});
|
});
|
||||||
|
|
||||||
return D3DERR_INVALIDCALL;
|
if (!is_hidden) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
present_count.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
@@ -86,11 +91,32 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffe
|
|||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
return D3DERR_INVALIDCALL;
|
if (!is_hidden) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pRasterStatus == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pRasterStatus = {};
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayMode(D3DDISPLAYMODE *pMode) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayMode(D3DDISPLAYMODE *pMode) {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
return D3DERR_INVALIDCALL;
|
if (!is_hidden) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pMode->Width = present_params.BackBufferWidth;
|
||||||
|
pMode->Height = present_params.BackBufferHeight;
|
||||||
|
pMode->RefreshRate = present_params.FullScreen_RefreshRateInHz;
|
||||||
|
pMode->Format = present_params.BackBufferFormat;
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDevice(IDirect3DDevice9 **ppDevice) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDevice(IDirect3DDevice9 **ppDevice) {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
@@ -108,17 +134,46 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentParameters(
|
|||||||
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
return D3DERR_INVALIDCALL;
|
if (!is_hidden) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pPresentationParameters == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pPresentationParameters = present_params;
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDirect3DSwapChain9Ex
|
// IDirect3DSwapChain9Ex
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetLastPresentCount(UINT *pLastPresentCount) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetLastPresentCount(UINT *pLastPresentCount) {
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
return D3DERR_INVALIDCALL;
|
if (!is_hidden) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pLastPresentCount == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pLastPresentCount = present_count.load(std::memory_order_relaxed);
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentStats(D3DPRESENTSTATS *pPresentationStatistics) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentStats(D3DPRESENTSTATS *pPresentationStatistics) {
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
return D3DERR_INVALIDCALL;
|
if (!is_hidden) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pPresentationStatistics == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pPresentationStatistics = {};
|
||||||
|
pPresentationStatistics->PresentCount =
|
||||||
|
present_count.load(std::memory_order_relaxed);
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMODEEX *pMode,
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMODEEX *pMode,
|
||||||
D3DDISPLAYROTATION *pRotation)
|
D3DDISPLAYROTATION *pRotation)
|
||||||
@@ -126,5 +181,22 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMO
|
|||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
return D3DERR_INVALIDCALL;
|
if (!is_hidden) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pMode->Size = sizeof(*pMode);
|
||||||
|
pMode->Width = present_params.BackBufferWidth;
|
||||||
|
pMode->Height = present_params.BackBufferHeight;
|
||||||
|
pMode->RefreshRate = present_params.FullScreen_RefreshRateInHz;
|
||||||
|
pMode->Format = present_params.BackBufferFormat;
|
||||||
|
pMode->ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE;
|
||||||
|
if (pRotation != nullptr) {
|
||||||
|
*pRotation = D3DDISPLAYROTATION_IDENTITY;
|
||||||
|
}
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,19 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
||||||
FakeIDirect3DSwapChain9(IDirect3DDevice9 *pDev, D3DPRESENT_PARAMETERS *present_params, bool is_d3d9ex) :
|
FakeIDirect3DSwapChain9(
|
||||||
pDev(pDev), is_d3d9ex(is_d3d9ex)
|
IDirect3DDevice9 *pDev,
|
||||||
|
D3DPRESENT_PARAMETERS *present_params,
|
||||||
|
bool is_d3d9ex,
|
||||||
|
bool is_hidden = false) :
|
||||||
|
pDev(pDev), is_d3d9ex(is_d3d9ex), is_hidden(is_hidden)
|
||||||
{
|
{
|
||||||
// copy presentation parameters
|
// copy presentation parameters
|
||||||
memcpy(&this->present_params, present_params, sizeof(this->present_params));
|
memcpy(&this->present_params, present_params, sizeof(this->present_params));
|
||||||
@@ -78,8 +83,10 @@ struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
|||||||
|
|
||||||
IDirect3DDevice9 *const pDev;
|
IDirect3DDevice9 *const pDev;
|
||||||
bool is_d3d9ex;
|
bool is_d3d9ex;
|
||||||
|
bool is_hidden;
|
||||||
|
|
||||||
std::atomic<ULONG> ref_cnt = 1;
|
std::atomic<ULONG> ref_cnt = 1;
|
||||||
|
std::atomic<UINT> present_count = 0;
|
||||||
|
|
||||||
D3DPRESENT_PARAMETERS present_params {};
|
D3DPRESENT_PARAMETERS present_params {};
|
||||||
std::vector<IDirect3DSurface9 *> render_targets;
|
std::vector<IDirect3DSurface9 *> render_targets;
|
||||||
|
|||||||
@@ -0,0 +1,715 @@
|
|||||||
|
#include "d3d9_gfdm.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "games/gitadora/gitadora.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
#include "d3d9_device.h"
|
||||||
|
|
||||||
|
bool gfdm_two_head_exclusive() {
|
||||||
|
return games::gitadora::is_arena_model()
|
||||||
|
&& games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE
|
||||||
|
&& !GRAPHICS_WINDOWED;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
|
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
||||||
|
D3DPRESENT_PARAMETERS *native_presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *native_fullscreen_display_modes,
|
||||||
|
UINT *logical_small_swapchain,
|
||||||
|
const char *operation)
|
||||||
|
{
|
||||||
|
if (!gfdm_two_head_exclusive()
|
||||||
|
|| logical_presentation_parameters == nullptr
|
||||||
|
|| native_presentation_parameters == nullptr
|
||||||
|
|| logical_small_swapchain == nullptr)
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
UINT small_slot = GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
for (UINT i = 1; i < GFDM_LOGICAL_HEAD_COUNT; i++) {
|
||||||
|
const auto ¶ms = logical_presentation_parameters[i];
|
||||||
|
if (params.BackBufferWidth != GFDM_SMALL_WIDTH
|
||||||
|
|| params.BackBufferHeight != GFDM_SMALL_HEIGHT)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (small_slot != GFDM_LOGICAL_HEAD_COUNT) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} found multiple SMALL slots: {} and {}",
|
||||||
|
operation,
|
||||||
|
small_slot,
|
||||||
|
i);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
small_slot = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (small_slot == GFDM_LOGICAL_HEAD_COUNT) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} could not find an 800x1280 logical SMALL descriptor",
|
||||||
|
operation);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logical_fullscreen_display_modes != nullptr
|
||||||
|
&& (logical_fullscreen_display_modes[small_slot].Width != GFDM_SMALL_WIDTH
|
||||||
|
|| logical_fullscreen_display_modes[small_slot].Height != GFDM_SMALL_HEIGHT))
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} SMALL slot {} mode is {}x{}, expected {}x{}",
|
||||||
|
operation,
|
||||||
|
small_slot,
|
||||||
|
logical_fullscreen_display_modes[small_slot].Width,
|
||||||
|
logical_fullscreen_display_modes[small_slot].Height,
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
native_presentation_parameters[0] = logical_presentation_parameters[0];
|
||||||
|
native_presentation_parameters[1] = logical_presentation_parameters[small_slot];
|
||||||
|
if (logical_fullscreen_display_modes != nullptr
|
||||||
|
&& native_fullscreen_display_modes != nullptr)
|
||||||
|
{
|
||||||
|
native_fullscreen_display_modes[0] = logical_fullscreen_display_modes[0];
|
||||||
|
native_fullscreen_display_modes[1] = logical_fullscreen_display_modes[small_slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} selected logical SMALL slot {} -> native group head 1{}",
|
||||||
|
operation,
|
||||||
|
small_slot,
|
||||||
|
logical_presentation_parameters[small_slot].hDeviceWindow == GFDM_SUBSCREEN_WINDOW
|
||||||
|
? " (named SMALL host)"
|
||||||
|
: "");
|
||||||
|
*logical_small_swapchain = small_slot;
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_fake_subscreen_adapter(UINT adapter) {
|
||||||
|
// MAIN and SMALL remain native D3D heads; LEFT/RIGHT are virtual.
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
return adapter >= 2 && adapter < GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FAKE_SUBSCREEN_ADAPTER && adapter > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_fake_subscreen_display_mode(
|
||||||
|
UINT adapter,
|
||||||
|
D3DDISPLAYMODE *mode)
|
||||||
|
{
|
||||||
|
(void) adapter;
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
mode->Width = GFDM_SIDE_WIDTH;
|
||||||
|
mode->Height = GFDM_SIDE_HEIGHT;
|
||||||
|
} else {
|
||||||
|
mode->Width = 1280;
|
||||||
|
mode->Height = 800;
|
||||||
|
}
|
||||||
|
mode->RefreshRate = 60;
|
||||||
|
mode->Format = D3DFMT_X8R8G8B8;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_fake_subscreen_display_mode_ex(
|
||||||
|
UINT adapter,
|
||||||
|
D3DDISPLAYMODEEX *mode)
|
||||||
|
{
|
||||||
|
D3DDISPLAYMODE legacy_mode {};
|
||||||
|
get_fake_subscreen_display_mode(adapter, &legacy_mode);
|
||||||
|
|
||||||
|
mode->Size = sizeof(*mode);
|
||||||
|
mode->Width = legacy_mode.Width;
|
||||||
|
mode->Height = legacy_mode.Height;
|
||||||
|
mode->RefreshRate = legacy_mode.RefreshRate;
|
||||||
|
mode->Format = legacy_mode.Format;
|
||||||
|
mode->ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
const char *operation)
|
||||||
|
{
|
||||||
|
if (!gfdm_two_head_exclusive()) {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
if (presentation_parameters == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &secondary = presentation_parameters[1];
|
||||||
|
if (secondary.BackBufferWidth != GFDM_SMALL_WIDTH
|
||||||
|
|| secondary.BackBufferHeight != GFDM_SMALL_HEIGHT)
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} expected physical SMALL {}x{} at group slot 1, got {}x{}",
|
||||||
|
operation,
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT,
|
||||||
|
secondary.BackBufferWidth,
|
||||||
|
secondary.BackBufferHeight);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GFDM_SUBSCREEN_WINDOW == nullptr || !IsWindow(GFDM_SUBSCREEN_WINDOW)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} cannot use the named SMALL window as physical group head 1",
|
||||||
|
operation);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
secondary.hDeviceWindow = GFDM_SUBSCREEN_WINDOW;
|
||||||
|
|
||||||
|
if (fullscreen_display_modes != nullptr
|
||||||
|
&& (fullscreen_display_modes[1].Width != GFDM_SMALL_WIDTH
|
||||||
|
|| fullscreen_display_modes[1].Height != GFDM_SMALL_HEIGHT))
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} SMALL head mode is {}x{}, expected 800x1280",
|
||||||
|
operation,
|
||||||
|
fullscreen_display_modes[1].Width,
|
||||||
|
fullscreen_display_modes[1].Height);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
UINT master_adapter,
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
const char *operation)
|
||||||
|
{
|
||||||
|
if (d3d == nullptr
|
||||||
|
|| presentation_parameters == nullptr
|
||||||
|
|| fullscreen_display_modes == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UINT head = 0; head < 2; head++) {
|
||||||
|
const bool explicitly_forced =
|
||||||
|
(head == 0 && GRAPHICS_FORCE_REFRESH > 0)
|
||||||
|
|| (head == 1 && GRAPHICS_FORCE_REFRESH_SUB.has_value());
|
||||||
|
if (explicitly_forced) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DDISPLAYMODEEX desktop_mode {};
|
||||||
|
desktop_mode.Size = sizeof(desktop_mode);
|
||||||
|
D3DDISPLAYROTATION rotation = D3DDISPLAYROTATION_IDENTITY;
|
||||||
|
const HRESULT result = d3d->GetAdapterDisplayModeEx(
|
||||||
|
master_adapter + head,
|
||||||
|
&desktop_mode,
|
||||||
|
&rotation);
|
||||||
|
if (FAILED(result)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} could not query native head {} desktop refresh, hr={}",
|
||||||
|
operation,
|
||||||
|
head,
|
||||||
|
FMT_HRESULT(result));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ¶meters = presentation_parameters[head];
|
||||||
|
if (desktop_mode.Width != parameters.BackBufferWidth
|
||||||
|
|| desktop_mode.Height != parameters.BackBufferHeight
|
||||||
|
|| desktop_mode.Format != parameters.BackBufferFormat
|
||||||
|
|| desktop_mode.RefreshRate == 0)
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} head {} mode differs from the desktop",
|
||||||
|
operation,
|
||||||
|
head);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameters.FullScreen_RefreshRateInHz != desktop_mode.RefreshRate
|
||||||
|
|| fullscreen_display_modes[head].RefreshRate != desktop_mode.RefreshRate)
|
||||||
|
{
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} head {} refresh {} / {} -> {} (rotation {})",
|
||||||
|
operation,
|
||||||
|
head,
|
||||||
|
parameters.FullScreen_RefreshRateInHz,
|
||||||
|
fullscreen_display_modes[head].RefreshRate,
|
||||||
|
desktop_mode.RefreshRate,
|
||||||
|
static_cast<UINT>(rotation));
|
||||||
|
presentation_parameters[head].FullScreen_RefreshRateInHz =
|
||||||
|
desktop_mode.RefreshRate;
|
||||||
|
fullscreen_display_modes[head].RefreshRate =
|
||||||
|
desktop_mode.RefreshRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT validate_gfdm_two_head_exclusive(
|
||||||
|
IDirect3D9 *d3d,
|
||||||
|
UINT adapter,
|
||||||
|
D3DDEVTYPE device_type,
|
||||||
|
DWORD behavior_flags,
|
||||||
|
const D3DPRESENT_PARAMETERS *presentation_parameters)
|
||||||
|
{
|
||||||
|
if (!(behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode requires D3DCREATE_ADAPTERGROUP_DEVICE");
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
if (adapter != 0) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive mode currently requires master adapter 0, game requested {}",
|
||||||
|
adapter);
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
if (presentation_parameters == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
const UINT native_adapter_count = d3d->GetAdapterCount();
|
||||||
|
if (native_adapter_count != 2) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode requires two native D3D9 adapters; found {}",
|
||||||
|
native_adapter_count);
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DCAPS9 caps {};
|
||||||
|
const HRESULT caps_result = d3d->GetDeviceCaps(adapter, device_type, &caps);
|
||||||
|
if (FAILED(caps_result)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive mode could not query native adapter caps, hr={}",
|
||||||
|
FMT_HRESULT(caps_result));
|
||||||
|
return caps_result;
|
||||||
|
}
|
||||||
|
if (caps.MasterAdapterOrdinal != adapter
|
||||||
|
|| caps.AdapterOrdinalInGroup != 0
|
||||||
|
|| caps.NumberOfAdaptersInGroup != 2)
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"invalid adapter group: master={}, index={}, size={}",
|
||||||
|
caps.MasterAdapterOrdinal,
|
||||||
|
caps.AdapterOrdinalInGroup,
|
||||||
|
caps.NumberOfAdaptersInGroup);
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &main = presentation_parameters[0];
|
||||||
|
const auto &small_params = presentation_parameters[1];
|
||||||
|
if (main.Windowed || small_params.Windowed) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive mode requires both group heads to be fullscreen");
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (small_params.BackBufferWidth != GFDM_SMALL_WIDTH
|
||||||
|
|| small_params.BackBufferHeight != GFDM_SMALL_HEIGHT)
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"SMALL head must be {}x{}; got {}x{}",
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT,
|
||||||
|
small_params.BackBufferWidth,
|
||||||
|
small_params.BackBufferHeight);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (main.EnableAutoDepthStencil != small_params.EnableAutoDepthStencil) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode requires matching automatic depth-stencil settings");
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (main.EnableAutoDepthStencil
|
||||||
|
&& (main.AutoDepthStencilFormat != small_params.AutoDepthStencilFormat
|
||||||
|
|| main.BackBufferWidth != small_params.BackBufferWidth
|
||||||
|
|| main.BackBufferHeight != small_params.BackBufferHeight
|
||||||
|
|| main.BackBufferFormat != small_params.BackBufferFormat))
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode cannot use unequal sizes with automatic depth-stencil");
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool gfdm_find_alternate_small_mode(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
UINT adapter,
|
||||||
|
const D3DDISPLAYMODEEX &desired,
|
||||||
|
D3DDISPLAYMODEEX *alternate)
|
||||||
|
{
|
||||||
|
if (d3d == nullptr || alternate == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DDISPLAYMODEFILTER filter {};
|
||||||
|
filter.Size = sizeof(filter);
|
||||||
|
filter.Format = desired.Format;
|
||||||
|
filter.ScanLineOrdering = D3DSCANLINEORDERING_UNKNOWN;
|
||||||
|
const UINT count = d3d->GetAdapterModeCountEx(adapter, &filter);
|
||||||
|
const bool portrait = desired.Width < desired.Height;
|
||||||
|
|
||||||
|
for (UINT index = 0; index < count; index++) {
|
||||||
|
D3DDISPLAYMODEEX mode {};
|
||||||
|
mode.Size = sizeof(mode);
|
||||||
|
if (FAILED(d3d->EnumAdapterModesEx(adapter, &filter, index, &mode))
|
||||||
|
|| mode.Width != desired.Width
|
||||||
|
|| mode.Height != desired.Height
|
||||||
|
|| mode.Format != desired.Format
|
||||||
|
|| (mode.RefreshRate == desired.RefreshRate
|
||||||
|
&& mode.ScanLineOrdering == desired.ScanLineOrdering))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*alternate = mode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int preserve_orientation = 1; preserve_orientation >= 0; preserve_orientation--) {
|
||||||
|
for (int preserve_refresh = 1; preserve_refresh >= 0; preserve_refresh--) {
|
||||||
|
for (UINT index = 0; index < count; index++) {
|
||||||
|
D3DDISPLAYMODEEX mode {};
|
||||||
|
mode.Size = sizeof(mode);
|
||||||
|
if (FAILED(d3d->EnumAdapterModesEx(adapter, &filter, index, &mode))
|
||||||
|
|| (mode.Width == desired.Width && mode.Height == desired.Height)
|
||||||
|
|| mode.Format != desired.Format)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preserve_orientation && (mode.Width < mode.Height) != portrait) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preserve_refresh && mode.RefreshRate != desired.RefreshRate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*alternate = mode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool gfdm_wait_for_small_mode(
|
||||||
|
HWND window,
|
||||||
|
const D3DDISPLAYMODEEX &expected)
|
||||||
|
{
|
||||||
|
const HMONITOR monitor =
|
||||||
|
window != nullptr ? MonitorFromWindow(window, MONITOR_DEFAULTTONULL) : nullptr;
|
||||||
|
MONITORINFOEXA monitor_info {};
|
||||||
|
monitor_info.cbSize = sizeof(monitor_info);
|
||||||
|
if (monitor == nullptr || !GetMonitorInfoA(monitor, &monitor_info)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UINT poll = 0; poll < 50; poll++) {
|
||||||
|
DEVMODEA current {};
|
||||||
|
current.dmSize = sizeof(current);
|
||||||
|
if (EnumDisplaySettingsExA(
|
||||||
|
monitor_info.szDevice,
|
||||||
|
ENUM_CURRENT_SETTINGS,
|
||||||
|
¤t,
|
||||||
|
0)
|
||||||
|
&& current.dmPelsWidth == expected.Width
|
||||||
|
&& current.dmPelsHeight == expected.Height
|
||||||
|
&& (expected.RefreshRate == 0
|
||||||
|
|| current.dmDisplayFrequency == expected.RefreshRate))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Sleep(10);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
IDirect3DDevice9Ex *device,
|
||||||
|
UINT master_adapter,
|
||||||
|
const D3DPRESENT_PARAMETERS *desired_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *desired_modes)
|
||||||
|
{
|
||||||
|
if (d3d == nullptr
|
||||||
|
|| device == nullptr
|
||||||
|
|| desired_parameters == nullptr
|
||||||
|
|| desired_modes == nullptr)
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DDISPLAYMODEEX alternate_small {};
|
||||||
|
if (!gfdm_find_alternate_small_mode(
|
||||||
|
d3d,
|
||||||
|
master_adapter + 1,
|
||||||
|
desired_modes[1],
|
||||||
|
&alternate_small))
|
||||||
|
{
|
||||||
|
log_warning("graphics::d3d9", "no alternate SMALL mode is available for recovery");
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DPRESENT_PARAMETERS temporary_parameters[2] {
|
||||||
|
desired_parameters[0],
|
||||||
|
desired_parameters[1]};
|
||||||
|
D3DDISPLAYMODEEX temporary_modes[2] {
|
||||||
|
desired_modes[0],
|
||||||
|
desired_modes[1]};
|
||||||
|
temporary_parameters[1].BackBufferWidth = alternate_small.Width;
|
||||||
|
temporary_parameters[1].BackBufferHeight = alternate_small.Height;
|
||||||
|
temporary_parameters[1].BackBufferFormat = alternate_small.Format;
|
||||||
|
temporary_parameters[1].FullScreen_RefreshRateInHz = alternate_small.RefreshRate;
|
||||||
|
temporary_modes[1] = alternate_small;
|
||||||
|
|
||||||
|
HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes);
|
||||||
|
const bool temporary_settled =
|
||||||
|
temporary_result == D3D_OK
|
||||||
|
&& gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, alternate_small);
|
||||||
|
|
||||||
|
D3DPRESENT_PARAMETERS restore_parameters[2] {
|
||||||
|
desired_parameters[0],
|
||||||
|
desired_parameters[1]};
|
||||||
|
D3DDISPLAYMODEEX restore_modes[2] {
|
||||||
|
desired_modes[0],
|
||||||
|
desired_modes[1]};
|
||||||
|
HRESULT restore_result = device->ResetEx(restore_parameters, restore_modes);
|
||||||
|
const bool restore_settled =
|
||||||
|
restore_result == D3D_OK
|
||||||
|
&& gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, desired_modes[1]);
|
||||||
|
|
||||||
|
if (temporary_result != D3D_OK) {
|
||||||
|
return temporary_result;
|
||||||
|
}
|
||||||
|
if (restore_result != D3D_OK) {
|
||||||
|
return restore_result;
|
||||||
|
}
|
||||||
|
if (!temporary_settled || !restore_settled) {
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
return device->CheckDeviceState(desired_parameters[0].hDeviceWindow) == D3D_OK
|
||||||
|
? D3D_OK
|
||||||
|
: D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WrappedIDirect3DDevice9::is_gfdm_two_head_exclusive() const {
|
||||||
|
return ::gfdm_two_head_exclusive();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WrappedIDirect3DDevice9::is_gfdm_logical_small_swapchain(
|
||||||
|
UINT swapchain) const
|
||||||
|
{
|
||||||
|
return swapchain == gfdm_logical_small_swapchain;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WrappedIDirect3DDevice9::is_gfdm_logical_side_swapchain(
|
||||||
|
UINT swapchain) const
|
||||||
|
{
|
||||||
|
return swapchain > 0
|
||||||
|
&& swapchain < GFDM_LOGICAL_HEAD_COUNT
|
||||||
|
&& swapchain != gfdm_logical_small_swapchain;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t WrappedIDirect3DDevice9::gfdm_hidden_side_swapchain_slot(
|
||||||
|
UINT swapchain) const
|
||||||
|
{
|
||||||
|
assert(is_gfdm_logical_side_swapchain(swapchain));
|
||||||
|
return swapchain < gfdm_logical_small_swapchain
|
||||||
|
? swapchain
|
||||||
|
: swapchain - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
|
||||||
|
const D3DPRESENT_PARAMETERS *presentation_parameters)
|
||||||
|
{
|
||||||
|
if (presentation_parameters == nullptr) {
|
||||||
|
gfdm_logical_group_parameters_valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::copy_n(
|
||||||
|
presentation_parameters,
|
||||||
|
gfdm_logical_group_parameters.size(),
|
||||||
|
gfdm_logical_group_parameters.begin());
|
||||||
|
gfdm_logical_group_parameters_valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeIDirect3DSwapChain9 *
|
||||||
|
WrappedIDirect3DDevice9::ensure_gfdm_hidden_side_swapchain(UINT iSwapChain) {
|
||||||
|
assert(is_gfdm_logical_side_swapchain(iSwapChain));
|
||||||
|
|
||||||
|
const size_t index = gfdm_hidden_side_swapchain_slot(iSwapChain);
|
||||||
|
if (fake_sub_swapchain[index] == nullptr) {
|
||||||
|
D3DPRESENT_PARAMETERS params {};
|
||||||
|
if (gfdm_logical_group_parameters_valid) {
|
||||||
|
params = gfdm_logical_group_parameters[iSwapChain];
|
||||||
|
} else {
|
||||||
|
params.BackBufferWidth = 1080;
|
||||||
|
params.BackBufferHeight = 1920;
|
||||||
|
params.BackBufferFormat = D3DFMT_X8R8G8B8;
|
||||||
|
params.BackBufferCount = 1;
|
||||||
|
params.MultiSampleType = D3DMULTISAMPLE_NONE;
|
||||||
|
params.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
|
params.Windowed = FALSE;
|
||||||
|
params.FullScreen_RefreshRateInHz = 60;
|
||||||
|
params.EnableAutoDepthStencil = FALSE;
|
||||||
|
params.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||||
|
}
|
||||||
|
fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(
|
||||||
|
this,
|
||||||
|
¶ms,
|
||||||
|
is_d3d9ex,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
return fake_sub_swapchain[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::release_gfdm_hidden_side_swapchains() {
|
||||||
|
for (UINT i = 1; i < GFDM_LOGICAL_HEAD_COUNT; i++) {
|
||||||
|
if (!is_gfdm_logical_side_swapchain(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const size_t index = gfdm_hidden_side_swapchain_slot(i);
|
||||||
|
if (fake_sub_swapchain[index] != nullptr) {
|
||||||
|
fake_sub_swapchain[index]->Release();
|
||||||
|
fake_sub_swapchain[index] = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() {
|
||||||
|
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
||||||
|
gfdm_recovery_armed = false;
|
||||||
|
gfdm_recovery_consumed.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::gfdm_arm_present_mode_recovery(
|
||||||
|
const D3DPRESENT_PARAMETERS *native_presentation_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *native_fullscreen_display_modes)
|
||||||
|
{
|
||||||
|
if (native_presentation_parameters == nullptr
|
||||||
|
|| native_fullscreen_display_modes == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
||||||
|
gfdm_recovery_parameters[0] = native_presentation_parameters[0];
|
||||||
|
gfdm_recovery_parameters[1] = native_presentation_parameters[1];
|
||||||
|
gfdm_recovery_modes[0] = native_fullscreen_display_modes[0];
|
||||||
|
gfdm_recovery_modes[1] = native_fullscreen_display_modes[1];
|
||||||
|
if (gfdm_recovery_modes[0].Size == 0) {
|
||||||
|
gfdm_recovery_modes[0].Size = sizeof(D3DDISPLAYMODEEX);
|
||||||
|
}
|
||||||
|
if (gfdm_recovery_modes[1].Size == 0) {
|
||||||
|
gfdm_recovery_modes[1].Size = sizeof(D3DDISPLAYMODEEX);
|
||||||
|
}
|
||||||
|
gfdm_recovery_armed =
|
||||||
|
!gfdm_recovery_parameters[0].Windowed
|
||||||
|
&& !gfdm_recovery_parameters[1].Windowed
|
||||||
|
&& IsWindow(gfdm_recovery_parameters[1].hDeviceWindow);
|
||||||
|
gfdm_recovery_consumed.store(
|
||||||
|
!gfdm_recovery_armed,
|
||||||
|
std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult
|
||||||
|
WrappedIDirect3DDevice9::gfdm_recover_present_mode_change(
|
||||||
|
HRESULT *failure_result)
|
||||||
|
{
|
||||||
|
if (failure_result != nullptr) {
|
||||||
|
*failure_result = D3D_OK;
|
||||||
|
}
|
||||||
|
if (!is_gfdm_two_head_exclusive() || !is_d3d9ex
|
||||||
|
|| gfdm_recovery_consumed.exchange(
|
||||||
|
true,
|
||||||
|
std::memory_order_acq_rel))
|
||||||
|
{
|
||||||
|
return GfdmPresentModeRecoveryResult::NotAttempted;
|
||||||
|
}
|
||||||
|
if (GetCurrentThreadId() != gfdm_device_creation_thread_id) {
|
||||||
|
if (failure_result != nullptr) {
|
||||||
|
*failure_result = D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
return GfdmPresentModeRecoveryResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 2> parameters {};
|
||||||
|
std::array<D3DDISPLAYMODEEX, 2> modes {};
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
||||||
|
if (!gfdm_recovery_armed) {
|
||||||
|
return GfdmPresentModeRecoveryResult::NotAttempted;
|
||||||
|
}
|
||||||
|
parameters = gfdm_recovery_parameters;
|
||||||
|
modes = gfdm_recovery_modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDirect3D9 *raw_d3d = nullptr;
|
||||||
|
HRESULT result = pReal->GetDirect3D(&raw_d3d);
|
||||||
|
IDirect3D9Ex *raw_d3d_ex = nullptr;
|
||||||
|
if (SUCCEEDED(result)) {
|
||||||
|
result = raw_d3d != nullptr
|
||||||
|
? raw_d3d->QueryInterface(IID_PPV_ARGS(&raw_d3d_ex))
|
||||||
|
: E_FAIL;
|
||||||
|
}
|
||||||
|
D3DDEVICE_CREATION_PARAMETERS creation {};
|
||||||
|
if (SUCCEEDED(result)) {
|
||||||
|
result = pReal->GetCreationParameters(&creation);
|
||||||
|
}
|
||||||
|
if (FAILED(result) || raw_d3d_ex == nullptr) {
|
||||||
|
if (raw_d3d_ex != nullptr) {
|
||||||
|
raw_d3d_ex->Release();
|
||||||
|
}
|
||||||
|
if (raw_d3d != nullptr) {
|
||||||
|
raw_d3d->Release();
|
||||||
|
}
|
||||||
|
if (failure_result != nullptr) {
|
||||||
|
*failure_result = result;
|
||||||
|
}
|
||||||
|
return GfdmPresentModeRecoveryResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = graphics_d3d9_gfdm_recover_two_head_present_mode(
|
||||||
|
raw_d3d_ex,
|
||||||
|
static_cast<IDirect3DDevice9Ex *>(pReal),
|
||||||
|
creation.AdapterOrdinal,
|
||||||
|
parameters.data(),
|
||||||
|
modes.data());
|
||||||
|
raw_d3d_ex->Release();
|
||||||
|
raw_d3d->Release();
|
||||||
|
|
||||||
|
if (result == D3DERR_NOTAVAILABLE) {
|
||||||
|
return GfdmPresentModeRecoveryResult::NotAttempted;
|
||||||
|
}
|
||||||
|
if (FAILED(result)) {
|
||||||
|
if (failure_result != nullptr) {
|
||||||
|
*failure_result = result;
|
||||||
|
}
|
||||||
|
return GfdmPresentModeRecoveryResult::Failed;
|
||||||
|
}
|
||||||
|
return GfdmPresentModeRecoveryResult::ReadyToRetry;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
#include "games/gitadora/gitadora.h"
|
||||||
|
|
||||||
|
inline constexpr UINT GFDM_SIDE_WIDTH = 1080;
|
||||||
|
inline constexpr UINT GFDM_SIDE_HEIGHT = 1920;
|
||||||
|
inline constexpr UINT GFDM_SMALL_WIDTH = games::gitadora::ARENA_SUBSCREEN_WIDTH;
|
||||||
|
inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
|
||||||
|
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
|
||||||
|
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
|
||||||
|
|
||||||
|
struct GfdmTwoHeadDeviceState {
|
||||||
|
explicit GfdmTwoHeadDeviceState(
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
UINT logical_small_swapchain = 2)
|
||||||
|
: presentation_parameters(presentation_parameters),
|
||||||
|
fullscreen_display_modes(fullscreen_display_modes),
|
||||||
|
logical_small_swapchain(logical_small_swapchain) {}
|
||||||
|
|
||||||
|
void use_native_parameters() {
|
||||||
|
presentation_parameters = native_presentation_parameters.data();
|
||||||
|
fullscreen_display_modes = source_fullscreen_display_modes != nullptr
|
||||||
|
? native_fullscreen_display_modes.data()
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 2> native_presentation_parameters {};
|
||||||
|
std::array<D3DDISPLAYMODEEX, 2> native_fullscreen_display_modes {};
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 2> recovery_parameters {};
|
||||||
|
std::array<D3DDISPLAYMODEEX, 2> recovery_modes {};
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters;
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes;
|
||||||
|
D3DDISPLAYMODEEX *const source_fullscreen_display_modes = fullscreen_display_modes;
|
||||||
|
UINT logical_small_swapchain;
|
||||||
|
bool recovery_candidate = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool gfdm_two_head_exclusive();
|
||||||
|
bool is_fake_subscreen_adapter(UINT adapter);
|
||||||
|
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
|
||||||
|
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
|
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
||||||
|
D3DPRESENT_PARAMETERS *native_presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *native_fullscreen_display_modes,
|
||||||
|
UINT *logical_small_swapchain,
|
||||||
|
const char *operation);
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
const char *operation);
|
||||||
|
|
||||||
|
void graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
UINT master_adapter,
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
const char *operation);
|
||||||
|
|
||||||
|
HRESULT validate_gfdm_two_head_exclusive(
|
||||||
|
IDirect3D9 *d3d,
|
||||||
|
UINT adapter,
|
||||||
|
D3DDEVTYPE device_type,
|
||||||
|
DWORD behavior_flags,
|
||||||
|
const D3DPRESENT_PARAMETERS *presentation_parameters);
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
IDirect3DDevice9Ex *device,
|
||||||
|
UINT master_adapter,
|
||||||
|
const D3DPRESENT_PARAMETERS *desired_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *desired_modes);
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
#include "d3d9_readback.h"
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace d3d9_readback {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
|
||||||
|
IDirect3DSurface9 *surface = nullptr;
|
||||||
|
const HRESULT hr = device->CreateOffscreenPlainSurface(
|
||||||
|
desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, &surface, nullptr);
|
||||||
|
|
||||||
|
if (FAILED(hr) || surface == nullptr) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to create readback surface, hr={}",
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SurfacePtr(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t surface_bytes(const D3DSURFACE_DESC &desc) {
|
||||||
|
size_t bytes_per_pixel = 4;
|
||||||
|
switch (desc.Format) {
|
||||||
|
case D3DFMT_R5G6B5:
|
||||||
|
case D3DFMT_X1R5G5B5:
|
||||||
|
case D3DFMT_A1R5G5B5:
|
||||||
|
bytes_per_pixel = 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<size_t>(desc.Width) * desc.Height * bytes_per_pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// idle surfaces are kept between captures, bucketed by layout so that screens of
|
||||||
|
// differing resolution do not evict each other. a new device drops everything,
|
||||||
|
// since system memory surfaces outlive Reset but not the device itself
|
||||||
|
class ReadbackPool {
|
||||||
|
public:
|
||||||
|
SurfacePtr acquire(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
|
||||||
|
if (this->device != device) {
|
||||||
|
this->drop();
|
||||||
|
this->device = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *bucket = this->find(desc);
|
||||||
|
if (bucket && !bucket->idle.empty()) {
|
||||||
|
auto surface = std::move(bucket->idle.back());
|
||||||
|
bucket->idle.pop_back();
|
||||||
|
|
||||||
|
const size_t bytes = surface_bytes(desc);
|
||||||
|
this->idle_bytes = this->idle_bytes > bytes ? this->idle_bytes - bytes : 0;
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return create_readback_surface(device, desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void release(IDirect3DDevice9 *device, SurfacePtr surface) {
|
||||||
|
if (!surface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DSURFACE_DESC desc {};
|
||||||
|
if (FAILED(surface->GetDesc(&desc))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t bytes = surface_bytes(desc);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
if (this->device != device || this->idle_bytes + bytes > MAX_IDLE_BYTES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *bucket = this->find(desc);
|
||||||
|
if (bucket == nullptr) {
|
||||||
|
if (this->buckets.size() >= MAX_BUCKETS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->buckets.push_back(Bucket { desc.Width, desc.Height, desc.Format, {} });
|
||||||
|
bucket = &this->buckets.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucket->idle.size() < MAX_IDLE_PER_BUCKET) {
|
||||||
|
bucket->idle.push_back(std::move(surface));
|
||||||
|
this->idle_bytes += bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// every cached surface holds a reference on the device, so they have to go
|
||||||
|
// before it does or the device never reaches a zero reference count
|
||||||
|
void clear_device(IDirect3DDevice9 *device) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
if (this->device != device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->drop();
|
||||||
|
this->device = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Bucket {
|
||||||
|
UINT width;
|
||||||
|
UINT height;
|
||||||
|
D3DFORMAT format;
|
||||||
|
std::vector<SurfacePtr> idle;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr size_t MAX_BUCKETS = GRAPHICS_CAPTURE_SCREEN_NO;
|
||||||
|
|
||||||
|
// one returning surface plus one for the next capture; a full screen surface
|
||||||
|
// is several megabytes, so the cap matters
|
||||||
|
static constexpr size_t MAX_IDLE_PER_BUCKET = 2;
|
||||||
|
|
||||||
|
// a 4K surface is 33MB, so the per bucket count alone does not bound this
|
||||||
|
static constexpr size_t MAX_IDLE_BYTES = 64u * 1024 * 1024;
|
||||||
|
|
||||||
|
void drop() {
|
||||||
|
this->buckets.clear();
|
||||||
|
this->idle_bytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bucket *find(const D3DSURFACE_DESC &desc) {
|
||||||
|
for (auto &bucket : this->buckets) {
|
||||||
|
if (bucket.width == desc.Width
|
||||||
|
&& bucket.height == desc.Height
|
||||||
|
&& bucket.format == desc.Format) {
|
||||||
|
return &bucket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mutex mutex;
|
||||||
|
std::vector<Bucket> buckets;
|
||||||
|
IDirect3DDevice9 *device = nullptr;
|
||||||
|
size_t idle_bytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// deliberately never destroyed: releasing D3D surfaces during static destruction
|
||||||
|
// would run after d3d9 may already be unloaded
|
||||||
|
ReadbackPool &pool() {
|
||||||
|
static ReadbackPool *instance = new ReadbackPool();
|
||||||
|
return *instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void release_device_resources(IDirect3DDevice9 *device) {
|
||||||
|
pool().clear_device(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
BackbufferCopy::~BackbufferCopy() {
|
||||||
|
if (this->pooled && this->surface) {
|
||||||
|
pool().release(this->device, std::move(this->surface));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||||
|
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) {
|
||||||
|
|
||||||
|
IDirect3DSurface9 *buffer = nullptr;
|
||||||
|
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
||||||
|
if (FAILED(hr) || buffer == nullptr) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to get back buffer for screen {}, hr={}",
|
||||||
|
screen,
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DSURFACE_DESC desc {};
|
||||||
|
hr = buffer->GetDesc(&desc);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to acquire back buffer descriptor, hr={}",
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
buffer->Release();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRenderTargetData rejects multisampled sources. no supported game has been
|
||||||
|
// seen presenting one, so resolving is left unimplemented rather than untested
|
||||||
|
if (desc.MultiSampleType != D3DMULTISAMPLE_NONE) {
|
||||||
|
static std::once_flag warned;
|
||||||
|
std::call_once(warned, [&desc] {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"back buffer is multisampled ({}), screenshots and capture are unsupported",
|
||||||
|
static_cast<uint32_t>(desc.MultiSampleType));
|
||||||
|
});
|
||||||
|
buffer->Release();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto destination = pooled
|
||||||
|
? pool().acquire(device, desc)
|
||||||
|
: create_readback_surface(device, desc);
|
||||||
|
if (!destination) {
|
||||||
|
buffer->Release();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = device->GetRenderTargetData(buffer, destination.get());
|
||||||
|
buffer->Release();
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to copy back buffer contents, hr={}",
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
if (pooled) {
|
||||||
|
pool().release(device, std::move(destination));
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackbufferCopy copy;
|
||||||
|
copy.screen = screen;
|
||||||
|
copy.desc = desc;
|
||||||
|
copy.device = device;
|
||||||
|
copy.surface = std::move(destination);
|
||||||
|
copy.pooled = pooled;
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
namespace d3d9_readback {
|
||||||
|
|
||||||
|
struct SurfaceReleaser {
|
||||||
|
void operator()(IDirect3DSurface9 *surface) const {
|
||||||
|
surface->Release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using SurfacePtr = std::unique_ptr<IDirect3DSurface9, SurfaceReleaser>;
|
||||||
|
|
||||||
|
// system memory copy of a back buffer; locking it neither stalls the GPU nor reads over PCIe
|
||||||
|
struct BackbufferCopy {
|
||||||
|
int screen {};
|
||||||
|
D3DSURFACE_DESC desc {};
|
||||||
|
IDirect3DDevice9 *device = nullptr;
|
||||||
|
SurfacePtr surface;
|
||||||
|
bool pooled = false;
|
||||||
|
|
||||||
|
BackbufferCopy() = default;
|
||||||
|
BackbufferCopy(BackbufferCopy &&) noexcept = default;
|
||||||
|
BackbufferCopy &operator=(BackbufferCopy &&) noexcept = default;
|
||||||
|
BackbufferCopy(const BackbufferCopy &) = delete;
|
||||||
|
BackbufferCopy &operator=(const BackbufferCopy &) = delete;
|
||||||
|
~BackbufferCopy();
|
||||||
|
};
|
||||||
|
|
||||||
|
// pooled copies reuse surfaces across calls and return them once the copy is destroyed,
|
||||||
|
// so the caller must keep it alive for as long as the pixels are being read
|
||||||
|
std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
IDirect3DSwapChain9 *swap_chain,
|
||||||
|
int screen,
|
||||||
|
bool pooled);
|
||||||
|
|
||||||
|
// pooled surfaces hold references on the device; call this before releasing it
|
||||||
|
void release_device_resources(IDirect3DDevice9 *device);
|
||||||
|
}
|
||||||
@@ -0,0 +1,681 @@
|
|||||||
|
#include "d3d9_screenshot.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <future>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <external/robin_hood.h>
|
||||||
|
#include <external/fpng/fpng.h>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "misc/clipboard.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
#include "util/fileutils.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/threadpool.h"
|
||||||
|
|
||||||
|
#include "d3d9_device.h"
|
||||||
|
#include "d3d9_readback.h"
|
||||||
|
|
||||||
|
// genpath picks filenames by probing the disk, so the whole save has to be serialised:
|
||||||
|
// a name is only taken once its file exists, not when genpath hands it out
|
||||||
|
static std::mutex SCREENSHOT_SAVE_M;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class ImageRequestKind {
|
||||||
|
Screenshot,
|
||||||
|
Capture,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ImageRequest {
|
||||||
|
ImageRequestKind kind;
|
||||||
|
int screen;
|
||||||
|
};
|
||||||
|
|
||||||
|
// a screen already read out of its surface, so nothing here touches D3D. the bytes
|
||||||
|
// are still in the surface's format; converting them is left to the encode
|
||||||
|
struct PendingWrite {
|
||||||
|
int screen {};
|
||||||
|
D3DFORMAT format {};
|
||||||
|
UINT width {};
|
||||||
|
UINT height {};
|
||||||
|
size_t pitch {};
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
std::string path;
|
||||||
|
bool saved = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PendingCapture {
|
||||||
|
int screen {};
|
||||||
|
D3DFORMAT format {};
|
||||||
|
UINT width {};
|
||||||
|
UINT height {};
|
||||||
|
size_t pitch {};
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// packed 24bpp RGB, what both the png encoder and the api capture consume
|
||||||
|
constexpr size_t RGB_PIXEL_SIZE = 3;
|
||||||
|
|
||||||
|
// the formats surface_to_rgb knows how to convert; the two must stay in sync
|
||||||
|
static std::optional<size_t> surface_pixel_size(D3DFORMAT format) {
|
||||||
|
switch (format) {
|
||||||
|
// what back buffers are actually created as in practice
|
||||||
|
case D3DFMT_X8R8G8B8:
|
||||||
|
case D3DFMT_A8R8G8B8:
|
||||||
|
|
||||||
|
// a valid display format, but no supported game has been seen presenting one
|
||||||
|
case D3DFMT_A2R10G10B10:
|
||||||
|
return 4;
|
||||||
|
|
||||||
|
// valid display formats, but no supported game has been seen presenting one
|
||||||
|
case D3DFMT_R5G6B5:
|
||||||
|
case D3DFMT_X1R5G5B5:
|
||||||
|
case D3DFMT_A1R5G5B5:
|
||||||
|
return 2;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImageSize {
|
||||||
|
size_t row_size {};
|
||||||
|
size_t total_size {};
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::optional<ImageSize> compute_image_size(
|
||||||
|
UINT width,
|
||||||
|
UINT height,
|
||||||
|
size_t bytes_per_pixel) {
|
||||||
|
|
||||||
|
if (width == 0 || height == 0
|
||||||
|
|| width > std::numeric_limits<size_t>::max() / bytes_per_pixel) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t row_size = static_cast<size_t>(width) * bytes_per_pixel;
|
||||||
|
if (height > std::numeric_limits<size_t>::max() / row_size) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageSize { row_size, static_cast<size_t>(height) * row_size };
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool resize_pixels(std::vector<uint8_t> &pixels, size_t size) {
|
||||||
|
try {
|
||||||
|
pixels.resize(size);
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
log_warning("graphics::d3d9", "failed to allocate image buffer: {}", error.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the api capture stages a whole back buffer every frame, so the staging buffer
|
||||||
|
// is recycled rather than reallocated. returned buffers keep their size, which
|
||||||
|
// leaves the reuse free of a zero fill
|
||||||
|
class CaptureBuffers {
|
||||||
|
public:
|
||||||
|
std::vector<uint8_t> take() {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
if (this->idle.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto buffer = std::move(this->idle.back());
|
||||||
|
this->idle.pop_back();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void give(std::vector<uint8_t> buffer) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
if (this->idle.size() < MAX_IDLE) {
|
||||||
|
this->idle.push_back(std::move(buffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// one per save in flight plus one for the next capture; a full screen is
|
||||||
|
// several megabytes, so the cap matters
|
||||||
|
static constexpr size_t MAX_IDLE = 2;
|
||||||
|
|
||||||
|
std::mutex mutex;
|
||||||
|
std::vector<std::vector<uint8_t>> idle;
|
||||||
|
};
|
||||||
|
|
||||||
|
// deliberately never destroyed, so a save still running at process exit cannot
|
||||||
|
// hand a buffer back to a dead free list
|
||||||
|
CaptureBuffers &capture_buffers() {
|
||||||
|
static CaptureBuffers *instance = new CaptureBuffers();
|
||||||
|
return *instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodes get their own pool: the dispatch below already occupies a worker on its
|
||||||
|
// pool, so queueing onto that one and waiting could starve itself. never destroyed
|
||||||
|
// for the same reason as the buffers above
|
||||||
|
ThreadPool &encode_pool() {
|
||||||
|
static auto *instance = new ThreadPool(2);
|
||||||
|
return *instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize the supported D3D formats to packed 24bpp RGB. callers screen the
|
||||||
|
// format through surface_pixel_size first, so the black fill below is a fallback
|
||||||
|
void surface_to_rgb(
|
||||||
|
D3DFORMAT format,
|
||||||
|
UINT width,
|
||||||
|
UINT height,
|
||||||
|
const uint8_t *data,
|
||||||
|
size_t pitch,
|
||||||
|
uint8_t *pixels) {
|
||||||
|
|
||||||
|
for (size_t row = 0; row < height; row++) {
|
||||||
|
size_t offset_row = row * width * 3;
|
||||||
|
switch (format) {
|
||||||
|
case D3DFMT_X8R8G8B8:
|
||||||
|
case D3DFMT_A8R8G8B8: {
|
||||||
|
for (size_t column = 0; column < width; column++) {
|
||||||
|
auto cell = data + row * pitch + column * 4;
|
||||||
|
auto pixel = &pixels[offset_row + column * 3];
|
||||||
|
pixel[0] = cell[2];
|
||||||
|
pixel[1] = cell[1];
|
||||||
|
pixel[2] = cell[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// the 5 and 6 bit channels are widened by bit replication so that
|
||||||
|
// full scale stays full scale
|
||||||
|
case D3DFMT_R5G6B5: {
|
||||||
|
auto cells = reinterpret_cast<const uint16_t *>(data + row * pitch);
|
||||||
|
for (size_t column = 0; column < width; column++) {
|
||||||
|
const uint16_t cell = cells[column];
|
||||||
|
const uint8_t red = (cell >> 11) & 0x1F;
|
||||||
|
const uint8_t green = (cell >> 5) & 0x3F;
|
||||||
|
const uint8_t blue = cell & 0x1F;
|
||||||
|
auto pixel = &pixels[offset_row + column * 3];
|
||||||
|
pixel[0] = (red << 3) | (red >> 2);
|
||||||
|
pixel[1] = (green << 2) | (green >> 4);
|
||||||
|
pixel[2] = (blue << 3) | (blue >> 2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case D3DFMT_X1R5G5B5:
|
||||||
|
case D3DFMT_A1R5G5B5: {
|
||||||
|
auto cells = reinterpret_cast<const uint16_t *>(data + row * pitch);
|
||||||
|
for (size_t column = 0; column < width; column++) {
|
||||||
|
const uint16_t cell = cells[column];
|
||||||
|
const uint8_t red = (cell >> 10) & 0x1F;
|
||||||
|
const uint8_t green = (cell >> 5) & 0x1F;
|
||||||
|
const uint8_t blue = cell & 0x1F;
|
||||||
|
auto pixel = &pixels[offset_row + column * 3];
|
||||||
|
pixel[0] = (red << 3) | (red >> 2);
|
||||||
|
pixel[1] = (green << 3) | (green >> 2);
|
||||||
|
pixel[2] = (blue << 3) | (blue >> 2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case D3DFMT_A2R10G10B10: {
|
||||||
|
auto cells = reinterpret_cast<const uint32_t *>(data + row * pitch);
|
||||||
|
for (size_t column = 0; column < width; column++) {
|
||||||
|
const uint32_t cell = cells[column];
|
||||||
|
auto pixel = &pixels[offset_row + column * 3];
|
||||||
|
pixel[0] = static_cast<uint8_t>((cell >> 22) & 0xFF);
|
||||||
|
pixel[1] = static_cast<uint8_t>((cell >> 12) & 0xFF);
|
||||||
|
pixel[2] = static_cast<uint8_t>((cell >> 2) & 0xFF);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
for (size_t column = 0; column < width; column++) {
|
||||||
|
auto pixel = &pixels[offset_row + column * 3];
|
||||||
|
pixel[0] = 0;
|
||||||
|
pixel[1] = 0;
|
||||||
|
pixel[2] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
using d3d9_readback::BackbufferCopy;
|
||||||
|
|
||||||
|
static void save_capture(PendingCapture capture) {
|
||||||
|
const auto size = compute_image_size(capture.width, capture.height, RGB_PIXEL_SIZE);
|
||||||
|
if (!size.has_value()) {
|
||||||
|
capture_buffers().give(std::move(capture.data));
|
||||||
|
graphics_capture_skip(capture.screen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pixels = std::unique_ptr<uint8_t[]>(new (std::nothrow) uint8_t[size->total_size]);
|
||||||
|
if (!pixels) {
|
||||||
|
log_warning("graphics::d3d9", "failed to allocate capture image buffer");
|
||||||
|
capture_buffers().give(std::move(capture.data));
|
||||||
|
graphics_capture_skip(capture.screen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a format we cannot read still has to produce a frame, or api clients stall
|
||||||
|
if (capture.data.empty()) {
|
||||||
|
std::memset(pixels.get(), 0, size->total_size);
|
||||||
|
} else {
|
||||||
|
surface_to_rgb(
|
||||||
|
capture.format,
|
||||||
|
capture.width,
|
||||||
|
capture.height,
|
||||||
|
capture.data.data(),
|
||||||
|
capture.pitch,
|
||||||
|
pixels.get());
|
||||||
|
|
||||||
|
capture_buffers().give(std::move(capture.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
graphics_capture_enqueue(capture.screen, pixels.release(), capture.width, capture.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SurfaceRead {
|
||||||
|
Ok,
|
||||||
|
Unsupported,
|
||||||
|
Failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
// copying the surface touches D3D, so it stays on the caller's thread. the bytes come
|
||||||
|
// out in the surface's own format; converting them is plain memory work for later
|
||||||
|
static SurfaceRead read_surface_raw(
|
||||||
|
const BackbufferCopy ©,
|
||||||
|
size_t &row_size,
|
||||||
|
std::vector<uint8_t> &out) {
|
||||||
|
|
||||||
|
const auto bytes_per_pixel = surface_pixel_size(copy.desc.Format);
|
||||||
|
if (!bytes_per_pixel.has_value()) {
|
||||||
|
static std::once_flag warned;
|
||||||
|
std::call_once(warned, [©] {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"unsupported surface format {}",
|
||||||
|
static_cast<uint32_t>(copy.desc.Format));
|
||||||
|
});
|
||||||
|
return SurfaceRead::Unsupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto size = compute_image_size(copy.desc.Width, copy.desc.Height, *bytes_per_pixel);
|
||||||
|
if (!size.has_value() || !resize_pixels(out, size->total_size)) {
|
||||||
|
return SurfaceRead::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DLOCKED_RECT locked {};
|
||||||
|
HRESULT hr = copy.surface->LockRect(&locked, nullptr, D3DLOCK_READONLY);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("graphics::d3d9", "failed to lock capture surface, hr={}", FMT_HRESULT(hr));
|
||||||
|
return SurfaceRead::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locked.Pitch < 0 || static_cast<size_t>(locked.Pitch) < size->row_size) {
|
||||||
|
log_warning("graphics::d3d9", "capture surface has invalid pitch {}", locked.Pitch);
|
||||||
|
copy.surface->UnlockRect();
|
||||||
|
return SurfaceRead::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = reinterpret_cast<const uint8_t *>(locked.pBits);
|
||||||
|
for (size_t row = 0; row < copy.desc.Height; row++) {
|
||||||
|
std::memcpy(
|
||||||
|
out.data() + row * size->row_size,
|
||||||
|
data + row * locked.Pitch,
|
||||||
|
size->row_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = copy.surface->UnlockRect();
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("graphics::d3d9", "failed to unlock capture surface, hr={}", FMT_HRESULT(hr));
|
||||||
|
return SurfaceRead::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
row_size = size->row_size;
|
||||||
|
return SurfaceRead::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool read_capture_surface(
|
||||||
|
const BackbufferCopy ©,
|
||||||
|
PendingCapture &capture) {
|
||||||
|
|
||||||
|
capture.screen = copy.screen;
|
||||||
|
capture.format = copy.desc.Format;
|
||||||
|
capture.width = copy.desc.Width;
|
||||||
|
capture.height = copy.desc.Height;
|
||||||
|
|
||||||
|
capture.data = capture_buffers().take();
|
||||||
|
const auto result = read_surface_raw(copy, capture.pitch, capture.data);
|
||||||
|
if (result == SurfaceRead::Ok) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_buffers().give(std::move(capture.data));
|
||||||
|
capture.data.clear();
|
||||||
|
|
||||||
|
// a format we cannot read is reported as a black frame rather than nothing,
|
||||||
|
// so a client polling the api keeps getting responses
|
||||||
|
return result == SurfaceRead::Unsupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool write_screenshot_png(
|
||||||
|
const std::string &file_path,
|
||||||
|
UINT width,
|
||||||
|
UINT height,
|
||||||
|
const std::vector<uint8_t> &pixels) {
|
||||||
|
|
||||||
|
// a no-op while FPNG_NO_SSE is set, but fpng requires it before any encode
|
||||||
|
static std::once_flag fpng_ready;
|
||||||
|
std::call_once(fpng_ready, [] { fpng::fpng_init(); });
|
||||||
|
|
||||||
|
log_info("graphics::d3d9", "saving screenshot to {}", file_path);
|
||||||
|
|
||||||
|
if (!fpng::fpng_encode_image_to_file(
|
||||||
|
file_path.c_str(),
|
||||||
|
pixels.data(),
|
||||||
|
static_cast<uint32_t>(width),
|
||||||
|
static_cast<uint32_t>(height),
|
||||||
|
3)) {
|
||||||
|
log_warning("graphics::d3d9", "failed to write screenshot png");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// screen 0 keeps the plain name so existing tooling and the clipboard copy are unaffected
|
||||||
|
static std::string screenshot_path_for_screen(const std::string &primary_path, int screen) {
|
||||||
|
if (screen == 0) {
|
||||||
|
return primary_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path path(primary_path);
|
||||||
|
return (path.parent_path() /
|
||||||
|
fmt::format("{}_{}{}", path.stem().string(), screen, path.extension().string()))
|
||||||
|
.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// games that crash or hang when the screenshot processor runs on another thread.
|
||||||
|
// D3DCREATE_MULTITHREADED is not a predictor of this; MDX omits it and threads fine
|
||||||
|
static bool image_processing_must_be_inline() {
|
||||||
|
static const robin_hood::unordered_set<std::string> THREAD_BAN {
|
||||||
|
"JMA",
|
||||||
|
#ifndef SPICE64
|
||||||
|
// KFC only crashes under threaded processing in 32-bit builds
|
||||||
|
"KFC",
|
||||||
|
#endif
|
||||||
|
"KMA",
|
||||||
|
"KLP",
|
||||||
|
"LMA",
|
||||||
|
};
|
||||||
|
|
||||||
|
return THREAD_BAN.contains(avs::game::MODEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dispatch_capture_save(PendingCapture capture) {
|
||||||
|
auto capture_process = [capture = std::move(capture)]() mutable {
|
||||||
|
// an escape from here would cross a thread boundary and terminate
|
||||||
|
try {
|
||||||
|
save_capture(std::move(capture));
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
log_warning("graphics::d3d9", "capture save failed: {}", error.what());
|
||||||
|
} catch (...) {
|
||||||
|
log_warning("graphics::d3d9", "capture save failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (image_processing_must_be_inline()) {
|
||||||
|
capture_process();
|
||||||
|
} else {
|
||||||
|
static auto pool = ThreadPool(2);
|
||||||
|
pool.add(std::move(capture_process));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// by this point the pixels are plain memory, so none of this needs the device
|
||||||
|
static void dispatch_screenshot_save(std::vector<PendingWrite> writes, size_t screen_count) {
|
||||||
|
auto screenshot_process = [writes = std::move(writes), screen_count]() mutable {
|
||||||
|
std::lock_guard<std::mutex> lock(SCREENSHOT_SAVE_M);
|
||||||
|
|
||||||
|
std::vector<int> screens;
|
||||||
|
screens.reserve(writes.size());
|
||||||
|
for (const auto &write : writes) {
|
||||||
|
screens.push_back(write.screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto base_path = graphics_screenshot_genpath(screens);
|
||||||
|
if (base_path.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &write : writes) {
|
||||||
|
write.path = screenshot_path_for_screen(base_path, write.screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// screens missing from writes either failed to be acquired or failed to read
|
||||||
|
size_t failed = screen_count - writes.size();
|
||||||
|
|
||||||
|
// a throw here would otherwise reach a thread boundary and terminate
|
||||||
|
auto encode_one = [](PendingWrite &write) {
|
||||||
|
try {
|
||||||
|
const auto rgb = compute_image_size(write.width, write.height, RGB_PIXEL_SIZE);
|
||||||
|
std::vector<uint8_t> pixels;
|
||||||
|
if (!rgb.has_value() || !resize_pixels(pixels, rgb->total_size)) {
|
||||||
|
write.saved = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
surface_to_rgb(
|
||||||
|
write.format,
|
||||||
|
write.width,
|
||||||
|
write.height,
|
||||||
|
write.data.data(),
|
||||||
|
write.pitch,
|
||||||
|
pixels.data());
|
||||||
|
|
||||||
|
// the encode below is the long part; the raw copy is dead by now
|
||||||
|
write.data.clear();
|
||||||
|
write.data.shrink_to_fit();
|
||||||
|
|
||||||
|
write.saved = write_screenshot_png(
|
||||||
|
write.path, write.width, write.height, pixels);
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"screenshot encode failed for {}: {}", write.path, error.what());
|
||||||
|
write.saved = false;
|
||||||
|
} catch (...) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"screenshot encode failed for {}", write.path);
|
||||||
|
write.saved = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
// sized up front and assigned by index: storing a future must not be able
|
||||||
|
// to throw once its task is queued, or the screen would encode twice
|
||||||
|
std::vector<std::future<void>> pending(writes.empty() ? 0 : writes.size() - 1);
|
||||||
|
for (size_t i = 1; i < writes.size(); i++) {
|
||||||
|
try {
|
||||||
|
pending[i - 1] = encode_pool().add([&writes, &encode_one, i] {
|
||||||
|
encode_one(writes[i]);
|
||||||
|
});
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
// nothing to queue onto; encoding it here still makes progress
|
||||||
|
encode_one(writes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!writes.empty()) {
|
||||||
|
encode_one(writes.front());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &task : pending) {
|
||||||
|
if (task.valid()) {
|
||||||
|
task.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string primary_path;
|
||||||
|
std::string notify_path;
|
||||||
|
for (const auto &write : writes) {
|
||||||
|
if (!write.saved) {
|
||||||
|
failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (notify_path.empty()) {
|
||||||
|
notify_path = write.path;
|
||||||
|
}
|
||||||
|
if (write.screen == 0) {
|
||||||
|
primary_path = write.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only the primary screen goes to the clipboard, but any saved file is a success
|
||||||
|
if (!primary_path.empty()) {
|
||||||
|
clipboard::copy_image(primary_path);
|
||||||
|
}
|
||||||
|
if (!notify_path.empty()) {
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
fmt::format("Screenshot saved: {}", fileutils::basename(notify_path)));
|
||||||
|
} else {
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
"Screenshot failed to save");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
log_warning("graphics::d3d9", "{} screenshot screen(s) missing", failed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// genpath and the path building below allocate, so an escape from here would
|
||||||
|
// cross a thread boundary and terminate
|
||||||
|
auto guarded = [process = std::move(screenshot_process)]() mutable {
|
||||||
|
try {
|
||||||
|
process();
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
log_warning("graphics::d3d9", "screenshot save failed: {}", error.what());
|
||||||
|
} catch (...) {
|
||||||
|
log_warning("graphics::d3d9", "screenshot save failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (image_processing_must_be_inline()) {
|
||||||
|
guarded();
|
||||||
|
} else {
|
||||||
|
static auto pool = ThreadPool(2);
|
||||||
|
pool.add(std::move(guarded));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process_image_request(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
WrappedIDirect3DDevice9 *wrapped_device,
|
||||||
|
const ImageRequest &request) {
|
||||||
|
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
|
||||||
|
|
||||||
|
std::vector<int> screens { request.screen };
|
||||||
|
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
|
||||||
|
screens.clear();
|
||||||
|
wrapped_device->get_screenshot_screens(screens);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BackbufferCopy> copies;
|
||||||
|
copies.reserve(screens.size());
|
||||||
|
for (const int screen : screens) {
|
||||||
|
std::optional<BackbufferCopy> copy;
|
||||||
|
|
||||||
|
IDirect3DSwapChain9 *swap_chain = nullptr;
|
||||||
|
HRESULT hr = wrapped_device->get_screenshot_swap_chain(screen, &swap_chain);
|
||||||
|
if (FAILED(hr) || swap_chain == nullptr) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to get swap chain for screen {}, hr={}",
|
||||||
|
screen,
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
} else {
|
||||||
|
// only the API capture path runs often enough to benefit from pooling
|
||||||
|
copy = d3d9_readback::acquire_backbuffer_copy(device, swap_chain, screen, !screenshot);
|
||||||
|
swap_chain->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copy.has_value()) {
|
||||||
|
copies.emplace_back(std::move(*copy));
|
||||||
|
} else if (!screenshot) {
|
||||||
|
graphics_capture_skip(request.screen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copies.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!screenshot) {
|
||||||
|
PendingCapture capture;
|
||||||
|
if (!read_capture_surface(copies.front(), capture)) {
|
||||||
|
graphics_capture_skip(request.screen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
copies.clear();
|
||||||
|
dispatch_capture_save(std::move(capture));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reading a surface touches the device, and doing that off the present thread
|
||||||
|
// has been seen to deadlock games whose device has no internal locking
|
||||||
|
std::vector<PendingWrite> writes;
|
||||||
|
writes.reserve(copies.size());
|
||||||
|
for (const auto © : copies) {
|
||||||
|
PendingWrite write;
|
||||||
|
write.screen = copy.screen;
|
||||||
|
write.format = copy.desc.Format;
|
||||||
|
write.width = copy.desc.Width;
|
||||||
|
write.height = copy.desc.Height;
|
||||||
|
|
||||||
|
if (read_surface_raw(copy, write.pitch, write.data) != SurfaceRead::Ok) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writes.push_back(std::move(write));
|
||||||
|
}
|
||||||
|
|
||||||
|
copies.clear();
|
||||||
|
|
||||||
|
dispatch_screenshot_save(std::move(writes), screens.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void graphics_d3d9_process_screenshot(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
WrappedIDirect3DDevice9 *wrapped_device) {
|
||||||
|
if (graphics_screenshot_consume()) {
|
||||||
|
process_image_request(device, wrapped_device, ImageRequest {
|
||||||
|
.kind = ImageRequestKind::Screenshot,
|
||||||
|
.screen = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void graphics_d3d9_process_capture(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
WrappedIDirect3DDevice9 *wrapped_device) {
|
||||||
|
int screen = 0;
|
||||||
|
if (graphics_capture_consume(&screen)) {
|
||||||
|
process_image_request(device, wrapped_device, ImageRequest {
|
||||||
|
.kind = ImageRequestKind::Capture,
|
||||||
|
.screen = screen,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
struct WrappedIDirect3DDevice9;
|
||||||
|
|
||||||
|
void graphics_d3d9_process_screenshot(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
WrappedIDirect3DDevice9 *wrapped_device);
|
||||||
|
|
||||||
|
void graphics_d3d9_process_capture(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
WrappedIDirect3DDevice9 *wrapped_device);
|
||||||
@@ -26,7 +26,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid
|
|||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (//riid == __uuidof(IUnknown) || Ignore IUnknown, it's often queried to test object equality between different interfaces
|
if ((riid == IID_IUnknown && pDev->is_gfdm_two_head_exclusive()) ||
|
||||||
riid == IID_IDirect3DSwapChain9 ||
|
riid == IID_IDirect3DSwapChain9 ||
|
||||||
riid == IID_IDirect3DSwapChain9Ex)
|
riid == IID_IDirect3DSwapChain9Ex)
|
||||||
{
|
{
|
||||||
@@ -89,7 +89,48 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
|
|||||||
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
|
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
|
||||||
}
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags));
|
HRESULT result = pReal->Present(
|
||||||
|
pSourceRect,
|
||||||
|
pDestRect,
|
||||||
|
hDestWindowOverride,
|
||||||
|
pDirtyRegion,
|
||||||
|
dwFlags);
|
||||||
|
|
||||||
|
// a successful game subscreen present suppresses the next forced fallback present.
|
||||||
|
if (SUCCEEDED(result) &&
|
||||||
|
(this == pDev->implicit_sub_swapchain || this == pDev->sub_swapchain[0])) {
|
||||||
|
graphics_d3d9_notify_subscreen_present();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some drivers report S_PRESENT_MODE_CHANGED after Windows moves the portrait SMALL
|
||||||
|
// head into the requested exclusive mode, leaving both group heads black. Toggle SMALL
|
||||||
|
// through another supported mode and restore it once, then retry the interrupted MAIN
|
||||||
|
// present. Recovery is restricted to the creation thread and consumes its armed state,
|
||||||
|
// so subsequent presents cannot repeat the mode switch.
|
||||||
|
if (pDev->is_gfdm_two_head_exclusive()
|
||||||
|
&& native_group_head == NativeGroupHead::Main
|
||||||
|
&& result == S_PRESENT_MODE_CHANGED)
|
||||||
|
{
|
||||||
|
HRESULT recovery_failure = D3D_OK;
|
||||||
|
const auto recovery = pDev->gfdm_recover_present_mode_change(
|
||||||
|
&recovery_failure);
|
||||||
|
if (recovery
|
||||||
|
== WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult::ReadyToRetry)
|
||||||
|
{
|
||||||
|
result = pReal->Present(
|
||||||
|
pSourceRect,
|
||||||
|
pDestRect,
|
||||||
|
hDestWindowOverride,
|
||||||
|
pDirtyRegion,
|
||||||
|
dwFlags);
|
||||||
|
} else if (recovery
|
||||||
|
== WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult::Failed
|
||||||
|
&& FAILED(recovery_failure))
|
||||||
|
{
|
||||||
|
result = recovery_failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK_RESULT(result);
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
||||||
CHECK_RESULT(pReal->GetFrontBufferData(pDestSurface));
|
CHECK_RESULT(pReal->GetFrontBufferData(pDestSurface));
|
||||||
|
|||||||
@@ -5,8 +5,13 @@
|
|||||||
interface WrappedIDirect3DDevice9;
|
interface WrappedIDirect3DDevice9;
|
||||||
|
|
||||||
struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
||||||
WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9 *orig) :
|
enum class NativeGroupHead { Unknown, Main, Small };
|
||||||
pDev(dev), pReal(orig), is_d3d9ex(false)
|
|
||||||
|
WrappedIDirect3DSwapChain9(
|
||||||
|
WrappedIDirect3DDevice9 *dev,
|
||||||
|
IDirect3DSwapChain9 *orig,
|
||||||
|
NativeGroupHead native_group_head = NativeGroupHead::Unknown) :
|
||||||
|
pDev(dev), pReal(orig), is_d3d9ex(false), native_group_head(native_group_head)
|
||||||
{
|
{
|
||||||
IDirect3DSwapChain9Ex *swapchain = nullptr;
|
IDirect3DSwapChain9Ex *swapchain = nullptr;
|
||||||
|
|
||||||
@@ -16,8 +21,11 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9Ex *orig) :
|
WrappedIDirect3DSwapChain9(
|
||||||
pDev(dev), pReal(orig), is_d3d9ex(true)
|
WrappedIDirect3DDevice9 *dev,
|
||||||
|
IDirect3DSwapChain9Ex *orig,
|
||||||
|
NativeGroupHead native_group_head = NativeGroupHead::Unknown) :
|
||||||
|
pDev(dev), pReal(orig), is_d3d9ex(true), native_group_head(native_group_head)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +61,7 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
|||||||
|
|
||||||
IDirect3DSwapChain9 *pReal;
|
IDirect3DSwapChain9 *pReal;
|
||||||
bool is_d3d9ex = false;
|
bool is_d3d9ex = false;
|
||||||
|
NativeGroupHead native_group_head = NativeGroupHead::Unknown;
|
||||||
|
|
||||||
bool should_run_hooks = true;
|
bool should_run_hooks = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,10 +17,14 @@
|
|||||||
#include "games/ddr/ddr.h"
|
#include "games/ddr/ddr.h"
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
|
#include "games/io.h"
|
||||||
|
#include "games/sdvx/sdvx.h"
|
||||||
#include "games/popn/popn.h"
|
#include "games/popn/popn.h"
|
||||||
|
#include "hooks/graphics/jpeg_encoder.h"
|
||||||
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
|
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
|
||||||
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
|
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
|
#include "misc/hotkeys.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "touch/touch_gestures.h"
|
#include "touch/touch_gestures.h"
|
||||||
@@ -28,7 +32,9 @@
|
|||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
#include "misc/wintouchemu.h"
|
||||||
#include "touch/native/inject.h"
|
#include "touch/native/inject.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
|
|
||||||
@@ -43,6 +49,8 @@ HWND SDVX_SUBSCREEN_WINDOW = nullptr;
|
|||||||
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
|
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
|
||||||
static HWND GFDM_LEFT_WINDOW = nullptr;
|
static HWND GFDM_LEFT_WINDOW = nullptr;
|
||||||
static HWND GFDM_RIGHT_WINDOW = nullptr;
|
static HWND GFDM_RIGHT_WINDOW = nullptr;
|
||||||
|
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
|
||||||
|
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
|
||||||
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
||||||
bool FAKE_SUBSCREEN_ADAPTER = false;
|
bool FAKE_SUBSCREEN_ADAPTER = false;
|
||||||
|
|
||||||
@@ -57,7 +65,6 @@ static bool GRAPHICS_SCREENSHOT_TRIGGER = false;
|
|||||||
static std::set<int> GRAPHICS_SCREENS { 0 };
|
static std::set<int> GRAPHICS_SCREENS { 0 };
|
||||||
static std::mutex GRAPHICS_SCREENS_M {};
|
static std::mutex GRAPHICS_SCREENS_M {};
|
||||||
static std::vector<int> GRAPHICS_CAPTURE_SCREENS;
|
static std::vector<int> GRAPHICS_CAPTURE_SCREENS;
|
||||||
static const size_t GRAPHICS_CAPTURE_SCREEN_NO = 4;
|
|
||||||
static std::mutex GRAPHICS_CAPTURE_SCREENS_M {};
|
static std::mutex GRAPHICS_CAPTURE_SCREENS_M {};
|
||||||
static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||||
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||||
@@ -112,6 +119,8 @@ uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0;
|
|||||||
// settings
|
// settings
|
||||||
std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146";
|
std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146";
|
||||||
std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots";
|
std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots";
|
||||||
|
bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = false;
|
||||||
|
bool GRAPHICS_SCREENSHOT_SUBSCREENS = false;
|
||||||
|
|
||||||
static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr;
|
static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr;
|
||||||
static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr;
|
static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr;
|
||||||
@@ -182,6 +191,13 @@ static bool is_gfdm_known_window(HWND hWnd) {
|
|||||||
return gitadora_window_name_for_hwnd(hWnd) != nullptr;
|
return gitadora_window_name_for_hwnd(hWnd) != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool is_gfdm_two_head_small_window(HWND hWnd) {
|
||||||
|
return games::gitadora::is_arena_model() &&
|
||||||
|
games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE &&
|
||||||
|
hWnd != nullptr &&
|
||||||
|
hWnd == GFDM_TWO_HEAD_SMALL_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
static bool gitadora_should_block_game_window_placement(HWND hWnd) {
|
static bool gitadora_should_block_game_window_placement(HWND hWnd) {
|
||||||
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -201,6 +217,107 @@ static void gitadora_remember_window(HWND hWnd, const std::string &window_name)
|
|||||||
} else if (window_name == "SMALL") {
|
} else if (window_name == "SMALL") {
|
||||||
GFDM_SUBSCREEN_WINDOW = hWnd;
|
GFDM_SUBSCREEN_WINDOW = hWnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// touch belongs to the SMALL panel when it exists, otherwise to the main window
|
||||||
|
// that draws the subscreen overlay
|
||||||
|
const bool hosts_touch = window_name == "SMALL" ||
|
||||||
|
(window_name == "GITADORA" && !graphics_gitadora_has_dedicated_subscreen());
|
||||||
|
if (nativetouch::is_hooked() && hWnd != nullptr && hosts_touch) {
|
||||||
|
nativetouch::inject::set_preferred_injection_window(hWnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphics_gitadora_has_dedicated_subscreen() {
|
||||||
|
return GFDM_SUBSCREEN_WINDOW != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphics_gitadora_prepare_two_head_device_window(
|
||||||
|
HWND hWnd, HMONITOR target_monitor, UINT desired_width, UINT desired_height) {
|
||||||
|
if (hWnd == nullptr || !IsWindow(hWnd)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: native secondary device window is invalid: {}",
|
||||||
|
fmt::ptr(hWnd));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ShowWindow_orig == nullptr || SetWindowLongA_orig == nullptr ||
|
||||||
|
SetWindowPos_orig == nullptr) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: window hooks are not initialized for {}",
|
||||||
|
fmt::ptr(hWnd));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT rect_before {};
|
||||||
|
GetWindowRect(hWnd, &rect_before);
|
||||||
|
const DWORD style_before = static_cast<DWORD>(GetWindowLongA(hWnd, GWL_STYLE));
|
||||||
|
const BOOL visible_before = IsWindowVisible(hWnd);
|
||||||
|
|
||||||
|
if (target_monitor != nullptr) {
|
||||||
|
GFDM_TWO_HEAD_SMALL_MONITOR = target_monitor;
|
||||||
|
}
|
||||||
|
const HMONITOR monitor = target_monitor != nullptr
|
||||||
|
? target_monitor
|
||||||
|
: (GFDM_TWO_HEAD_SMALL_MONITOR != nullptr
|
||||||
|
? GFDM_TWO_HEAD_SMALL_MONITOR
|
||||||
|
: MonitorFromWindow(hWnd, MONITOR_DEFAULTTONULL));
|
||||||
|
MONITORINFO monitor_info {};
|
||||||
|
monitor_info.cbSize = sizeof(monitor_info);
|
||||||
|
if (monitor == nullptr || !GetMonitorInfoA(monitor, &monitor_info)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: could not find the SMALL monitor for {}",
|
||||||
|
fmt::ptr(hWnd));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arena requests D3DCREATE_NOWINDOWCHANGES, so place its SMALL window on
|
||||||
|
// the second head before D3D9 takes ownership of it.
|
||||||
|
const DWORD fullscreen_style = (style_before & ~WS_OVERLAPPEDWINDOW) | WS_POPUP;
|
||||||
|
const bool style_changed = fullscreen_style != style_before;
|
||||||
|
if (style_changed) {
|
||||||
|
SetWindowLongA_orig(hWnd, GWL_STYLE, static_cast<LONG>(fullscreen_style));
|
||||||
|
}
|
||||||
|
const RECT &monitor_rect = monitor_info.rcMonitor;
|
||||||
|
const int host_width = desired_width != 0
|
||||||
|
? static_cast<int>(desired_width)
|
||||||
|
: monitor_rect.right - monitor_rect.left;
|
||||||
|
const int host_height = desired_height != 0
|
||||||
|
? static_cast<int>(desired_height)
|
||||||
|
: monitor_rect.bottom - monitor_rect.top;
|
||||||
|
const bool rect_changed = rect_before.left != monitor_rect.left ||
|
||||||
|
rect_before.top != monitor_rect.top ||
|
||||||
|
rect_before.right != monitor_rect.left + host_width ||
|
||||||
|
rect_before.bottom != monitor_rect.top + host_height;
|
||||||
|
if ((style_changed || rect_changed) && !SetWindowPos_orig(
|
||||||
|
hWnd,
|
||||||
|
HWND_TOP,
|
||||||
|
monitor_rect.left,
|
||||||
|
monitor_rect.top,
|
||||||
|
host_width,
|
||||||
|
host_height,
|
||||||
|
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: failed to place SMALL window, error={}",
|
||||||
|
GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visible_before) {
|
||||||
|
ShowWindow_orig(hWnd, SW_SHOWNOACTIVATE);
|
||||||
|
}
|
||||||
|
if (MonitorFromWindow(hWnd, MONITOR_DEFAULTTONULL) != monitor) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: SMALL window {} is on the wrong monitor",
|
||||||
|
fmt::ptr(hWnd));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GFDM_TWO_HEAD_SMALL_WINDOW = hWnd;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool gitadora_should_allow_small_resize() {
|
static bool gitadora_should_allow_small_resize() {
|
||||||
@@ -307,6 +424,22 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
|
||||||
|
// drop mouse inputs since only wintouches should be used
|
||||||
|
switch (uMsg) {
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
case WM_LBUTTONDOWN:
|
||||||
|
case WM_LBUTTONUP:
|
||||||
|
case WM_MBUTTONDOWN:
|
||||||
|
case WM_MBUTTONUP:
|
||||||
|
case WM_RBUTTONDOWN:
|
||||||
|
case WM_RBUTTONUP:
|
||||||
|
case WM_XBUTTONDOWN:
|
||||||
|
case WM_XBUTTONUP:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// window resize
|
// window resize
|
||||||
graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);
|
graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);
|
||||||
|
|
||||||
@@ -496,8 +629,10 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool is_sdvx = avs::game::is_model("KFC");
|
||||||
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
|
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
|
||||||
bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen");
|
bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen");
|
||||||
|
bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen");
|
||||||
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
|
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
|
||||||
const std::string gfdm_window_name = games::gitadora::is_arena_model()
|
const std::string gfdm_window_name = games::gitadora::is_arena_model()
|
||||||
? gitadora_canonical_window_name(effective_window_name)
|
? gitadora_canonical_window_name(effective_window_name)
|
||||||
@@ -600,10 +735,36 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW);
|
graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SDVX registers touch on both windows, so name the one synthetic touches must land on
|
||||||
|
// instead of letting window creation order decide: the sub screen window when windowed,
|
||||||
|
// the main window in fullscreen since the game reads it in primary-display coordinates
|
||||||
|
if (nativetouch::is_hooked() &&
|
||||||
|
result != nullptr &&
|
||||||
|
(GRAPHICS_WINDOWED ? is_sdvx_sub_window : is_sdvx_main_window)) {
|
||||||
|
log_misc(
|
||||||
|
"graphics",
|
||||||
|
"SDVX touch surface is {}, {}",
|
||||||
|
fmt::ptr(result),
|
||||||
|
window_name);
|
||||||
|
nativetouch::inject::set_preferred_injection_window(result);
|
||||||
|
}
|
||||||
|
|
||||||
// only hook touch window if multiple windows are allowed
|
// only hook touch window if multiple windows are allowed
|
||||||
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
||||||
gitadora_remember_window(result, gfdm_window_name);
|
gitadora_remember_window(result, gfdm_window_name);
|
||||||
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
}
|
||||||
|
if (is_gfdm_sub_window &&
|
||||||
|
((GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) ||
|
||||||
|
games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE)) {
|
||||||
|
gitadora_remember_window(result, gfdm_window_name);
|
||||||
|
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
|
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the dedicated SMALL window is the touch panel; mouse and API touch target it
|
||||||
|
if (nativetouch::is_hooked() && result != nullptr) {
|
||||||
|
nativetouch::inject::register_and_attach_window(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
gitadora_force_window_style(result);
|
gitadora_force_window_style(result);
|
||||||
@@ -750,6 +911,10 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
|||||||
nHeight,
|
nHeight,
|
||||||
bRepaint);
|
bRepaint);
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// sound voltex windowed mode adjustments
|
// sound voltex windowed mode adjustments
|
||||||
if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) {
|
if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) {
|
||||||
RECT rect {};
|
RECT rect {};
|
||||||
@@ -786,7 +951,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
|||||||
nWidth = rect.right - rect.left;
|
nWidth = rect.right - rect.left;
|
||||||
nHeight = rect.bottom - rect.top;
|
nHeight = rect.bottom - rect.top;
|
||||||
|
|
||||||
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
|
if (games::iidx::NATIVE_TOUCH) {
|
||||||
|
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
|
||||||
|
} else {
|
||||||
|
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
||||||
log_info(
|
log_info(
|
||||||
@@ -853,6 +1022,11 @@ static HCURSOR WINAPI SetCursor_hook(HCURSOR hCursor) {
|
|||||||
|
|
||||||
static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
|
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE)) {
|
||||||
|
return GetWindowLongA(hWnd, nIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// DDR window style fix
|
// DDR window style fix
|
||||||
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
|
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
|
||||||
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
||||||
@@ -873,6 +1047,11 @@ static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
|||||||
|
|
||||||
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
|
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE)) {
|
||||||
|
return GetWindowLongW(hWnd, nIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// DDR overlapped window fix
|
// DDR overlapped window fix
|
||||||
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
|
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
|
||||||
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
||||||
@@ -894,6 +1073,12 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
|||||||
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
||||||
int X, int Y, int cx, int cy, UINT uFlags) {
|
int X, int Y, int cx, int cy, UINT uFlags) {
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
|
((uFlags & SWP_HIDEWINDOW) ||
|
||||||
|
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// windowed mode adjustments
|
// windowed mode adjustments
|
||||||
if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) {
|
if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) {
|
||||||
return TRUE;
|
return TRUE;
|
||||||
@@ -918,6 +1103,13 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
|
(nCmdShow == SW_HIDE || nCmdShow == SW_MINIMIZE ||
|
||||||
|
nCmdShow == SW_SHOWMINIMIZED || nCmdShow == SW_SHOWMINNOACTIVE ||
|
||||||
|
nCmdShow == SW_FORCEMINIMIZE)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
if (games::gitadora::is_arena_model() &&
|
if (games::gitadora::is_arena_model() &&
|
||||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||||
hWnd != GRAPHICS_HOOKED_WINDOW) {
|
hWnd != GRAPHICS_HOOKED_WINDOW) {
|
||||||
@@ -946,6 +1138,15 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fullscreen SDVX keeps two adapters so the subscreen overlay can draw, so the game still
|
||||||
|
// creates the sub window even when the user asked for it to be gone
|
||||||
|
if (avs::game::is_model("KFC") &&
|
||||||
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||||
|
hWnd == SDVX_SUBSCREEN_WINDOW) {
|
||||||
|
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
return ShowWindow_orig(hWnd, nCmdShow);
|
return ShowWindow_orig(hWnd, nCmdShow);
|
||||||
}
|
}
|
||||||
@@ -1160,8 +1361,11 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
|
|||||||
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
||||||
|
|
||||||
const bool native_touch_overlay =
|
const bool native_touch_overlay =
|
||||||
(games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
|
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
|
||||||
(games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
|
(games::popn::NATIVE_TOUCH &&
|
||||||
|
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) ||
|
||||||
|
(games::gitadora::NATIVE_TOUCH &&
|
||||||
|
games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
|
||||||
if (native_touch_overlay) {
|
if (native_touch_overlay) {
|
||||||
nativetouch::inject::register_and_attach_window(hWnd);
|
nativetouch::inject::register_and_attach_window(hWnd);
|
||||||
}
|
}
|
||||||
@@ -1219,6 +1423,12 @@ void graphics_screens_get(std::vector<int> &screens) {
|
|||||||
screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
|
screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void graphics_poll_screenshot_hotkey() {
|
||||||
|
if (hotkeys::consume_screenshot()) {
|
||||||
|
graphics_screenshot_trigger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void graphics_screenshot_trigger() {
|
void graphics_screenshot_trigger() {
|
||||||
GRAPHICS_SCREENSHOT_TRIGGER = true;
|
GRAPHICS_SCREENSHOT_TRIGGER = true;
|
||||||
}
|
}
|
||||||
@@ -1271,10 +1481,12 @@ void graphics_capture_skip(int screen) {
|
|||||||
GRAPHICS_CAPTURE_CV[screen].notify_one();
|
GRAPHICS_CAPTURE_CV[screen].notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
|
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
|
||||||
bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp,
|
int divide, uint64_t *timestamp,
|
||||||
int *width, int *height) {
|
int *width, int *height) {
|
||||||
|
|
||||||
|
out = nullptr;
|
||||||
|
|
||||||
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1346,11 +1558,7 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
|
|||||||
capture_height = height_new;
|
capture_height = height_new;
|
||||||
}
|
}
|
||||||
|
|
||||||
// compress
|
out = std::move(capture_data);
|
||||||
auto success = TooJpeg::writeJpeg(
|
|
||||||
receiver, capture_data.get(),
|
|
||||||
capture_width, capture_height,
|
|
||||||
rgb, quality, downsample);
|
|
||||||
|
|
||||||
// status
|
// status
|
||||||
if (timestamp) {
|
if (timestamp) {
|
||||||
@@ -1363,11 +1571,45 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
|
|||||||
*height = capture_height;
|
*height = capture_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
|
||||||
|
int quality, int divide, uint64_t *timestamp,
|
||||||
|
int *width, int *height) {
|
||||||
|
|
||||||
|
out.clear();
|
||||||
|
|
||||||
|
std::shared_ptr<uint8_t[]> pixels;
|
||||||
|
int capture_width = 0;
|
||||||
|
int capture_height = 0;
|
||||||
|
if (!graphics_capture_receive_raw(
|
||||||
|
screen, pixels, divide, timestamp, &capture_width, &capture_height)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compress
|
||||||
|
const bool success = jpeg_encoder::encode(
|
||||||
|
out, pixels.get(),
|
||||||
|
capture_width, capture_height, quality);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
out.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// status
|
||||||
|
if (width) {
|
||||||
|
*width = capture_width;
|
||||||
|
}
|
||||||
|
if (height) {
|
||||||
|
*height = capture_height;
|
||||||
|
}
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string graphics_screenshot_genpath() {
|
std::string graphics_screenshot_genpath(const std::vector<int> &screens) {
|
||||||
|
|
||||||
// verify dir path
|
// verify dir path
|
||||||
if (GRAPHICS_SCREENSHOT_DIR.empty()) {
|
if (GRAPHICS_SCREENSHOT_DIR.empty()) {
|
||||||
@@ -1392,11 +1634,21 @@ std::string graphics_screenshot_genpath() {
|
|||||||
auto tm_now = *std::gmtime(&t_now);
|
auto tm_now = *std::gmtime(&t_now);
|
||||||
auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d"));
|
auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d"));
|
||||||
|
|
||||||
// find next filename
|
// find next filename; the whole set has to be free so one shot stays numbered together
|
||||||
size_t id = 0;
|
size_t id = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id);
|
auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id);
|
||||||
if (!fileutils::file_exists(filepath)) {
|
bool available = !fileutils::file_exists(filepath);
|
||||||
|
for (const auto screen : screens) {
|
||||||
|
if (!available) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (screen != 0) {
|
||||||
|
available = !fileutils::file_exists(fmt::format(
|
||||||
|
"{}\\{}_{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id, screen));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (available) {
|
||||||
return filepath;
|
return filepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -12,8 +13,6 @@
|
|||||||
#include <dwmapi.h>
|
#include <dwmapi.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "external/toojpeg/toojpeg.h"
|
|
||||||
|
|
||||||
// order must match spice2x_AutoOrientation UI enum order
|
// order must match spice2x_AutoOrientation UI enum order
|
||||||
enum graphics_orientation {
|
enum graphics_orientation {
|
||||||
ORIENTATION_CW = 0,
|
ORIENTATION_CW = 0,
|
||||||
@@ -111,6 +110,8 @@ extern bool FAKE_SUBSCREEN_ADAPTER;
|
|||||||
// settings
|
// settings
|
||||||
extern std::string GRAPHICS_DEVICEID;
|
extern std::string GRAPHICS_DEVICEID;
|
||||||
extern std::string GRAPHICS_SCREENSHOT_DIR;
|
extern std::string GRAPHICS_SCREENSHOT_DIR;
|
||||||
|
extern bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY;
|
||||||
|
extern bool GRAPHICS_SCREENSHOT_SUBSCREENS;
|
||||||
|
|
||||||
// Direct3D 9 settings
|
// Direct3D 9 settings
|
||||||
extern std::optional<UINT> D3D9_ADAPTER;
|
extern std::optional<UINT> D3D9_ADAPTER;
|
||||||
@@ -119,23 +120,44 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
|
|||||||
|
|
||||||
void graphics_init();
|
void graphics_init();
|
||||||
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
||||||
|
bool graphics_gitadora_has_dedicated_subscreen();
|
||||||
|
// The native GITADORA two-head D3D9 group uses the game's named SMALL
|
||||||
|
// device window for the native physical SMALL head. The game requests
|
||||||
|
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
|
||||||
|
// to the requested native SMALL mode before the native fullscreen device is
|
||||||
|
// created. During the adapter-group startup warmup, the monitor can briefly
|
||||||
|
// report a different mode; preserve its origin but not that temporary size.
|
||||||
|
bool graphics_gitadora_prepare_two_head_device_window(
|
||||||
|
HWND hWnd, HMONITOR target_monitor = nullptr, UINT desired_width = 0,
|
||||||
|
UINT desired_height = 0);
|
||||||
void graphics_add_wnd_proc(WNDPROC wndProc);
|
void graphics_add_wnd_proc(WNDPROC wndProc);
|
||||||
void graphics_remove_wnd_proc(WNDPROC wndProc);
|
void graphics_remove_wnd_proc(WNDPROC wndProc);
|
||||||
void graphics_hook_subscreen_window(HWND hWnd);
|
void graphics_hook_subscreen_window(HWND hWnd);
|
||||||
void graphics_screens_register(int screen);
|
void graphics_screens_register(int screen);
|
||||||
void graphics_screens_unregister(int screen);
|
void graphics_screens_unregister(int screen);
|
||||||
void graphics_screens_get(std::vector<int> &screens);
|
void graphics_screens_get(std::vector<int> &screens);
|
||||||
|
void graphics_poll_screenshot_hotkey();
|
||||||
void graphics_screenshot_trigger();
|
void graphics_screenshot_trigger();
|
||||||
bool graphics_screenshot_consume();
|
bool graphics_screenshot_consume();
|
||||||
|
|
||||||
|
inline constexpr size_t GRAPHICS_CAPTURE_SCREEN_NO = 4;
|
||||||
|
|
||||||
void graphics_capture_trigger(int screen);
|
void graphics_capture_trigger(int screen);
|
||||||
bool graphics_capture_consume(int *screen);
|
bool graphics_capture_consume(int *screen);
|
||||||
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height);
|
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height);
|
||||||
void graphics_capture_skip(int screen);
|
void graphics_capture_skip(int screen);
|
||||||
bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
|
// on success `out` owns packed 24bpp RGB pixels, width * height * 3 bytes
|
||||||
bool rgb = true, int quality = 80, bool downsample = true, int divide = 0,
|
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
|
||||||
|
int divide = 0,
|
||||||
uint64_t *timestamp = nullptr,
|
uint64_t *timestamp = nullptr,
|
||||||
int *width = nullptr, int *height = nullptr);
|
int *width = nullptr, int *height = nullptr);
|
||||||
std::string graphics_screenshot_genpath();
|
// on success `out` holds the encoded JPEG; its storage is reused across calls
|
||||||
|
bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
|
||||||
|
int quality = 80, int divide = 0,
|
||||||
|
uint64_t *timestamp = nullptr,
|
||||||
|
int *width = nullptr, int *height = nullptr);
|
||||||
|
// the returned path is for screen 0; any extra screens only reserve their suffixed names
|
||||||
|
std::string graphics_screenshot_genpath(const std::vector<int> &screens = {});
|
||||||
|
|
||||||
// graphics_windowed.cpp
|
// graphics_windowed.cpp
|
||||||
void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#include "jpeg_encoder.h"
|
||||||
|
|
||||||
|
#ifdef SPICE_JPEG
|
||||||
|
|
||||||
|
#include <csetjmp>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include <jpeglib.h>
|
||||||
|
|
||||||
|
namespace jpeg_encoder {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr size_t CHUNK_SIZE = 16 * 1024;
|
||||||
|
|
||||||
|
// libjpeg writes through a destination manager; this one appends straight into
|
||||||
|
// the caller's vector so the encoded frame is never copied
|
||||||
|
struct VectorDestination {
|
||||||
|
jpeg_destination_mgr mgr {};
|
||||||
|
std::vector<uint8_t> *out = nullptr;
|
||||||
|
uint8_t chunk[CHUNK_SIZE] {};
|
||||||
|
};
|
||||||
|
|
||||||
|
void dest_init(j_compress_ptr cinfo) {
|
||||||
|
auto dest = reinterpret_cast<VectorDestination *>(cinfo->dest);
|
||||||
|
dest->mgr.next_output_byte = dest->chunk;
|
||||||
|
dest->mgr.free_in_buffer = CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// libjpeg cannot unwind a C++ exception out of its own frames, so growing the
|
||||||
|
// output has to fail by value and be turned into an error_exit by the caller
|
||||||
|
bool dest_append(VectorDestination *dest, size_t size) {
|
||||||
|
try {
|
||||||
|
dest->out->insert(dest->out->end(), dest->chunk, dest->chunk + size);
|
||||||
|
return true;
|
||||||
|
} catch (...) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean dest_empty(j_compress_ptr cinfo) {
|
||||||
|
auto dest = reinterpret_cast<VectorDestination *>(cinfo->dest);
|
||||||
|
|
||||||
|
// returning FALSE would mean suspension to libjpeg, not failure
|
||||||
|
if (!dest_append(dest, CHUNK_SIZE)) {
|
||||||
|
(*cinfo->err->error_exit)(reinterpret_cast<j_common_ptr>(cinfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
dest->mgr.next_output_byte = dest->chunk;
|
||||||
|
dest->mgr.free_in_buffer = CHUNK_SIZE;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dest_term(j_compress_ptr cinfo) {
|
||||||
|
auto dest = reinterpret_cast<VectorDestination *>(cinfo->dest);
|
||||||
|
if (!dest_append(dest, CHUNK_SIZE - dest->mgr.free_in_buffer)) {
|
||||||
|
(*cinfo->err->error_exit)(reinterpret_cast<j_common_ptr>(cinfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the default handler calls exit(), which is not an option inside a game process
|
||||||
|
struct ErrorManager {
|
||||||
|
jpeg_error_mgr mgr {};
|
||||||
|
jmp_buf escape {};
|
||||||
|
};
|
||||||
|
|
||||||
|
void on_error(j_common_ptr cinfo) {
|
||||||
|
longjmp(reinterpret_cast<ErrorManager *>(cinfo->err)->escape, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_message(j_common_ptr) {
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool encode(
|
||||||
|
std::vector<uint8_t> &out,
|
||||||
|
const uint8_t *pixels,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int quality) {
|
||||||
|
|
||||||
|
if (!pixels || width <= 0 || height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality < 1) {
|
||||||
|
quality = 1;
|
||||||
|
} else if (quality > 100) {
|
||||||
|
quality = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// zero init is load bearing: the trap below is armed before the struct is
|
||||||
|
// created, and jpeg_destroy_compress only tolerates that on a zeroed struct
|
||||||
|
jpeg_compress_struct cinfo {};
|
||||||
|
ErrorManager err;
|
||||||
|
VectorDestination dest;
|
||||||
|
|
||||||
|
cinfo.err = jpeg_std_error(&err.mgr);
|
||||||
|
err.mgr.error_exit = on_error;
|
||||||
|
err.mgr.output_message = on_message;
|
||||||
|
|
||||||
|
if (setjmp(err.escape)) {
|
||||||
|
jpeg_destroy_compress(&cinfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
jpeg_create_compress(&cinfo);
|
||||||
|
|
||||||
|
dest.out = &out;
|
||||||
|
dest.mgr.init_destination = dest_init;
|
||||||
|
dest.mgr.empty_output_buffer = dest_empty;
|
||||||
|
dest.mgr.term_destination = dest_term;
|
||||||
|
cinfo.dest = &dest.mgr;
|
||||||
|
|
||||||
|
cinfo.image_width = static_cast<JDIMENSION>(width);
|
||||||
|
cinfo.image_height = static_cast<JDIMENSION>(height);
|
||||||
|
cinfo.in_color_space = JCS_RGB;
|
||||||
|
cinfo.input_components = 3;
|
||||||
|
|
||||||
|
jpeg_set_defaults(&cinfo);
|
||||||
|
jpeg_set_quality(&cinfo, quality, TRUE);
|
||||||
|
|
||||||
|
// 4:2:0, matching what the capture path asked the previous encoder for
|
||||||
|
cinfo.comp_info[0].h_samp_factor = 2;
|
||||||
|
cinfo.comp_info[0].v_samp_factor = 2;
|
||||||
|
|
||||||
|
jpeg_start_compress(&cinfo, TRUE);
|
||||||
|
|
||||||
|
const size_t pitch = static_cast<size_t>(width) * 3;
|
||||||
|
while (cinfo.next_scanline < cinfo.image_height) {
|
||||||
|
auto row = const_cast<uint8_t *>(pixels + cinfo.next_scanline * pitch);
|
||||||
|
JSAMPROW rows[1] = { row };
|
||||||
|
jpeg_write_scanlines(&cinfo, rows, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
jpeg_finish_compress(&cinfo);
|
||||||
|
jpeg_destroy_compress(&cinfo);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else // SPICE_JPEG
|
||||||
|
|
||||||
|
namespace jpeg_encoder {
|
||||||
|
|
||||||
|
// builds without libjpeg-turbo (the WinXP toolchains) simply cannot encode;
|
||||||
|
// callers already treat a false return as "no frame available"
|
||||||
|
bool encode(std::vector<uint8_t> &out, const uint8_t *, int, int, int) {
|
||||||
|
out.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPICE_JPEG
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace jpeg_encoder {
|
||||||
|
|
||||||
|
// appends a baseline 4:2:0 JPEG of packed 24bpp RGB pixels to `out`
|
||||||
|
bool encode(
|
||||||
|
std::vector<uint8_t> &out,
|
||||||
|
const uint8_t *pixels,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int quality);
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@
|
|||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
|
#include "games/popn/popn.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
|
#include "util/deferlog.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -24,12 +26,13 @@ constexpr UINT CODEPAGE_SHIFT_JIS = 932;
|
|||||||
static decltype(GetACP) *GetACP_orig = nullptr;
|
static decltype(GetACP) *GetACP_orig = nullptr;
|
||||||
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
|
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
|
||||||
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
|
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
|
||||||
|
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
|
||||||
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
|
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
|
||||||
|
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
|
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
|
||||||
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
|
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
|
||||||
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
|
static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr;
|
||||||
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
|
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
|
||||||
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
|
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
|
||||||
#endif
|
#endif
|
||||||
@@ -182,9 +185,32 @@ static BOOL WINAPI IsDBCSLeadByte_hook (
|
|||||||
BYTE TestChar
|
BYTE TestChar
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
if (IsDBCSLeadByteEx_orig) {
|
||||||
|
return IsDBCSLeadByteEx_orig(CODEPAGE_SHIFT_JIS, TestChar);
|
||||||
|
}
|
||||||
|
|
||||||
return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
|
return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static BOOL WINAPI IsDBCSLeadByteEx_hook(
|
||||||
|
UINT CodePage,
|
||||||
|
BYTE TestChar)
|
||||||
|
{
|
||||||
|
switch (CodePage) {
|
||||||
|
case CP_ACP:
|
||||||
|
case CP_THREAD_ACP:
|
||||||
|
CodePage = CODEPAGE_SHIFT_JIS;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsDBCSLeadByteEx_orig(CodePage, TestChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
static
|
static
|
||||||
int
|
int
|
||||||
WINAPI
|
WINAPI
|
||||||
@@ -220,6 +246,8 @@ WideCharToMultiByte_hook(
|
|||||||
lpUsedDefaultChar);
|
lpUsedDefaultChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
int
|
int
|
||||||
WINAPI
|
WINAPI
|
||||||
GetLocaleInfoA_hook(
|
GetLocaleInfoA_hook(
|
||||||
@@ -250,6 +278,18 @@ GetLocaleInfoA_hook(
|
|||||||
void hooks::lang::early_init() {
|
void hooks::lang::early_init() {
|
||||||
log_info("hooks::lang", "early initialization");
|
log_info("hooks::lang", "early initialization");
|
||||||
|
|
||||||
|
const auto native_code_page = GetACP();
|
||||||
|
if (native_code_page == CP_UTF8) {
|
||||||
|
log_warning(
|
||||||
|
"hooks::lang",
|
||||||
|
"Windows is using UTF-8 as the system code page; "
|
||||||
|
"some games may render text incorrectly or behave unexpectedly");
|
||||||
|
|
||||||
|
deferredlogs::defer_error_messages({
|
||||||
|
"Windows is using UTF-8 as the system code page",
|
||||||
|
" some games may render text incorrectly or behave unexpectedly"});
|
||||||
|
}
|
||||||
|
|
||||||
// hooking these two functions fixes the jubeat mojibake
|
// hooking these two functions fixes the jubeat mojibake
|
||||||
detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
|
detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
|
||||||
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
|
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
|
||||||
@@ -296,8 +336,9 @@ void hooks::lang::early_init() {
|
|||||||
&GetLocaleInfoA_orig);
|
&GetLocaleInfoA_orig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for TDJ subscreen search keyboard
|
// for TDJ subscreen search keyboard and T44 narrow-string handling
|
||||||
if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) {
|
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
|
||||||
|
avs::game::is_model("T44")) {
|
||||||
log_info("hooks::lang", "hooking IsDBCSLeadByte");
|
log_info("hooks::lang", "hooking IsDBCSLeadByte");
|
||||||
detour::trampoline_try(
|
detour::trampoline_try(
|
||||||
"kernel32.dll",
|
"kernel32.dll",
|
||||||
@@ -306,7 +347,25 @@ void hooks::lang::early_init() {
|
|||||||
&IsDBCSLeadByte_orig);
|
&IsDBCSLeadByte_orig);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (games::gitadora::is_arena_model()) {
|
if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) {
|
||||||
|
detour::trampoline_try(
|
||||||
|
"kernel32.dll",
|
||||||
|
"IsDBCSLeadByteEx",
|
||||||
|
IsDBCSLeadByteEx_hook,
|
||||||
|
&IsDBCSLeadByteEx_orig);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
const auto hook_wide_char_to_multi_byte =
|
||||||
|
games::gitadora::is_arena_model() || avs::game::is_model("T44");
|
||||||
|
#else
|
||||||
|
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
|
||||||
|
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (hook_wide_char_to_multi_byte) {
|
||||||
log_info("hooks::lang", "hooking WideCharToMultiByte");
|
log_info("hooks::lang", "hooking WideCharToMultiByte");
|
||||||
detour::trampoline_try(
|
detour::trampoline_try(
|
||||||
"kernel32.dll",
|
"kernel32.dll",
|
||||||
@@ -314,7 +373,6 @@ void hooks::lang::early_init() {
|
|||||||
WideCharToMultiByte_hook,
|
WideCharToMultiByte_hook,
|
||||||
&WideCharToMultiByte_orig);
|
&WideCharToMultiByte_orig);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <assert.h>
|
|
||||||
#include <shlwapi.h>
|
#include <shlwapi.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <cfg/configurator.h>
|
#include <cfg/configurator.h>
|
||||||
@@ -15,6 +14,7 @@
|
|||||||
#include "acio/icca/icca.h"
|
#include "acio/icca/icca.h"
|
||||||
#include "acio/mdxf/mdxf.h"
|
#include "acio/mdxf/mdxf.h"
|
||||||
#include "api/controller.h"
|
#include "api/controller.h"
|
||||||
|
#include "api/stream_server.h"
|
||||||
#include "avs/automap.h"
|
#include "avs/automap.h"
|
||||||
#include "avs/core.h"
|
#include "avs/core.h"
|
||||||
#include "avs/ea3.h"
|
#include "avs/ea3.h"
|
||||||
@@ -38,10 +38,10 @@
|
|||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "games/hpm/hpm.h"
|
#include "games/hpm/hpm.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
|
#include "games/iidx/poke.h"
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
#include "games/iidx/camera.h"
|
#include "games/iidx/camera.h"
|
||||||
#endif
|
#endif
|
||||||
#include "games/iidx/poke.h"
|
|
||||||
#include "games/jb/jb.h"
|
#include "games/jb/jb.h"
|
||||||
#include "games/mga/mga.h"
|
#include "games/mga/mga.h"
|
||||||
#include "games/nost/nost.h"
|
#include "games/nost/nost.h"
|
||||||
@@ -88,13 +88,13 @@
|
|||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "launcher/logger.h"
|
#include "launcher/logger.h"
|
||||||
#include "launcher/signal.h"
|
#include "launcher/signal.h"
|
||||||
#include "launcher/superexit.h"
|
|
||||||
#include "launcher/richpresence.h"
|
#include "launcher/richpresence.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "launcher/options.h"
|
#include "launcher/options.h"
|
||||||
#include "misc/bt5api.h"
|
#include "misc/bt5api.h"
|
||||||
#include "misc/device.h"
|
#include "misc/device.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "misc/hotkeys.h"
|
||||||
#include "misc/extdev.h"
|
#include "misc/extdev.h"
|
||||||
#include "misc/ami2000.h"
|
#include "misc/ami2000.h"
|
||||||
#include "misc/sciunit.h"
|
#include "misc/sciunit.h"
|
||||||
@@ -151,6 +151,7 @@ std::string CARD_OVERRIDES[2];
|
|||||||
|
|
||||||
// sub-systems
|
// sub-systems
|
||||||
std::unique_ptr<api::Controller> API_CONTROLLER;
|
std::unique_ptr<api::Controller> API_CONTROLLER;
|
||||||
|
std::unique_ptr<api::StreamServer> API_STREAM_SERVER;
|
||||||
std::unique_ptr<rawinput::RawInputManager> RI_MGR;
|
std::unique_ptr<rawinput::RawInputManager> RI_MGR;
|
||||||
|
|
||||||
// trigger NVIDIA Optimus & AMD Enduro High Performance Graphics
|
// trigger NVIDIA Optimus & AMD Enduro High Performance Graphics
|
||||||
@@ -199,6 +200,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
bool api_pretty = false;
|
bool api_pretty = false;
|
||||||
bool api_debug = false;
|
bool api_debug = false;
|
||||||
unsigned short api_port = 1337;
|
unsigned short api_port = 1337;
|
||||||
|
bool api_stream_enable = false;
|
||||||
std::string api_pass = "";
|
std::string api_pass = "";
|
||||||
std::vector<std::string> api_serial_port;
|
std::vector<std::string> api_serial_port;
|
||||||
std::vector<DWORD> api_serial_baud;
|
std::vector<DWORD> api_serial_baud;
|
||||||
@@ -524,22 +526,25 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
games::iidx::FLIP_CAMS = true;
|
games::iidx::FLIP_CAMS = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IIDX CONNECT_CAMERA logic here for cases where user is running without -iidx module
|
// Resolve the IIDX camera policy here so it also applies without the -iidx module.
|
||||||
// for now, we assume that user may be running on a cab
|
const auto &cab_camera_access = options[launcher::Options::IIDXCabCamAccess];
|
||||||
// (games::iidx::DISABLE_CAMS starts out as false unless user overrides)
|
if (cab_camera_access.is_active()) {
|
||||||
// we will check again in IIDX module with a different default
|
const auto value = cab_camera_access.value_text();
|
||||||
assert(!games::iidx::DISABLE_CAMS.has_value());
|
if (value == "off") {
|
||||||
if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
|
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
|
||||||
games::iidx::DISABLE_CAMS = true;
|
} else if (value == "on") {
|
||||||
|
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::on;
|
||||||
|
} else if (value == "legacy") {
|
||||||
|
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::legacy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::IIDXCabCamAccess].is_active() &&
|
|
||||||
options[launcher::Options::IIDXCabCamAccess].value_text() == "off") {
|
if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
|
||||||
games::iidx::DISABLE_CAMS = true;
|
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::IIDXCamHook].value_bool()) {
|
if (options[launcher::Options::IIDXCamHook].value_bool()) {
|
||||||
games::iidx::TDJ_CAMERA = true;
|
games::iidx::TDJ_CAMERA = true;
|
||||||
// Disable legacy behaviour to avoid conflict
|
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
|
||||||
games::iidx::DISABLE_CAMS = true;
|
|
||||||
}
|
}
|
||||||
// CONNECT_CAMERA env var will be set once logging is enabled
|
// CONNECT_CAMERA env var will be set once logging is enabled
|
||||||
|
|
||||||
@@ -644,6 +649,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::LoadGitaDoraModule].value_bool()) {
|
if (options[launcher::Options::LoadGitaDoraModule].value_bool()) {
|
||||||
attach_gitadora = true;
|
attach_gitadora = true;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraDisableFrameLimiter].value_bool()) {
|
||||||
|
games::gitadora::DISABLE_FRAME_LIMITER = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
|
if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
|
||||||
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
|
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
|
||||||
}
|
}
|
||||||
@@ -1027,6 +1035,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::APIScreenMirrorDivide].is_active()) {
|
if (options[launcher::Options::APIScreenMirrorDivide].is_active()) {
|
||||||
api::modules::CAPTURE_DIVIDE = options[launcher::Options::APIScreenMirrorDivide].value_uint32();
|
api::modules::CAPTURE_DIVIDE = options[launcher::Options::APIScreenMirrorDivide].value_uint32();
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::APIStreamEnable].value_bool() && !cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
api_stream_enable = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (options[launcher::Options::DisableDebugHooks].value_bool()) {
|
if (options[launcher::Options::DisableDebugHooks].value_bool()) {
|
||||||
debughook::DEBUGHOOK_LOGGING = false;
|
debughook::DEBUGHOOK_LOGGING = false;
|
||||||
@@ -1098,6 +1109,12 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::ScreenshotFolder].is_active()) {
|
if (options[launcher::Options::ScreenshotFolder].is_active()) {
|
||||||
GRAPHICS_SCREENSHOT_DIR = options[launcher::Options::ScreenshotFolder].value_text();
|
GRAPHICS_SCREENSHOT_DIR = options[launcher::Options::ScreenshotFolder].value_text();
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::ScreenshotIncludeOverlay].value_bool()) {
|
||||||
|
GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = true;
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::ScreenshotSubscreens].value_bool()) {
|
||||||
|
GRAPHICS_SCREENSHOT_SUBSCREENS = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::DisableColoredOutput].value_bool()) {
|
if (options[launcher::Options::DisableColoredOutput].value_bool()) {
|
||||||
logger::COLOR = false;
|
logger::COLOR = false;
|
||||||
}
|
}
|
||||||
@@ -1672,6 +1689,26 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options[launcher::Options::PathToModules].is_active() && !cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
log_warning(
|
||||||
|
"launcher",
|
||||||
|
"WARNING - user specified -modules option\n\n\n"
|
||||||
|
"!!! !!!\n"
|
||||||
|
"!!! Using -modules changes which game DLLs get loaded! !!!\n"
|
||||||
|
"!!! Unless you know exactly what you are doing, clear -modules !!!\n"
|
||||||
|
"!!! and try again; usually this is accidentally set by users !!!\n"
|
||||||
|
"!!! without understanding the implications. !!!\n"
|
||||||
|
"!!! !!!\n"
|
||||||
|
);
|
||||||
|
deferredlogs::defer_error_messages({
|
||||||
|
"-modules option specified by user",
|
||||||
|
" game DLLs and patches are loaded from that folder instead of the spice folder,",
|
||||||
|
" and it is also prepended to the DLL search path, so dependencies may resolve to",
|
||||||
|
" unexpected copies; instead, clear -modules option and place spice binaries in",
|
||||||
|
" the intended game directory",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (launcher::signal::DISABLE && !cfg::CONFIGURATOR_STANDALONE) {
|
if (launcher::signal::DISABLE && !cfg::CONFIGURATOR_STANDALONE) {
|
||||||
log_warning(
|
log_warning(
|
||||||
"launcher",
|
"launcher",
|
||||||
@@ -1702,31 +1739,25 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SDVXFullscreenLandscape disabled due to it messing with in-game camera angle
|
if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !GRAPHICS_WINDOWED) {
|
||||||
// FullscreenOrientationFlip continues to live on, however.
|
#if SPICE64
|
||||||
if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !cfg::CONFIGURATOR_STANDALONE) {
|
GRAPHICS_FS_ORIENTATION_SWAP = true;
|
||||||
log_fatal("launcher", "-sdvxlandscape removed due to a camera bug in SDVX!");
|
#else
|
||||||
|
log_warning("launcher", "-sdvxlandscape is not supported in 32-bit SDVX, ignoring...");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
// if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !GRAPHICS_WINDOWED) {
|
|
||||||
// #if SPICE64
|
|
||||||
// GRAPHICS_FS_ORIENTATION_SWAP = true;
|
|
||||||
// #else
|
|
||||||
// log_warning("launcher", "-sdvxlandscape is not supported in 32-bit SDVX, ignoring...");
|
|
||||||
// #endif
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (options[launcher::Options::FullscreenOrientationFlip].value_bool() && !GRAPHICS_WINDOWED) {
|
if (options[launcher::Options::FullscreenOrientationFlip].value_bool() && !GRAPHICS_WINDOWED) {
|
||||||
GRAPHICS_FS_ORIENTATION_SWAP = true;
|
GRAPHICS_FS_ORIENTATION_SWAP = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply an explicit off outside of the -iidx module so it also works on cabinets.
|
||||||
// for cab usage - set environment variables (outside of -iidx module)
|
if (games::iidx::CAB_CAMERA_ACCESS == games::iidx::cab_camera_access_mode::off &&
|
||||||
if (games::iidx::DISABLE_CAMS.has_value() &&
|
|
||||||
games::iidx::DISABLE_CAMS.value() &&
|
|
||||||
!cfg::CONFIGURATOR_STANDALONE) {
|
!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0");
|
log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0");
|
||||||
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() &&
|
if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() &&
|
||||||
games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" &&
|
games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" &&
|
||||||
!cfg::CONFIGURATOR_STANDALONE) {
|
!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
@@ -1778,12 +1809,11 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
nvapi::initialize();
|
nvapi::initialize();
|
||||||
// add application profile to nvcp
|
// add application profile to nvcp
|
||||||
nvapi::set_profile_settings();
|
nvapi::set_profile_settings();
|
||||||
// enable super exit
|
// keep ALT+F4 available during lengthy non-standalone boot
|
||||||
superexit::enable();
|
hotkeys::start();
|
||||||
|
|
||||||
// enable subscreen touch emulation
|
// enable subscreen touch emulation
|
||||||
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
|
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
|
||||||
games::iidx::poke::enable();
|
games::iidx::ENABLE_POKE = true;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::NostalgiaPoke].is_active()) {
|
if (options[launcher::Options::NostalgiaPoke].is_active()) {
|
||||||
games::nost::ENABLE_POKE = true;
|
games::nost::ENABLE_POKE = true;
|
||||||
@@ -1855,6 +1885,13 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (check_dll("jubeat2019.dll")) {
|
||||||
|
avs::game::DLL_NAME = "jubeat2019.dll";
|
||||||
|
attach_io = true;
|
||||||
|
attach_jb = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// RB
|
// RB
|
||||||
if (check_dll("reflecbeat.dll")) {
|
if (check_dll("reflecbeat.dll")) {
|
||||||
avs::game::DLL_NAME = "reflecbeat.dll";
|
avs::game::DLL_NAME = "reflecbeat.dll";
|
||||||
@@ -2283,7 +2320,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
games.push_back(new games::sdvx::SDVXGame());
|
games.push_back(new games::sdvx::SDVXGame());
|
||||||
}
|
}
|
||||||
if (attach_jb) {
|
if (attach_jb) {
|
||||||
avs::core::set_default_heap_size("jubeat.dll");
|
avs::core::set_default_heap_size(avs::game::DLL_NAME);
|
||||||
games.push_back(new games::jb::JBGame());
|
games.push_back(new games::jb::JBGame());
|
||||||
}
|
}
|
||||||
if (attach_nostalgia) {
|
if (attach_nostalgia) {
|
||||||
@@ -2446,6 +2483,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
|
|
||||||
// initialize raw input
|
// initialize raw input
|
||||||
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
||||||
|
hotkeys::enable_raw_input();
|
||||||
for (const auto &device : sextet_devices) {
|
for (const auto &device : sextet_devices) {
|
||||||
RI_MGR->sextet_register(device);
|
RI_MGR->sextet_register(device);
|
||||||
}
|
}
|
||||||
@@ -2468,6 +2506,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
log_misc("rawinput", "Analog mappings:");
|
log_misc("rawinput", "Analog mappings:");
|
||||||
dump_analog_bindings();
|
dump_analog_bindings();
|
||||||
|
|
||||||
|
// mappings are ready; begin screenshot and coin polling during late startup
|
||||||
|
hotkeys::enable_input();
|
||||||
|
|
||||||
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
|
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
|
||||||
if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) {
|
if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) {
|
||||||
GRAPHICS_SHOW_CURSOR = true;
|
GRAPHICS_SHOW_CURSOR = true;
|
||||||
@@ -2696,9 +2737,20 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
for (size_t i = 0; i < std::min(api_serial_port.size(), api_serial_baud.size()); i++) {
|
for (size_t i = 0; i < std::min(api_serial_port.size(), api_serial_baud.size()); i++) {
|
||||||
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
|
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
|
||||||
}
|
}
|
||||||
|
// the websocket already sits on the API port plus one, so the stream takes plus two
|
||||||
// start coin input thread
|
if (api_stream_enable) {
|
||||||
eamuse_coin_start_thread();
|
if (!api_enable) {
|
||||||
|
log_fatal("launcher", "video stream requires API port to be set (-api)");
|
||||||
|
} else if (api_port + 2 > 65535) {
|
||||||
|
log_fatal(
|
||||||
|
"launcher",
|
||||||
|
"ignoring the video stream, API port {} leaves no room for port plus two",
|
||||||
|
api_port);
|
||||||
|
} else {
|
||||||
|
API_STREAM_SERVER = std::make_unique<api::StreamServer>(
|
||||||
|
static_cast<unsigned short>(api_port + 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// pin macro
|
// pin macro
|
||||||
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
|
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
|
||||||
@@ -2749,6 +2801,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
log_info("launcher", "calling game entry");
|
log_info("launcher", "calling game entry");
|
||||||
avs::game::entry_main();
|
avs::game::entry_main();
|
||||||
|
|
||||||
|
// stop screenshot and coin polling; mapped SuperExit and ALT+F4 remain active
|
||||||
|
hotkeys::disable_input();
|
||||||
|
|
||||||
// clear presence
|
// clear presence
|
||||||
richpresence::shutdown();
|
richpresence::shutdown();
|
||||||
|
|
||||||
@@ -2784,11 +2839,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// free api controller
|
// free api controller
|
||||||
|
API_STREAM_SERVER.reset();
|
||||||
API_CONTROLLER.reset();
|
API_CONTROLLER.reset();
|
||||||
|
|
||||||
// stop coin input thread
|
|
||||||
eamuse_coin_stop_thread();
|
|
||||||
|
|
||||||
eamuse_pin_macro_stop_thread();
|
eamuse_pin_macro_stop_thread();
|
||||||
|
|
||||||
// BT5API
|
// BT5API
|
||||||
@@ -2799,6 +2852,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
sdk::fini_sdk_modules();
|
sdk::fini_sdk_modules();
|
||||||
|
|
||||||
// stop raw input
|
// stop raw input
|
||||||
|
hotkeys::disable_raw_input();
|
||||||
RI_MGR.reset();
|
RI_MGR.reset();
|
||||||
|
|
||||||
// debug hook
|
// debug hook
|
||||||
@@ -2831,8 +2885,8 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
// dispose crypt
|
// dispose crypt
|
||||||
crypt::dispose();
|
crypt::dispose();
|
||||||
|
|
||||||
// disable super exit
|
// end early/late ALT+F4 monitoring at the same teardown point as legacy SuperExit
|
||||||
superexit::disable();
|
hotkeys::stop();
|
||||||
|
|
||||||
// disable poke
|
// disable poke
|
||||||
games::iidx::poke::disable();
|
games::iidx::poke::disable();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -25,13 +26,16 @@ namespace logger {
|
|||||||
bool COLOR = true;
|
bool COLOR = true;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
static bool RUNNING = false;
|
static std::atomic<bool> RUNNING = false;
|
||||||
static WORD DEFAULT_ATTRIBUTES = 0;
|
static WORD DEFAULT_ATTRIBUTES = 0;
|
||||||
static std::mutex EVENT_MUTEX;
|
static std::mutex EVENT_MUTEX;
|
||||||
static std::condition_variable EVENT_CV;
|
static std::condition_variable EVENT_CV;
|
||||||
static std::thread *THREAD = nullptr;
|
static std::thread *THREAD = nullptr;
|
||||||
|
static HANDLE THREAD_FINISHED = nullptr;
|
||||||
|
static std::atomic<bool> THREAD_ABANDONED = false;
|
||||||
static std::mutex OUTPUT_MUTEX;
|
static std::mutex OUTPUT_MUTEX;
|
||||||
static bool OUTPUT_BUFFER_HOT = false;
|
static std::mutex FLUSH_MUTEX;
|
||||||
|
static std::atomic<bool> OUTPUT_BUFFER_HOT = false;
|
||||||
static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER1;
|
static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER1;
|
||||||
static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER2;
|
static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER2;
|
||||||
static std::vector<std::pair<std::string, Style>> *OUTPUT_BUFFER = &OUTPUT_BUFFER1;
|
static std::vector<std::pair<std::string, Style>> *OUTPUT_BUFFER = &OUTPUT_BUFFER1;
|
||||||
@@ -71,7 +75,9 @@ namespace logger {
|
|||||||
SetConsoleTextAttribute(hTerminal, info.wAttributes);
|
SetConsoleTextAttribute(hTerminal, info.wAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void output_buffer_flush() {
|
// the buffer is swapped under OUTPUT_MUTEX but drained outside of it, so two concurrent
|
||||||
|
// drains would leave one of them iterating a buffer that push() has started appending to
|
||||||
|
static void output_buffer_flush_locked() {
|
||||||
|
|
||||||
// get buffer and swap
|
// get buffer and swap
|
||||||
auto buffer = output_buffer_swap();
|
auto buffer = output_buffer_swap();
|
||||||
@@ -142,6 +148,23 @@ namespace logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void output_buffer_flush() {
|
||||||
|
|
||||||
|
// a detached logging thread can hold FLUSH_MUTEX forever, so never wait on it
|
||||||
|
if (THREAD_ABANDONED) {
|
||||||
|
std::unique_lock<std::mutex> guard(FLUSH_MUTEX, std::try_to_lock);
|
||||||
|
if (guard.owns_lock()) {
|
||||||
|
output_buffer_flush_locked();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> guard(FLUSH_MUTEX);
|
||||||
|
|
||||||
|
output_buffer_flush_locked();
|
||||||
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
|
|
||||||
// don't start if blocking
|
// don't start if blocking
|
||||||
@@ -151,6 +174,7 @@ namespace logger {
|
|||||||
|
|
||||||
// start logging thread
|
// start logging thread
|
||||||
RUNNING = true;
|
RUNNING = true;
|
||||||
|
THREAD_FINISHED = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
||||||
THREAD = new std::thread([] {
|
THREAD = new std::thread([] {
|
||||||
std::unique_lock<std::mutex> lock(EVENT_MUTEX);
|
std::unique_lock<std::mutex> lock(EVENT_MUTEX);
|
||||||
|
|
||||||
@@ -160,7 +184,7 @@ namespace logger {
|
|||||||
while (RUNNING) {
|
while (RUNNING) {
|
||||||
|
|
||||||
// wait for hot buffer
|
// wait for hot buffer
|
||||||
EVENT_CV.wait(lock, [] { return OUTPUT_BUFFER_HOT; });
|
EVENT_CV.wait(lock, [] { return OUTPUT_BUFFER_HOT.load(); });
|
||||||
OUTPUT_BUFFER_HOT = false;
|
OUTPUT_BUFFER_HOT = false;
|
||||||
|
|
||||||
// flush buffer
|
// flush buffer
|
||||||
@@ -180,22 +204,51 @@ namespace logger {
|
|||||||
HANDLE hTerminal = GetStdHandle(STD_OUTPUT_HANDLE);
|
HANDLE hTerminal = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
SetConsoleTextAttribute(hTerminal, DEFAULT_ATTRIBUTES);
|
SetConsoleTextAttribute(hTerminal, DEFAULT_ATTRIBUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (THREAD_FINISHED) {
|
||||||
|
SetEvent(THREAD_FINISHED);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
log_info("logger", "stop");
|
|
||||||
|
// NOTE: don't log to the logger here!
|
||||||
|
|
||||||
|
RUNNING = false;
|
||||||
|
|
||||||
// clean up thread if required
|
// clean up thread if required
|
||||||
RUNNING = false;
|
|
||||||
if (THREAD) {
|
if (THREAD) {
|
||||||
|
|
||||||
// fake notify to exit wait loop
|
// fake notify to exit wait loop
|
||||||
OUTPUT_BUFFER_HOT = true;
|
OUTPUT_BUFFER_HOT = true;
|
||||||
EVENT_CV.notify_all();
|
EVENT_CV.notify_all();
|
||||||
|
|
||||||
// join and clean up
|
// never block forever - this also runs on the fatal/crash path, where the logging
|
||||||
THREAD->join();
|
// thread may be suspended or wedged and would take the whole process down with it
|
||||||
|
const bool finished = THREAD_FINISHED != nullptr &&
|
||||||
|
WaitForSingleObject(THREAD_FINISHED, 1000) == WAIT_OBJECT_0;
|
||||||
|
|
||||||
|
if (finished) {
|
||||||
|
THREAD->join();
|
||||||
|
|
||||||
|
CloseHandle(THREAD_FINISHED);
|
||||||
|
THREAD_FINISHED = nullptr;
|
||||||
|
} else {
|
||||||
|
THREAD->detach();
|
||||||
|
THREAD_ABANDONED = true;
|
||||||
|
|
||||||
|
// THREAD_FINISHED is leaked on purpose: the detached thread can still wake up and
|
||||||
|
// signal it, and closing it here risks signaling an unrelated recycled handle
|
||||||
|
|
||||||
|
// write out whatever the logging thread never got to
|
||||||
|
output_buffer_flush();
|
||||||
|
|
||||||
|
if (LOG_FILE && LOG_FILE != INVALID_HANDLE_VALUE) {
|
||||||
|
FlushFileBuffers(LOG_FILE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delete THREAD;
|
delete THREAD;
|
||||||
THREAD = nullptr;
|
THREAD = nullptr;
|
||||||
}
|
}
|
||||||
@@ -229,17 +282,14 @@ namespace logger {
|
|||||||
// check if blocking or the logging thread is not running
|
// check if blocking or the logging thread is not running
|
||||||
if (BLOCKING || !RUNNING) {
|
if (BLOCKING || !RUNNING) {
|
||||||
|
|
||||||
// blocking guard
|
|
||||||
static std::mutex blocking_lock;
|
|
||||||
std::lock_guard<std::mutex> blocking_guard(blocking_lock);
|
|
||||||
|
|
||||||
// immediately process logs
|
// immediately process logs
|
||||||
output_buffer_flush();
|
output_buffer_flush();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// mark buffer as hot
|
// never block here - the logging thread can be suspended while holding EVENT_MUTEX,
|
||||||
std::unique_lock<std::mutex> lock(EVENT_MUTEX);
|
// and it re-checks OUTPUT_BUFFER_HOT before waiting again
|
||||||
|
std::unique_lock<std::mutex> lock(EVENT_MUTEX, std::try_to_lock);
|
||||||
OUTPUT_BUFFER_HOT = true;
|
OUTPUT_BUFFER_HOT = true;
|
||||||
EVENT_CV.notify_one();
|
EVENT_CV.notify_one();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,9 +330,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc =
|
.desc =
|
||||||
"Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
|
"Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
|
||||||
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
|
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
|
||||||
"Strongly consider combining this with -forceres option to render at monitor native resolution\n\n"
|
"Strongly consider combining this with -forceres option to render at monitor native resolution.\n\n"
|
||||||
"WARNING: for SDVX, this messes with camera angle for note lanes, causing it to be zoomed out, and causes performance issues "
|
"For SDVX, use the dedicated -sdvxlandscape option.",
|
||||||
"with Live2D!",
|
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.hidden = true,
|
.hidden = true,
|
||||||
.category = "Full Screen Settings"
|
.category = "Full Screen Settings"
|
||||||
@@ -641,17 +640,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// IIDXCabCamAccess
|
// IIDXCabCamAccess
|
||||||
.title = "IIDX Use Official AC Cams",
|
.title = "IIDX Official AC Camera Access",
|
||||||
.name = "iidxcabcams",
|
.name = "iidxcabcams",
|
||||||
.desc = "For IIDX25+, allow direct access to cameras from real arcade cabinets. "
|
.desc = "Controls how the game accesses USB cameras for IIDX 25+.\n\n"
|
||||||
"Only turn this on if you have OFFICIAL arcade cameras connected to the correct USB ports. Default: auto.",
|
"auto (default): use [off] when the IIDX module is enabled; otherwise, use [on].\n\n"
|
||||||
|
"on: game discovers and directly accesses cameras; requires official cameras on correct USB ports.\n\n"
|
||||||
|
"legacy: for IIDX 25/26 only; allow camera access with emulated discovery.\n\n"
|
||||||
|
"off: prevent game from accessing USB cameras.",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
.game_name = "Beatmania IIDX",
|
.game_name = "Beatmania IIDX",
|
||||||
.category = "Cab Peripherals",
|
.category = "Cab Peripherals",
|
||||||
.elements = {
|
.elements = {
|
||||||
{"auto", ""},
|
{"auto", ""},
|
||||||
|
{"on", ""},
|
||||||
|
{"legacy", ""},
|
||||||
{"off", ""},
|
{"off", ""},
|
||||||
{"on", ""}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -978,7 +981,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.display_name = "sdvxsubredraw",
|
.display_name = "sdvxsubredraw",
|
||||||
.aliases= "sdvxsubredraw",
|
.aliases= "sdvxsubredraw",
|
||||||
.desc = "Check if submonitor in fullscreen mode doesn't update every frame; "
|
.desc = "Check if submonitor in fullscreen mode doesn't update every frame; "
|
||||||
"this option forces subscreen to redraw every frame.",
|
"this option presents the subscreen when the game does not.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Sound Voltex",
|
.game_name = "Sound Voltex",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
@@ -1172,7 +1175,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
|
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
|
||||||
.name = "popnsubredraw",
|
.name = "popnsubredraw",
|
||||||
.desc = "Check if submonitor in fullscreen mode appears stuck; "
|
.desc = "Check if submonitor in fullscreen mode appears stuck; "
|
||||||
"this option forces subscreen to redraw every frame.",
|
"this option presents the subscreen when the game does not.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Pop'n Music",
|
.game_name = "Pop'n Music",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
@@ -1203,6 +1206,19 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.quick_setting_category = "Game",
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.title = "GitaDora Disable Frame Limiter (EXPERIMENTAL)",
|
||||||
|
.name = "gdnoframelimiter",
|
||||||
|
.desc = "Only for GITADORA to GALAXY WAVE; not for XG series or Arena Model.\n\n"
|
||||||
|
"Game engine's built-in frame limiter can result in unstable frame pacing "
|
||||||
|
"(around 56-58 FPS) on modern Windows. Turning this on disables that and "
|
||||||
|
"forces the game to rely on V-Sync instead, resulting in a stable FPS.\n\n"
|
||||||
|
"Ensure your monitors are set to 60Hz.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.title = "GitaDora Cabinet Type",
|
.title = "GitaDora Cabinet Type",
|
||||||
.name = "gdcabtype",
|
.name = "gdcabtype",
|
||||||
@@ -1299,7 +1315,9 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.name = "gdalayout",
|
.name = "gdalayout",
|
||||||
.desc = "For Arena Model: select how many windows to create.\n\n"
|
.desc = "For Arena Model: select how many windows to create.\n\n"
|
||||||
"1 window: main only; use the subscreen overlay for SMALL.\n\n"
|
"1 window: main only; use the subscreen overlay for SMALL.\n\n"
|
||||||
"2 windows: main + SMALL. Note: currently only works for windowed mode, broken for fullscreen.\n\n"
|
"2 windows: main + SMALL. Without -w, this uses native D3D9 adapter-group fullscreen "
|
||||||
|
"with physical MAIN/SMALL and virtual LEFT/RIGHT. It requires two active displays on "
|
||||||
|
"one adapter group. With -w, it uses borderless windows instead.\n\n"
|
||||||
"4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the "
|
"4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the "
|
||||||
"right resolution; see wiki for details.",
|
"right resolution; see wiki for details.",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
@@ -1725,6 +1743,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.setting_name = "1",
|
.setting_name = "1",
|
||||||
.category = "Companion & API",
|
.category = "Companion & API",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// APIStreamEnable
|
||||||
|
.title = "API Video Stream Server Enable",
|
||||||
|
.name = "apistream",
|
||||||
|
.desc = "Serves the mirrored screen as a video stream, on the API port plus two; "
|
||||||
|
"alternative to API screen capture. Requires -api.\n\n"
|
||||||
|
"http://host:apiport+2/stream.mjpg - MJPEG\n\n"
|
||||||
|
"http://host:apiport+2/stream.h264 - H.264\n\n"
|
||||||
|
"Parameters: screen (0-3), fps (1-60, default 30), q (1-100, default 70).\n\n"
|
||||||
|
"Example with -api 1337: http://host:1339/stream.h264?fps=30&q=70\n\n"
|
||||||
|
"VIEW ONLY - touch input still requires -api. "
|
||||||
|
"No password protection or encryption of any kind; video sent in the clear!",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "Companion & API",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.title = "Enable All IO Modules",
|
.title = "Enable All IO Modules",
|
||||||
.name = "io",
|
.name = "io",
|
||||||
@@ -1791,10 +1824,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Force Touch Emulation",
|
.title = "Force Legacy Touch Emulation",
|
||||||
.name = "touchemuforce",
|
.name = "touchemuforce",
|
||||||
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
|
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
|
||||||
"This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.",
|
"Do not enable this unless you have trouble with using a mouse to emulate touch input.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
@@ -2165,7 +2198,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{"25", "+25 dB"},
|
{"25", "+25 dB"},
|
||||||
{"30", "+30 dB"},
|
{"30", "+30 dB"},
|
||||||
},
|
},
|
||||||
.quick_setting_category = "Audio",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// AudioResample
|
// AudioResample
|
||||||
@@ -3012,17 +3044,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// SDVXFullscreenLandscape
|
// SDVXFullscreenLandscape
|
||||||
.title = "(DISABLED) SDVX Landscape Mode (SDVX5+)",
|
.title = "SDVX Full Screen Landscape Mode (SDVX5+, EXPERIMENTAL)",
|
||||||
.name = "sdvxlandscape",
|
.name = "sdvxlandscape",
|
||||||
.desc =
|
.desc =
|
||||||
"Option HIDDEN and DISABLED due to it affecting in-game camera angle for displaying lanes.\n\n"
|
|
||||||
"Allows you to play in landscape by transposing resolution and applying image scaling.\n\n"
|
"Allows you to play in landscape by transposing resolution and applying image scaling.\n\n"
|
||||||
"Works only for SDVX5 and above! This is identical to -forceresswap.\n\n"
|
"Corrects the in-game perspective camera for the transposed display aspect ratio.\n\n"
|
||||||
|
"Works only for SDVX5 and above.\n\n"
|
||||||
"Will launch at 1080p by default; strongly consider combining this with -forceres option to render at monitor native resolution.",
|
"Will launch at 1080p by default; strongly consider combining this with -forceres option to render at monitor native resolution.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.hidden = true,
|
|
||||||
.game_name = "Sound Voltex",
|
.game_name = "Sound Voltex",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// spice2x_EnableSMXStage
|
// spice2x_EnableSMXStage
|
||||||
@@ -3380,6 +3412,22 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "OBS Control",
|
.category = "OBS Control",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// ScreenshotIncludeOverlay
|
||||||
|
.title = "Include Overlay in Screenshots",
|
||||||
|
.name = "screenshotoverlay",
|
||||||
|
.desc = "Includes Spice overlay in screenshots.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "General Overlay",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// ScreenshotSubscreens
|
||||||
|
.title = "Include Subscreens in Screenshots",
|
||||||
|
.name = "screenshotsub",
|
||||||
|
.desc = "Saves each subscreen as a separate PNG alongside the primary screenshot.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "General Overlay",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ namespace launcher {
|
|||||||
LoadHelloPopnMusicModule,
|
LoadHelloPopnMusicModule,
|
||||||
LoadGitaDoraModule,
|
LoadGitaDoraModule,
|
||||||
GitaDoraTwoChannelAudio,
|
GitaDoraTwoChannelAudio,
|
||||||
|
GitaDoraDisableFrameLimiter,
|
||||||
GitaDoraCabinetType,
|
GitaDoraCabinetType,
|
||||||
GitaDoraLefty,
|
GitaDoraLefty,
|
||||||
GitaDoraWailHold,
|
GitaDoraWailHold,
|
||||||
@@ -169,6 +170,7 @@ namespace launcher {
|
|||||||
APIDebugMode,
|
APIDebugMode,
|
||||||
APIScreenMirrorQuality,
|
APIScreenMirrorQuality,
|
||||||
APIScreenMirrorDivide,
|
APIScreenMirrorDivide,
|
||||||
|
APIStreamEnable,
|
||||||
EnableAllIOModules,
|
EnableAllIOModules,
|
||||||
EnableACIOModule,
|
EnableACIOModule,
|
||||||
EnableICCAModule,
|
EnableICCAModule,
|
||||||
@@ -319,7 +321,9 @@ namespace launcher {
|
|||||||
OBSWebSocketHost,
|
OBSWebSocketHost,
|
||||||
OBSWebSocketPort,
|
OBSWebSocketPort,
|
||||||
OBSWebSocketPassword,
|
OBSWebSocketPassword,
|
||||||
OBSWebSocketDebug
|
OBSWebSocketDebug,
|
||||||
|
ScreenshotIncludeOverlay,
|
||||||
|
ScreenshotSubscreens
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class OptionsCategory {
|
enum class OptionsCategory {
|
||||||
|
|||||||
+401
-45
@@ -1,5 +1,6 @@
|
|||||||
#include "signal.h"
|
#include "signal.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <future>
|
#include <future>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static decltype(MiniDumpWriteDump) *MiniDumpWriteDump_local = nullptr;
|
static decltype(MiniDumpWriteDump) *MiniDumpWriteDump_local = nullptr;
|
||||||
|
static LONG EXCEPTION_HANDLER_ACTIVE = 0;
|
||||||
|
|
||||||
namespace launcher::signal {
|
namespace launcher::signal {
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ static std::string control_code(DWORD dwCtrlType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
|
static std::string exception_code(const struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||||
switch (ExceptionRecord->ExceptionCode) {
|
switch (ExceptionRecord->ExceptionCode) {
|
||||||
V(EXCEPTION_ACCESS_VIOLATION);
|
V(EXCEPTION_ACCESS_VIOLATION);
|
||||||
V(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
|
V(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
|
||||||
@@ -77,6 +79,388 @@ static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
|
|||||||
|
|
||||||
#undef V
|
#undef V
|
||||||
|
|
||||||
|
static const char *access_operation(ULONG_PTR operation) {
|
||||||
|
switch (operation) {
|
||||||
|
case 0:
|
||||||
|
return "read";
|
||||||
|
case 1:
|
||||||
|
return "write";
|
||||||
|
case 8:
|
||||||
|
return "execute";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *memory_state(DWORD state) {
|
||||||
|
switch (state) {
|
||||||
|
case MEM_COMMIT:
|
||||||
|
return "committed";
|
||||||
|
case MEM_FREE:
|
||||||
|
return "free";
|
||||||
|
case MEM_RESERVE:
|
||||||
|
return "reserved";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *memory_type(DWORD type) {
|
||||||
|
switch (type) {
|
||||||
|
case MEM_IMAGE:
|
||||||
|
return "image";
|
||||||
|
case MEM_MAPPED:
|
||||||
|
return "mapped";
|
||||||
|
case MEM_PRIVATE:
|
||||||
|
return "private";
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *memory_protection(DWORD protection) {
|
||||||
|
switch (protection & 0xff) {
|
||||||
|
case PAGE_EXECUTE:
|
||||||
|
return "execute";
|
||||||
|
case PAGE_EXECUTE_READ:
|
||||||
|
return "execute-read";
|
||||||
|
case PAGE_EXECUTE_READWRITE:
|
||||||
|
return "execute-read-write";
|
||||||
|
case PAGE_EXECUTE_WRITECOPY:
|
||||||
|
return "execute-write-copy";
|
||||||
|
case PAGE_NOACCESS:
|
||||||
|
return "no-access";
|
||||||
|
case PAGE_READONLY:
|
||||||
|
return "read-only";
|
||||||
|
case PAGE_READWRITE:
|
||||||
|
return "read-write";
|
||||||
|
case PAGE_WRITECOPY:
|
||||||
|
return "write-copy";
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_exception_parameters(const struct _EXCEPTION_RECORD *record) {
|
||||||
|
const auto parameter_count = record->NumberParameters <= EXCEPTION_MAXIMUM_PARAMETERS ?
|
||||||
|
record->NumberParameters : EXCEPTION_MAXIMUM_PARAMETERS;
|
||||||
|
|
||||||
|
log_warning("signal", "exception flags: 0x{:08x}, parameters: {}",
|
||||||
|
record->ExceptionFlags, parameter_count);
|
||||||
|
|
||||||
|
for (DWORD parameter = 0; parameter < parameter_count; parameter++) {
|
||||||
|
#ifdef _WIN64
|
||||||
|
log_warning("signal", "exception parameter[{}]: {:016x}",
|
||||||
|
parameter, record->ExceptionInformation[parameter]);
|
||||||
|
#else
|
||||||
|
log_warning("signal", "exception parameter[{}]: {:08x}",
|
||||||
|
parameter, record->ExceptionInformation[parameter]);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool query_memory(const void *address, MEMORY_BASIC_INFORMATION *memory) {
|
||||||
|
return VirtualQuery(address, memory, sizeof(*memory)) == sizeof(*memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_memory_information(const char *label, const MEMORY_BASIC_INFORMATION &memory) {
|
||||||
|
log_warning("signal",
|
||||||
|
"{}: base={}, size=0x{:x}, allocation_base={}, state={} (0x{:x}), "
|
||||||
|
"protect={} (0x{:x}), type={} (0x{:x})",
|
||||||
|
label,
|
||||||
|
fmt::ptr(memory.BaseAddress),
|
||||||
|
memory.RegionSize,
|
||||||
|
fmt::ptr(memory.AllocationBase),
|
||||||
|
memory_state(memory.State),
|
||||||
|
memory.State,
|
||||||
|
memory_protection(memory.Protect),
|
||||||
|
memory.Protect,
|
||||||
|
memory_type(memory.Type),
|
||||||
|
memory.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_memory_region(const char *label, const void *address) {
|
||||||
|
MEMORY_BASIC_INFORMATION memory {};
|
||||||
|
if (!query_memory(address, &memory)) {
|
||||||
|
log_warning("signal", "{}: VirtualQuery failed for {}: 0x{:08x}",
|
||||||
|
label, fmt::ptr(address), GetLastError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_memory_information(label, memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_adjacent_memory_regions(const void *address) {
|
||||||
|
MEMORY_BASIC_INFORMATION current {};
|
||||||
|
if (!query_memory(address, ¤t)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto current_base = reinterpret_cast<uintptr_t>(current.BaseAddress);
|
||||||
|
if (current_base > 0) {
|
||||||
|
MEMORY_BASIC_INFORMATION previous {};
|
||||||
|
if (query_memory(reinterpret_cast<const void *>(current_base - 1), &previous)) {
|
||||||
|
log_memory_information("fault memory previous", previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto current_end = current_base + current.RegionSize;
|
||||||
|
if (current_end > current_base) {
|
||||||
|
MEMORY_BASIC_INFORMATION next {};
|
||||||
|
if (query_memory(reinterpret_cast<const void *>(current_end), &next)) {
|
||||||
|
log_memory_information("fault memory next", next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_exception_module(const void *address) {
|
||||||
|
MEMORY_BASIC_INFORMATION memory {};
|
||||||
|
if (!query_memory(address, &memory) || memory.Type != MEM_IMAGE || memory.AllocationBase == nullptr) {
|
||||||
|
log_warning("signal", "exception module: unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char module_path[MAX_PATH] {};
|
||||||
|
const auto module = static_cast<HMODULE>(memory.AllocationBase);
|
||||||
|
const auto module_base = reinterpret_cast<uintptr_t>(memory.AllocationBase);
|
||||||
|
const auto exception_address = reinterpret_cast<uintptr_t>(address);
|
||||||
|
const auto module_length = GetModuleFileNameA(module, module_path, sizeof(module_path));
|
||||||
|
|
||||||
|
if (module_length > 0) {
|
||||||
|
const std::string module_name(module_path, std::min<size_t>(module_length, sizeof(module_path)));
|
||||||
|
log_warning("signal", "exception module: '{}' base={}, rva=0x{:x}",
|
||||||
|
module_name, fmt::ptr(memory.AllocationBase), exception_address - module_base);
|
||||||
|
} else {
|
||||||
|
log_warning("signal", "exception module: base={}, rva=0x{:x}",
|
||||||
|
fmt::ptr(memory.AllocationBase), exception_address - module_base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_instruction_bytes(const void *address) {
|
||||||
|
MEMORY_BASIC_INFORMATION memory {};
|
||||||
|
if (!query_memory(address, &memory) ||
|
||||||
|
memory.State != MEM_COMMIT ||
|
||||||
|
(memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0) {
|
||||||
|
log_warning("signal", "instruction bytes: unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t max_instruction_bytes = 16;
|
||||||
|
const auto region_end = reinterpret_cast<uintptr_t>(memory.BaseAddress) + memory.RegionSize;
|
||||||
|
const auto instruction_address = reinterpret_cast<uintptr_t>(address);
|
||||||
|
const auto available = region_end > instruction_address ? region_end - instruction_address : 0;
|
||||||
|
const auto byte_count = std::min(max_instruction_bytes, static_cast<size_t>(available));
|
||||||
|
uint8_t bytes[max_instruction_bytes] {};
|
||||||
|
SIZE_T bytes_read = 0;
|
||||||
|
if (byte_count == 0 ||
|
||||||
|
!ReadProcessMemory(GetCurrentProcess(), address, bytes, byte_count, &bytes_read)) {
|
||||||
|
log_warning("signal", "instruction bytes: read failed at {}: 0x{:08x}",
|
||||||
|
fmt::ptr(address), GetLastError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string byte_string;
|
||||||
|
byte_string.reserve(bytes_read * 3);
|
||||||
|
|
||||||
|
for (size_t index = 0; index < bytes_read; index++) {
|
||||||
|
fmt::format_to(std::back_inserter(byte_string), "{:02x}{}",
|
||||||
|
bytes[index], index + 1 < bytes_read ? " " : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning("signal", "instruction bytes: {}", byte_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_stack_context(const CONTEXT *context) {
|
||||||
|
const auto *tib = reinterpret_cast<const NT_TIB *>(NtCurrentTeb());
|
||||||
|
const auto stack_base = reinterpret_cast<uintptr_t>(tib->StackBase);
|
||||||
|
const auto stack_limit = reinterpret_cast<uintptr_t>(tib->StackLimit);
|
||||||
|
#ifdef _WIN64
|
||||||
|
const auto stack_pointer = static_cast<uintptr_t>(context->Rsp);
|
||||||
|
#else
|
||||||
|
const auto stack_pointer = static_cast<uintptr_t>(context->Esp);
|
||||||
|
#endif
|
||||||
|
const auto stack_used = stack_base > stack_pointer ? stack_base - stack_pointer : 0;
|
||||||
|
const auto stack_remaining = stack_pointer > stack_limit ? stack_pointer - stack_limit : 0;
|
||||||
|
|
||||||
|
log_warning("signal", "stack: base={}, limit={}, pointer={}, used=0x{:x}, remaining=0x{:x}",
|
||||||
|
fmt::ptr(tib->StackBase),
|
||||||
|
fmt::ptr(tib->StackLimit),
|
||||||
|
fmt::ptr(reinterpret_cast<const void *>(stack_pointer)),
|
||||||
|
stack_used,
|
||||||
|
stack_remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_exception_context(struct _EXCEPTION_POINTERS *ExceptionInfo, DWORD last_error) {
|
||||||
|
const auto *record = ExceptionInfo->ExceptionRecord;
|
||||||
|
const auto *context = ExceptionInfo->ContextRecord;
|
||||||
|
|
||||||
|
log_warning("signal", "thread id: {}", GetCurrentThreadId());
|
||||||
|
log_warning("signal", "thread last error (GetLastError()) at exception: 0x{:08x}", last_error);
|
||||||
|
log_warning("signal", "exception address: {}", fmt::ptr(record->ExceptionAddress));
|
||||||
|
log_exception_module(record->ExceptionAddress);
|
||||||
|
log_instruction_bytes(record->ExceptionAddress);
|
||||||
|
log_exception_parameters(record);
|
||||||
|
|
||||||
|
if ((record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION ||
|
||||||
|
record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) &&
|
||||||
|
record->NumberParameters >= 2) {
|
||||||
|
const auto operation = record->ExceptionInformation[0];
|
||||||
|
const auto address = reinterpret_cast<const void *>(record->ExceptionInformation[1]);
|
||||||
|
log_warning("signal", "invalid memory access: {} at {}",
|
||||||
|
access_operation(operation), fmt::ptr(address));
|
||||||
|
log_memory_region("fault memory", address);
|
||||||
|
log_adjacent_memory_regions(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR && record->NumberParameters >= 3) {
|
||||||
|
log_warning("signal", "in-page error status: 0x{:08x}",
|
||||||
|
static_cast<DWORD>(record->ExceptionInformation[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_stack_context(context);
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
log_warning("signal", "registers: rax={:016x} rbx={:016x} rcx={:016x} rdx={:016x}",
|
||||||
|
context->Rax, context->Rbx, context->Rcx, context->Rdx);
|
||||||
|
log_warning("signal", "registers: rsi={:016x} rdi={:016x} rbp={:016x} rsp={:016x}",
|
||||||
|
context->Rsi, context->Rdi, context->Rbp, context->Rsp);
|
||||||
|
log_warning("signal", "registers: r8={:016x} r9={:016x} r10={:016x} r11={:016x}",
|
||||||
|
context->R8, context->R9, context->R10, context->R11);
|
||||||
|
log_warning("signal", "registers: r12={:016x} r13={:016x} r14={:016x} r15={:016x}",
|
||||||
|
context->R12, context->R13, context->R14, context->R15);
|
||||||
|
log_warning("signal", "registers: rip={:016x} eflags={:08x}",
|
||||||
|
context->Rip, context->EFlags);
|
||||||
|
#else
|
||||||
|
log_warning("signal", "registers: eax={:08x} ebx={:08x} ecx={:08x} edx={:08x}",
|
||||||
|
context->Eax, context->Ebx, context->Ecx, context->Edx);
|
||||||
|
log_warning("signal", "registers: esi={:08x} edi={:08x} ebp={:08x} esp={:08x}",
|
||||||
|
context->Esi, context->Edi, context->Ebp, context->Esp);
|
||||||
|
log_warning("signal", "registers: eip={:08x} eflags={:08x}",
|
||||||
|
context->Eip, context->EFlags);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_minidump(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
||||||
|
if (MiniDumpWriteDump_local == nullptr) {
|
||||||
|
log_warning("signal", "minidump creation function not available, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE minidump_file = CreateFileA(
|
||||||
|
"minidump.dmp",
|
||||||
|
GENERIC_WRITE,
|
||||||
|
0,
|
||||||
|
nullptr,
|
||||||
|
CREATE_ALWAYS,
|
||||||
|
FILE_ATTRIBUTE_NORMAL,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
if (minidump_file == INVALID_HANDLE_VALUE) {
|
||||||
|
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
|
||||||
|
GetLastError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
|
||||||
|
ExceptionParam.ThreadId = GetCurrentThreadId();
|
||||||
|
ExceptionParam.ExceptionPointers = ExceptionInfo;
|
||||||
|
ExceptionParam.ClientPointers = FALSE;
|
||||||
|
|
||||||
|
constexpr auto extended_type = static_cast<MINIDUMP_TYPE>(
|
||||||
|
MiniDumpWithUnloadedModules |
|
||||||
|
MiniDumpWithIndirectlyReferencedMemory |
|
||||||
|
MiniDumpWithProcessThreadData |
|
||||||
|
MiniDumpWithFullMemoryInfo |
|
||||||
|
MiniDumpWithThreadInfo |
|
||||||
|
MiniDumpIgnoreInaccessibleMemory);
|
||||||
|
|
||||||
|
auto written_type = extended_type;
|
||||||
|
auto minidump_written = MiniDumpWriteDump_local(
|
||||||
|
GetCurrentProcess(),
|
||||||
|
GetCurrentProcessId(),
|
||||||
|
minidump_file,
|
||||||
|
extended_type,
|
||||||
|
&ExceptionParam,
|
||||||
|
nullptr,
|
||||||
|
nullptr);
|
||||||
|
auto minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
|
||||||
|
|
||||||
|
if (!minidump_written) {
|
||||||
|
log_warning("signal", "failed to write extended minidump: 0x{:08x}; retrying MiniDumpNormal",
|
||||||
|
minidump_error);
|
||||||
|
|
||||||
|
LARGE_INTEGER file_start {};
|
||||||
|
if (SetFilePointerEx(minidump_file, file_start, nullptr, FILE_BEGIN) &&
|
||||||
|
SetEndOfFile(minidump_file)) {
|
||||||
|
written_type = MiniDumpNormal;
|
||||||
|
minidump_written = MiniDumpWriteDump_local(
|
||||||
|
GetCurrentProcess(),
|
||||||
|
GetCurrentProcessId(),
|
||||||
|
minidump_file,
|
||||||
|
MiniDumpNormal,
|
||||||
|
&ExceptionParam,
|
||||||
|
nullptr,
|
||||||
|
nullptr);
|
||||||
|
minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
|
||||||
|
} else {
|
||||||
|
minidump_error = GetLastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LARGE_INTEGER minidump_size {};
|
||||||
|
const auto minidump_size_available = minidump_written &&
|
||||||
|
GetFileSizeEx(minidump_file, &minidump_size);
|
||||||
|
CloseHandle(minidump_file);
|
||||||
|
|
||||||
|
if (minidump_written) {
|
||||||
|
if (minidump_size_available) {
|
||||||
|
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x}, size={} bytes)",
|
||||||
|
static_cast<unsigned>(written_type), minidump_size.QuadPart);
|
||||||
|
} else {
|
||||||
|
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x})",
|
||||||
|
static_cast<unsigned>(written_type));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_warning("signal", "failed to write 'minidump.dmp': 0x{:08x}", minidump_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_exception_chain(const struct _EXCEPTION_RECORD *record) {
|
||||||
|
constexpr size_t max_exception_chain_depth = 8;
|
||||||
|
auto *record_cause = record->ExceptionRecord;
|
||||||
|
size_t depth = 0;
|
||||||
|
|
||||||
|
while (record_cause != nullptr && depth < max_exception_chain_depth) {
|
||||||
|
struct _EXCEPTION_RECORD cause {};
|
||||||
|
SIZE_T bytes_read = 0;
|
||||||
|
if (!ReadProcessMemory(
|
||||||
|
GetCurrentProcess(),
|
||||||
|
record_cause,
|
||||||
|
&cause,
|
||||||
|
sizeof(cause),
|
||||||
|
&bytes_read) ||
|
||||||
|
bytes_read != sizeof(cause)) {
|
||||||
|
log_warning("signal", "failed to read exception chain record {} at {}: 0x{:08x}",
|
||||||
|
depth, fmt::ptr(record_cause), GetLastError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning("signal", "caused by: {} at {}",
|
||||||
|
exception_code(&cause), fmt::ptr(cause.ExceptionAddress));
|
||||||
|
log_exception_parameters(&cause);
|
||||||
|
record_cause = cause.ExceptionRecord;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record_cause != nullptr) {
|
||||||
|
log_warning("signal", "exception chain truncated after {} records", max_exception_chain_depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
||||||
log_info("signal", "console ctrl handler called: {}", control_code(dwCtrlType));
|
log_info("signal", "console ctrl handler called: {}", control_code(dwCtrlType));
|
||||||
|
|
||||||
@@ -90,15 +474,23 @@ static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
||||||
|
const auto last_error = GetLastError();
|
||||||
|
|
||||||
// ignore signal if disabled or no exception info provided
|
// ignore signal if disabled or no exception info provided
|
||||||
if (!launcher::signal::DISABLE && ExceptionInfo != nullptr) {
|
if (!launcher::signal::DISABLE &&
|
||||||
|
ExceptionInfo != nullptr &&
|
||||||
|
ExceptionInfo->ExceptionRecord != nullptr &&
|
||||||
|
InterlockedCompareExchange(&EXCEPTION_HANDLER_ACTIVE, 1, 0) == 0) {
|
||||||
|
|
||||||
// get exception record
|
// get exception record
|
||||||
struct _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord;
|
struct _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord;
|
||||||
|
|
||||||
// print signal
|
// print signal
|
||||||
log_warning("signal", "exception raised: {}", exception_code(ExceptionRecord));
|
log_warning("signal", "exception raised: {}", exception_code(ExceptionRecord));
|
||||||
|
log_exception_context(ExceptionInfo, last_error);
|
||||||
|
|
||||||
|
// write the minidump before other diagnostics inspect potentially damaged state
|
||||||
|
write_minidump(ExceptionInfo);
|
||||||
|
|
||||||
switch (ExceptionRecord->ExceptionCode) {
|
switch (ExceptionRecord->ExceptionCode) {
|
||||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||||
@@ -128,12 +520,11 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
|||||||
// (e.g., ACIO init hang due to RTSS)
|
// (e.g., ACIO init hang due to RTSS)
|
||||||
deferredlogs::dump_to_logger(true);
|
deferredlogs::dump_to_logger(true);
|
||||||
|
|
||||||
// walk the exception chain
|
// dump memory information before inspecting potentially damaged exception and stack data
|
||||||
struct _EXCEPTION_RECORD *record_cause = ExceptionRecord->ExceptionRecord;
|
memutils::show_available_memory();
|
||||||
while (record_cause != nullptr) {
|
|
||||||
log_warning("signal", "caused by: {}", exception_code(record_cause));
|
// walk the exception chain with bounded, checked reads
|
||||||
record_cause = record_cause->ExceptionRecord;
|
log_exception_chain(ExceptionRecord);
|
||||||
}
|
|
||||||
|
|
||||||
// print stacktrace
|
// print stacktrace
|
||||||
StackWalker sw;
|
StackWalker sw;
|
||||||
@@ -142,47 +533,12 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
|||||||
log_warning("signal", "failed to print callstack");
|
log_warning("signal", "failed to print callstack");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MiniDumpWriteDump_local != nullptr) {
|
|
||||||
HANDLE minidump_file = CreateFileA(
|
|
||||||
"minidump.dmp",
|
|
||||||
GENERIC_WRITE,
|
|
||||||
0,
|
|
||||||
nullptr,
|
|
||||||
CREATE_ALWAYS,
|
|
||||||
FILE_ATTRIBUTE_NORMAL,
|
|
||||||
nullptr);
|
|
||||||
|
|
||||||
if (minidump_file != INVALID_HANDLE_VALUE) {
|
|
||||||
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
|
|
||||||
ExceptionParam.ThreadId = GetCurrentThreadId();
|
|
||||||
ExceptionParam.ExceptionPointers = ExceptionInfo;
|
|
||||||
ExceptionParam.ClientPointers = FALSE;
|
|
||||||
|
|
||||||
MiniDumpWriteDump_local(
|
|
||||||
GetCurrentProcess(),
|
|
||||||
GetCurrentProcessId(),
|
|
||||||
minidump_file,
|
|
||||||
MiniDumpNormal,
|
|
||||||
&ExceptionParam,
|
|
||||||
nullptr,
|
|
||||||
nullptr);
|
|
||||||
|
|
||||||
CloseHandle(minidump_file);
|
|
||||||
} else {
|
|
||||||
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
|
|
||||||
GetLastError());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log_warning("signal", "minidump creation function not available, skipping");
|
|
||||||
}
|
|
||||||
|
|
||||||
// dump memory information
|
|
||||||
memutils::show_available_memory();
|
|
||||||
|
|
||||||
// this will stall all UI threads for this process
|
// this will stall all UI threads for this process
|
||||||
show_popup_for_crash();
|
show_popup_for_crash();
|
||||||
|
|
||||||
log_fatal("signal", "end");
|
log_fatal("signal", "end");
|
||||||
|
|
||||||
|
InterlockedExchange(&EXCEPTION_HANDLER_ACTIVE, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return EXCEPTION_CONTINUE_SEARCH;
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
#include "superexit.h"
|
#include "superexit.h"
|
||||||
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#include "windows.h"
|
#include "windows.h"
|
||||||
|
|
||||||
|
#include "cfg/configurator.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "rawinput/rawinput.h"
|
|
||||||
#include "util/logging.h"
|
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "misc/eamuse.h"
|
#include "util/logging.h"
|
||||||
#include "games/io.h"
|
|
||||||
#include "overlay/overlay.h"
|
|
||||||
|
|
||||||
namespace superexit {
|
namespace superexit {
|
||||||
|
|
||||||
static std::thread *THREAD = nullptr;
|
|
||||||
static bool THREAD_RUNNING = false;
|
|
||||||
|
|
||||||
bool has_focus() {
|
bool has_focus() {
|
||||||
HWND fg_wnd = GetForegroundWindow();
|
HWND fg_wnd = GetForegroundWindow();
|
||||||
if (fg_wnd == NULL) {
|
if (fg_wnd == NULL) {
|
||||||
@@ -29,92 +22,22 @@ namespace superexit {
|
|||||||
return fg_pid == GetCurrentProcessId();
|
return fg_pid == GetCurrentProcessId();
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable() {
|
void handle_hotkeys(bool alt_f4, bool mapped_exit) {
|
||||||
|
if (!alt_f4 && !mapped_exit) {
|
||||||
// check if already running
|
|
||||||
if (THREAD)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// create new thread
|
|
||||||
THREAD_RUNNING = true;
|
|
||||||
THREAD = new std::thread([] {
|
|
||||||
|
|
||||||
// log
|
|
||||||
log_info("superexit", "enabled");
|
|
||||||
|
|
||||||
// set variable to false to stop
|
|
||||||
while (THREAD_RUNNING) {
|
|
||||||
|
|
||||||
// check rawinput for ALT+F4
|
|
||||||
bool rawinput_exit = false;
|
|
||||||
if (RI_MGR != nullptr) {
|
|
||||||
auto devices = RI_MGR->devices_get();
|
|
||||||
for (auto &device : devices) {
|
|
||||||
switch (device.type) {
|
|
||||||
case rawinput::KEYBOARD: {
|
|
||||||
auto &key_states = device.keyboardInfo->key_states;
|
|
||||||
for (int page_index = 0; page_index < 1024; page_index += 256) {
|
|
||||||
if (key_states[page_index + VK_MENU]
|
|
||||||
&& key_states[page_index + VK_F4]) {
|
|
||||||
rawinput_exit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool async_key_exit =
|
|
||||||
(GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
|
|
||||||
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
|
|
||||||
|
|
||||||
bool overlay_exit = false;
|
|
||||||
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
|
||||||
if (buttons &&
|
|
||||||
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
|
|
||||||
GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::SuperExit))) {
|
|
||||||
overlay_exit = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for exit
|
|
||||||
if (rawinput_exit || async_key_exit) {
|
|
||||||
if (has_focus()) {
|
|
||||||
log_info("superexit", "detected ALT+F4, exiting...");
|
|
||||||
launcher::shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (overlay_exit) {
|
|
||||||
if (has_focus()) {
|
|
||||||
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
|
|
||||||
launcher::shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// slow down
|
|
||||||
Sleep(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void disable() {
|
|
||||||
if (!THREAD) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||||
// stop old thread
|
return;
|
||||||
THREAD_RUNNING = false;
|
}
|
||||||
THREAD->join();
|
if (!has_focus()) {
|
||||||
|
return;
|
||||||
// delete thread
|
}
|
||||||
delete THREAD;
|
if (alt_f4) {
|
||||||
THREAD = nullptr;
|
log_info("superexit", "detected ALT+F4, exiting...");
|
||||||
|
launcher::shutdown();
|
||||||
// log
|
return;
|
||||||
log_info("superexit", "disabled");
|
}
|
||||||
|
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
|
||||||
|
launcher::shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,5 @@
|
|||||||
|
|
||||||
namespace superexit {
|
namespace superexit {
|
||||||
bool has_focus();
|
bool has_focus();
|
||||||
void enable();
|
void handle_hotkeys(bool alt_f4, bool mapped_exit);
|
||||||
void disable();
|
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-16
@@ -527,25 +527,78 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABI
|
|||||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
toojpeg (zlib)
|
fpng (Unlicense)
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
zlib License
|
Copyright (c) 2021 Richard Geldreich, Jr.
|
||||||
|
Incorporates public domain code by Alex Evans, the original miniz by
|
||||||
|
Richard Geldreich, Jr., and Huffman code size work by Alistair Moffat and
|
||||||
|
Jyrki Katajainen.
|
||||||
|
|
||||||
Copyright (c) 2011-2016 Stephan Brumme
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
This software is provided 'as-is', without any express or implied warranty. In
|
Anyone is free to copy, modify, publish, use, compile, sell, or distribute
|
||||||
no event will the authors be held liable for any damages arising from the use
|
this software, either in source code form or as a compiled binary, for any
|
||||||
of this software.
|
purpose, commercial or non-commercial, and by any means.
|
||||||
Permission is granted to anyone to use this software for any purpose, including
|
|
||||||
commercial applications, and to alter it and redistribute it freely, subject to
|
In jurisdictions that recognize copyright laws, the author or authors of this
|
||||||
the following restrictions:
|
software dedicate any and all copyright interest in the software to the public
|
||||||
1. The origin of this software must not be misrepresented; you must not claim
|
domain. We make this dedication for the benefit of the public at large and to
|
||||||
that you wrote the original software. If you use this software in a product,
|
the detriment of our heirs and successors. We intend this dedication to be an
|
||||||
an acknowledgment in the product documentation would be appreciated but is
|
overt act of relinquishment in perpetuity of all present and future rights to
|
||||||
not required.
|
this software under copyright law.
|
||||||
2. Altered source versions must be plainly marked as such, and must not be
|
|
||||||
misrepresented as being the original software.
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
3. This notice may not be removed or altered from any source distribution.
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
libjpeg-turbo (IJG)
|
||||||
|
-------------------------------------------
|
||||||
|
This software is based in part on the work of the Independent JPEG Group.
|
||||||
|
|
||||||
|
Only the libjpeg API library is used here. Per the libjpeg-turbo licensing
|
||||||
|
terms that portion is covered by the IJG License, reproduced below; the SIMD
|
||||||
|
sources bear the zlib License, whose terms are subsumed by the IJG License in
|
||||||
|
the context of the overall libjpeg API library. The Modified (3-clause) BSD
|
||||||
|
License covers the TurboJPEG API library and build system, neither of which is
|
||||||
|
distributed here.
|
||||||
|
|
||||||
|
The authors make NO WARRANTY or representation, either express or implied,
|
||||||
|
with respect to this software, its quality, accuracy, merchantability, or
|
||||||
|
fitness for a particular purpose. This software is provided "AS IS", and you,
|
||||||
|
its user, assume the entire risk as to its quality and accuracy.
|
||||||
|
|
||||||
|
This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding.
|
||||||
|
All Rights Reserved except as specified below.
|
||||||
|
|
||||||
|
Permission is hereby granted to use, copy, modify, and distribute this
|
||||||
|
software (or portions thereof) for any purpose, without fee, subject to these
|
||||||
|
conditions:
|
||||||
|
(1) If any part of the source code for this software is distributed, then this
|
||||||
|
README file must be included, with this copyright and no-warranty notice
|
||||||
|
unaltered; and any additions, deletions, or changes to the original files
|
||||||
|
must be clearly indicated in accompanying documentation.
|
||||||
|
(2) If only executable code is distributed, then the accompanying
|
||||||
|
documentation must state that "this software is based in part on the work of
|
||||||
|
the Independent JPEG Group".
|
||||||
|
(3) Permission for use of this software is granted only if the user accepts
|
||||||
|
full responsibility for any undesirable consequences; the authors accept
|
||||||
|
NO LIABILITY for damages of any kind.
|
||||||
|
|
||||||
|
These conditions apply to any software derived from or based on the IJG code,
|
||||||
|
not just to the unmodified library. If you use our work, you ought to
|
||||||
|
acknowledge us.
|
||||||
|
|
||||||
|
Permission is NOT granted for the use of any IJG author's name or company name
|
||||||
|
in advertising or publicity relating to this software or products derived from
|
||||||
|
it. This software may be referred to only as "the Independent JPEG Group's
|
||||||
|
software".
|
||||||
|
|
||||||
|
We specifically permit and encourage the use of this software as the basis of
|
||||||
|
commercial products, provided that all warranty or liability claims are
|
||||||
|
assumed by the product vendor.
|
||||||
|
|
||||||
robin_hood.h (MIT)
|
robin_hood.h (MIT)
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
@@ -1353,3 +1406,23 @@ Contributions
|
|||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
cardio - Felix - MIT License
|
cardio - Felix - MIT License
|
||||||
scard - nolm - MIT License
|
scard - nolm - MIT License
|
||||||
|
|
||||||
|
x264 (GPL-2.0-or-later)
|
||||||
|
-------------------------------------------
|
||||||
|
https://www.videolan.org/developers/x264.html
|
||||||
|
Statically linked for the API video stream H.264 encoder.
|
||||||
|
|
||||||
|
Copyright (C) 2003-2024 x264 project
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
|
||||||
|
Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
@@ -58,17 +58,18 @@ namespace clipboard {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the screenshot button is print screen, the OpenClipboard call seems to fail often if we only
|
// print screen key leaves the OS briefly holding the clipboard, so retry
|
||||||
// call it once, probably due to a race condition. So, we can try calling a lot until we can open it.
|
// spinning without yielding starves the thread we are waiting on and looks like a hang
|
||||||
bool clipboard_open = false;
|
bool clipboard_open = false;
|
||||||
for (int i = 0; i < 1000000; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
if (OpenClipboard(nullptr)) {
|
if (OpenClipboard(nullptr)) {
|
||||||
clipboard_open = true;
|
clipboard_open = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Sleep(5);
|
||||||
}
|
}
|
||||||
if (!clipboard_open) {
|
if (!clipboard_open) {
|
||||||
log_warning("clipboard", "Failed to open clipboard");
|
log_warning("clipboard", "failed to open clipboard");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-64
@@ -1,6 +1,7 @@
|
|||||||
#include "eamuse.h"
|
#include "eamuse.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
@@ -10,7 +11,6 @@
|
|||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
@@ -25,10 +25,8 @@ static double CARD_INSERT_TIME[2] = {0, 0};
|
|||||||
static double CARD_INSERT_TIMEOUT = 2.0;
|
static double CARD_INSERT_TIMEOUT = 2.0;
|
||||||
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
|
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
|
||||||
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
||||||
static int COIN_STOCK = 0;
|
static std::atomic_int COIN_STOCK {0};
|
||||||
static bool COIN_BLOCK = false;
|
static std::atomic_bool COIN_BLOCK {false};
|
||||||
static std::thread *COIN_INPUT_THREAD;
|
|
||||||
static bool COIN_INPUT_THREAD_ACTIVE = false;
|
|
||||||
static uint16_t KEYPAD_STATE[] = {0, 0};
|
static uint16_t KEYPAD_STATE[] = {0, 0};
|
||||||
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
|
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
|
||||||
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
|
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
|
||||||
@@ -292,79 +290,49 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool eamuse_coin_get_block() {
|
bool eamuse_coin_get_block() {
|
||||||
return COIN_BLOCK;
|
return COIN_BLOCK.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_coin_set_block(bool block) {
|
void eamuse_coin_set_block(bool block) {
|
||||||
COIN_BLOCK = block;
|
COIN_BLOCK.store(block, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
int eamuse_coin_get_stock() {
|
int eamuse_coin_get_stock() {
|
||||||
return COIN_STOCK;
|
return COIN_STOCK.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_coin_set_stock(int amount) {
|
void eamuse_coin_set_stock(int amount) {
|
||||||
COIN_STOCK = amount;
|
COIN_STOCK.store(amount, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool eamuse_coin_consume(int amount) {
|
bool eamuse_coin_consume(int amount) {
|
||||||
if (COIN_STOCK < amount) {
|
auto stock = COIN_STOCK.load(std::memory_order_relaxed);
|
||||||
return false;
|
while (stock >= amount) {
|
||||||
} else {
|
if (COIN_STOCK.compare_exchange_weak(
|
||||||
COIN_STOCK -= amount;
|
stock,
|
||||||
return true;
|
stock - amount,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int eamuse_coin_consume_stock() {
|
int eamuse_coin_consume_stock() {
|
||||||
int stock = COIN_STOCK;
|
return COIN_STOCK.exchange(0, std::memory_order_relaxed);
|
||||||
COIN_STOCK = 0;
|
|
||||||
return stock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int eamuse_coin_add() {
|
int eamuse_coin_add() {
|
||||||
return ++COIN_STOCK;
|
return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_coin_start_thread() {
|
void eamuse_coin_insert() {
|
||||||
|
if (COIN_BLOCK.load(std::memory_order_relaxed)) {
|
||||||
// set active
|
log_info("eamuse", "coin inserted while blocked");
|
||||||
COIN_INPUT_THREAD_ACTIVE = true;
|
} else {
|
||||||
|
log_info("eamuse", "coin insert");
|
||||||
// create thread
|
COIN_STOCK.fetch_add(1, std::memory_order_relaxed);
|
||||||
COIN_INPUT_THREAD = new std::thread([]() {
|
}
|
||||||
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
|
|
||||||
static bool COIN_INPUT_KEY_STATE = false;
|
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
while (COIN_INPUT_THREAD_ACTIVE) {
|
|
||||||
|
|
||||||
// check input key
|
|
||||||
if (overlay_buttons && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(
|
|
||||||
games::OverlayButtons::InsertCoin))) {
|
|
||||||
if (!COIN_INPUT_KEY_STATE) {
|
|
||||||
if (COIN_BLOCK)
|
|
||||||
log_info("eamuse", "coin inserted while blocked");
|
|
||||||
else {
|
|
||||||
log_info("eamuse", "coin insert");
|
|
||||||
COIN_STOCK++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
COIN_INPUT_KEY_STATE = true;
|
|
||||||
} else {
|
|
||||||
COIN_INPUT_KEY_STATE = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// once every two frames
|
|
||||||
timer.sleep(1000 / 30);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void eamuse_coin_stop_thread() {
|
|
||||||
COIN_INPUT_THREAD_ACTIVE = false;
|
|
||||||
COIN_INPUT_THREAD->join();
|
|
||||||
delete COIN_INPUT_THREAD;
|
|
||||||
COIN_INPUT_THREAD = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_pin_macro_start_thread() {
|
void eamuse_pin_macro_start_thread() {
|
||||||
@@ -409,7 +377,6 @@ void eamuse_pin_macro_start_thread() {
|
|||||||
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
|
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
|
||||||
|
|
||||||
std::optional<uint8_t> active_unit = std::nullopt;
|
std::optional<uint8_t> active_unit = std::nullopt;
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
while (PIN_MACRO_THREAD_ACTIVE) {
|
while (PIN_MACRO_THREAD_ACTIVE) {
|
||||||
// wait for key press or auto-trigger
|
// wait for key press or auto-trigger
|
||||||
@@ -438,7 +405,7 @@ void eamuse_pin_macro_start_thread() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!active_unit.has_value()) {
|
if (!active_unit.has_value()) {
|
||||||
timer.sleep(20);
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,22 +421,22 @@ void eamuse_pin_macro_start_thread() {
|
|||||||
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
|
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
|
||||||
}
|
}
|
||||||
pin_index[unit]++;
|
pin_index[unit]++;
|
||||||
timer.sleep(100);
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
|
||||||
// clear
|
// clear
|
||||||
eamuse_set_keypad_overrides(unit, 0);
|
eamuse_set_keypad_overrides(unit, 0);
|
||||||
timer.sleep(50);
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
|
||||||
// end of PIN
|
// end of PIN
|
||||||
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
|
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
|
||||||
active_unit = std::nullopt;
|
active_unit = std::nullopt;
|
||||||
timer.sleep(120);
|
std::this_thread::sleep_for(std::chrono::milliseconds(120));
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.sleep(200);
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -675,7 +642,8 @@ void eamuse_autodetect_game() {
|
|||||||
eamuse_set_game("Beatmania IIDX");
|
eamuse_set_game("Beatmania IIDX");
|
||||||
else if (avs::game::is_model("J44") ||
|
else if (avs::game::is_model("J44") ||
|
||||||
avs::game::is_model("K44") ||
|
avs::game::is_model("K44") ||
|
||||||
avs::game::is_model("L44"))
|
avs::game::is_model("L44") ||
|
||||||
|
avs::game::is_model("T44"))
|
||||||
eamuse_set_game("Jubeat");
|
eamuse_set_game("Jubeat");
|
||||||
else if (avs::game::is_model("KDM"))
|
else if (avs::game::is_model("KDM"))
|
||||||
eamuse_set_game("Dance Evolution");
|
eamuse_set_game("Dance Evolution");
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ bool eamuse_coin_consume(int amount);
|
|||||||
int eamuse_coin_consume_stock();
|
int eamuse_coin_consume_stock();
|
||||||
|
|
||||||
int eamuse_coin_add();
|
int eamuse_coin_add();
|
||||||
|
void eamuse_coin_insert();
|
||||||
void eamuse_coin_start_thread();
|
|
||||||
void eamuse_coin_stop_thread();
|
|
||||||
|
|
||||||
void eamuse_pin_macro_start_thread();
|
void eamuse_pin_macro_start_thread();
|
||||||
void eamuse_pin_macro_stop_thread();
|
void eamuse_pin_macro_stop_thread();
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#include "hotkeys.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <stop_token>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "games/io.h"
|
||||||
|
#include "launcher/superexit.h"
|
||||||
|
#include "misc/eamuse.h"
|
||||||
|
#include "overlay/overlay.h"
|
||||||
|
#include "rawinput/rawinput.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace hotkeys {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 8 ms targets short screenshot pulses; sleep_for may use a coarser scheduler
|
||||||
|
// interval during early boot or when process timer adjustments are disabled
|
||||||
|
constexpr auto MIN_SAMPLE_INTERVAL = std::chrono::milliseconds(8);
|
||||||
|
|
||||||
|
std::atomic_bool SCREENSHOT_PENDING {false};
|
||||||
|
std::mutex INPUT_MUTEX;
|
||||||
|
bool INPUT_ENABLED = false;
|
||||||
|
bool RAW_INPUT_ENABLED = false;
|
||||||
|
std::jthread WORKER;
|
||||||
|
|
||||||
|
bool read_button(std::vector<Button> *buttons, size_t index) {
|
||||||
|
// getState retains each binding's focus, modifier, inversion, and debounce policy
|
||||||
|
return RI_MGR && buttons && index < buttons->size() &&
|
||||||
|
GameAPI::Buttons::getState(RI_MGR, buttons->at(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_alt_f4() {
|
||||||
|
// preserve both legacy detection paths whenever raw input is available
|
||||||
|
bool pressed = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
|
||||||
|
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
|
||||||
|
if (!RAW_INPUT_ENABLED || !RI_MGR) {
|
||||||
|
return pressed;
|
||||||
|
}
|
||||||
|
return pressed || RI_MGR->keyboard_combo_pressed(VK_MENU, VK_F4);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool rising_edge(bool current, bool &previous) {
|
||||||
|
const bool edge = current && !previous;
|
||||||
|
previous = current;
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
void run(std::stop_token stop_token) {
|
||||||
|
bool screenshot_previous = false;
|
||||||
|
bool coin_previous = false;
|
||||||
|
|
||||||
|
while (!stop_token.stop_requested()) {
|
||||||
|
bool coin_edge = false;
|
||||||
|
bool super_exit_current = false;
|
||||||
|
bool alt_f4_current = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
// lifecycle functions hold this mutex until raw-input polling is complete
|
||||||
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
|
if (INPUT_ENABLED || RAW_INPUT_ENABLED) {
|
||||||
|
auto *buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||||
|
const bool screenshot_down = INPUT_ENABLED && read_button(
|
||||||
|
buttons, games::OverlayButtons::Screenshot);
|
||||||
|
const bool coin_current = INPUT_ENABLED && read_button(
|
||||||
|
buttons, games::OverlayButtons::InsertCoin);
|
||||||
|
const bool super_exit_down = RAW_INPUT_ENABLED && read_button(
|
||||||
|
buttons, games::OverlayButtons::SuperExit);
|
||||||
|
|
||||||
|
// global_hotkeys_triggered takes OVERLAY_MUTEX, then the overlay's
|
||||||
|
// hotkeys_mutex; its button reads may then take device mutexes. polling
|
||||||
|
// every mapped-input tick is intentional so HotkeyToggle releases cannot
|
||||||
|
// be missed when the render thread stalls.
|
||||||
|
const bool gate_active = overlay::global_hotkeys_triggered();
|
||||||
|
const bool screenshot_current = screenshot_down && gate_active;
|
||||||
|
super_exit_current = super_exit_down && gate_active;
|
||||||
|
|
||||||
|
if (rising_edge(screenshot_current, screenshot_previous)) {
|
||||||
|
SCREENSHOT_PENDING.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
coin_edge = rising_edge(coin_current, coin_previous);
|
||||||
|
} else {
|
||||||
|
screenshot_previous = false;
|
||||||
|
coin_previous = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
alt_f4_current = read_alt_f4();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coin_edge) {
|
||||||
|
eamuse_coin_insert();
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass held state so returning focus can exit without another key press
|
||||||
|
superexit::handle_hotkeys(alt_f4_current, super_exit_current);
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(MIN_SAMPLE_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
if (WORKER.joinable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||||
|
WORKER = std::jthread(run);
|
||||||
|
log_info("hotkeys", "sampler started");
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable_raw_input() {
|
||||||
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
|
RAW_INPUT_ENABLED = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable_input() {
|
||||||
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
|
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||||
|
INPUT_ENABLED = true;
|
||||||
|
log_info("hotkeys", "configured input enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable_input() {
|
||||||
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
|
INPUT_ENABLED = false;
|
||||||
|
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||||
|
log_info("hotkeys", "configured input disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable_raw_input() {
|
||||||
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
|
RAW_INPUT_ENABLED = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
if (!WORKER.joinable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WORKER.request_stop();
|
||||||
|
WORKER.join();
|
||||||
|
log_info("hotkeys", "sampler stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool consume_screenshot() {
|
||||||
|
return SCREENSHOT_PENDING.exchange(false, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace hotkeys {
|
||||||
|
|
||||||
|
// ALT+F4 monitoring spans boot and teardown; configured actions are enabled separately
|
||||||
|
void start();
|
||||||
|
void enable_raw_input();
|
||||||
|
void enable_input();
|
||||||
|
void disable_input();
|
||||||
|
void disable_raw_input();
|
||||||
|
void stop();
|
||||||
|
bool consume_screenshot();
|
||||||
|
}
|
||||||
@@ -4,19 +4,44 @@
|
|||||||
|
|
||||||
#include "wintouchemu.h"
|
#include "wintouchemu.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "cfg/screen_resize.h"
|
||||||
|
#include "games/gitadora/gitadora.h"
|
||||||
|
#include "games/iidx/iidx.h"
|
||||||
|
#include "games/popn/popn.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "overlay/windows/generic_sub.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/time.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
#include "rawinput/touch.h"
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
|
|
||||||
namespace wintouchemu {
|
namespace wintouchemu {
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
POINT pos;
|
||||||
|
bool last_button_pressed;
|
||||||
|
DWORD touch_event;
|
||||||
|
} mouse_state_t;
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
bool FORCE = false;
|
bool FORCE = false;
|
||||||
|
bool INJECT_MOUSE_AS_WM_TOUCH = false;
|
||||||
|
bool LOG_FPS = false;
|
||||||
bool ADD_TOUCH_FLAG_PRIMARY = false;
|
bool ADD_TOUCH_FLAG_PRIMARY = false;
|
||||||
|
|
||||||
|
// state
|
||||||
|
double last_touch_event = 0.0;
|
||||||
|
|
||||||
static inline bool is_emu_enabled() {
|
static inline bool is_emu_enabled() {
|
||||||
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
|
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
|
||||||
}
|
}
|
||||||
@@ -56,13 +81,16 @@ namespace wintouchemu {
|
|||||||
|
|
||||||
// state
|
// state
|
||||||
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
|
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
|
||||||
|
bool USE_MOUSE = false;
|
||||||
std::vector<TouchEvent> TOUCH_EVENTS;
|
std::vector<TouchEvent> TOUCH_EVENTS;
|
||||||
std::vector<TouchPoint> TOUCH_POINTS;
|
std::vector<TouchPoint> TOUCH_POINTS;
|
||||||
HMODULE HOOKED_MODULE = nullptr;
|
HMODULE HOOKED_MODULE = nullptr;
|
||||||
std::string WINDOW_TITLE_START = "";
|
std::string WINDOW_TITLE_START = "";
|
||||||
bool INITIALIZED = false;
|
std::optional<std::string> WINDOW_TITLE_END = std::nullopt;
|
||||||
|
volatile bool INITIALIZED = false;
|
||||||
|
mouse_state_t mouse_state;
|
||||||
|
|
||||||
void hook(const char *window_title, HMODULE module) {
|
void hook(const char *window_title, HMODULE module, int delay_in_s) {
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
auto system_metrics_hook = detour::iat_try(
|
auto system_metrics_hook = detour::iat_try(
|
||||||
@@ -81,8 +109,31 @@ namespace wintouchemu {
|
|||||||
// set module and title
|
// set module and title
|
||||||
HOOKED_MODULE = module;
|
HOOKED_MODULE = module;
|
||||||
WINDOW_TITLE_START = window_title;
|
WINDOW_TITLE_START = window_title;
|
||||||
log_misc("wintouchemu", "initializing");
|
|
||||||
INITIALIZED = true;
|
if (0 < delay_in_s) {
|
||||||
|
// some games crash when touch events are injected too early during boot
|
||||||
|
std::thread t([&]() {
|
||||||
|
log_misc("wintouchemu", "defer initialization until later (with delay of {}s)", delay_in_s);
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(delay_in_s));
|
||||||
|
log_misc("wintouchemu", "initializing", delay_in_s);
|
||||||
|
INITIALIZED = true;
|
||||||
|
});
|
||||||
|
t.detach();
|
||||||
|
} else {
|
||||||
|
log_misc("wintouchemu", "initializing");
|
||||||
|
INITIALIZED = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hook_title_ends(const char *window_title_start, const char *window_title_end, HMODULE module) {
|
||||||
|
hook(window_title_start, module);
|
||||||
|
|
||||||
|
WINDOW_TITLE_END = window_title_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void flip_touch_points(PTOUCHINPUT point) {
|
||||||
|
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
|
||||||
|
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
||||||
@@ -94,6 +145,7 @@ namespace wintouchemu {
|
|||||||
|
|
||||||
// set touch inputs
|
// set touch inputs
|
||||||
bool result = false;
|
bool result = false;
|
||||||
|
bool mouse_used = false;
|
||||||
for (UINT input = 0; input < cInputs; input++) {
|
for (UINT input = 0; input < cInputs; input++) {
|
||||||
auto *touch_input = &pInputs[input];
|
auto *touch_input = &pInputs[input];
|
||||||
|
|
||||||
@@ -126,7 +178,13 @@ namespace wintouchemu {
|
|||||||
|
|
||||||
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
|
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
|
||||||
|
|
||||||
if (overlay::OVERLAY) {
|
if (GRAPHICS_IIDX_WSUB) {
|
||||||
|
// touch was received on subscreen window.
|
||||||
|
RECT clientRect {};
|
||||||
|
GetClientRect(TDJ_SUBSCREEN_WINDOW, &clientRect);
|
||||||
|
x = (float) x / clientRect.right * SPICETOUCH_TOUCH_WIDTH + SPICETOUCH_TOUCH_X;
|
||||||
|
y = (float) y / clientRect.bottom * SPICETOUCH_TOUCH_HEIGHT + SPICETOUCH_TOUCH_Y;
|
||||||
|
} else if (overlay::OVERLAY) {
|
||||||
// touch was received on global coords
|
// touch was received on global coords
|
||||||
valid = overlay::OVERLAY->transform_touch_point(&x, &y);
|
valid = overlay::OVERLAY->transform_touch_point(&x, &y);
|
||||||
} else {
|
} else {
|
||||||
@@ -172,10 +230,89 @@ namespace wintouchemu {
|
|||||||
touch_input->cxContact = 0;
|
touch_input->cxContact = 0;
|
||||||
touch_input->cyContact = 0;
|
touch_input->cyContact = 0;
|
||||||
|
|
||||||
} else {
|
if (avs::game::is_model("KFC") &&
|
||||||
|
rawinput::touch::DISPLAY_INITIALIZED &&
|
||||||
|
rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
|
||||||
|
flip_touch_points(touch_input);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (USE_MOUSE && !mouse_used) {
|
||||||
|
|
||||||
|
// disable further mouse inputs this call
|
||||||
|
mouse_used = true;
|
||||||
|
|
||||||
|
if (mouse_state.touch_event) {
|
||||||
|
result = true;
|
||||||
|
touch_input->x = mouse_state.pos.x;
|
||||||
|
touch_input->y = mouse_state.pos.y;
|
||||||
|
|
||||||
|
if (GRAPHICS_WINDOWED) {
|
||||||
|
touch_input->x -= SPICETOUCH_TOUCH_X;
|
||||||
|
touch_input->y -= SPICETOUCH_TOUCH_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// log_misc(
|
||||||
|
// "wintouchemu",
|
||||||
|
// "mouse state ({}, {}) event={}",
|
||||||
|
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
|
||||||
|
|
||||||
|
auto valid = true;
|
||||||
|
if (overlay::OVERLAY) {
|
||||||
|
valid = overlay::OVERLAY->transform_touch_point(
|
||||||
|
&touch_input->x, &touch_input->y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// touch inputs require 100x precision per pixel
|
||||||
|
touch_input->x *= 100;
|
||||||
|
touch_input->y *= 100;
|
||||||
|
touch_input->hSource = hTouchInput;
|
||||||
|
touch_input->dwID = 0;
|
||||||
|
touch_input->dwFlags = 0;
|
||||||
|
switch (mouse_state.touch_event) {
|
||||||
|
case TOUCHEVENTF_DOWN:
|
||||||
|
if (valid) {
|
||||||
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
||||||
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TOUCHEVENTF_MOVE:
|
||||||
|
if (valid) {
|
||||||
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
||||||
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TOUCHEVENTF_UP:
|
||||||
|
// don't check valid so that this touch ID can be released
|
||||||
|
if (ADD_TOUCH_FLAG_PRIMARY) {
|
||||||
|
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch_input->dwFlags |= TOUCHEVENTF_UP;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
touch_input->dwMask = 0;
|
||||||
|
touch_input->dwTime = 0;
|
||||||
|
touch_input->dwExtraInfo = 0;
|
||||||
|
touch_input->cxContact = 0;
|
||||||
|
touch_input->cyContact = 0;
|
||||||
|
|
||||||
|
// reset it since the event was consumed & propagated as touch
|
||||||
|
mouse_state.touch_event = 0;
|
||||||
|
}
|
||||||
|
} else if (!GRAPHICS_IIDX_WSUB) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For some reason, Nostalgia won't show an active touch point unless a move event
|
||||||
|
* triggers in the same frame. To work around this, we just supply a fake move
|
||||||
|
* event if we didn't update the same pointer ID in the same call.
|
||||||
|
*/
|
||||||
|
|
||||||
// beatstream requires a MOVE for active points in each update
|
|
||||||
// add one for every active point without a matching input event
|
|
||||||
// find touch point which has no associated input event
|
// find touch point which has no associated input event
|
||||||
TouchPoint *touch_point = nullptr;
|
TouchPoint *touch_point = nullptr;
|
||||||
for (auto &tp : TOUCH_POINTS) {
|
for (auto &tp : TOUCH_POINTS) {
|
||||||
@@ -243,6 +380,21 @@ namespace wintouchemu {
|
|||||||
title = get_window_title(hWnd);
|
title = get_window_title(hWnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if a window title end is set, check to see if it matches
|
||||||
|
if (WINDOW_TITLE_END.has_value() && !string_ends_with(title.c_str(), WINDOW_TITLE_END.value().c_str())) {
|
||||||
|
hWnd = nullptr;
|
||||||
|
title = "";
|
||||||
|
|
||||||
|
for (auto &window : find_windows_beginning_with(WINDOW_TITLE_START)) {
|
||||||
|
auto check_title = get_window_title(window);
|
||||||
|
if (string_ends_with(check_title.c_str(), WINDOW_TITLE_END.value().c_str())) {
|
||||||
|
hWnd = std::move(window);
|
||||||
|
title = std::move(check_title);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check window
|
// check window
|
||||||
if (hWnd == nullptr) {
|
if (hWnd == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -250,13 +402,39 @@ namespace wintouchemu {
|
|||||||
|
|
||||||
// check if windowed
|
// check if windowed
|
||||||
if (GRAPHICS_WINDOWED) {
|
if (GRAPHICS_WINDOWED) {
|
||||||
// create touch window - create overlay if not yet existing at this point
|
if (GRAPHICS_IIDX_WSUB) {
|
||||||
log_info("wintouchemu", "create touch window relative to main game window");
|
// no handling is needed here
|
||||||
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
|
// graphics::MoveWindow_hook will attach hook to windowed subscreen
|
||||||
|
log_info("wintouchemu", "attach touch hook to windowed subscreen for TDJ");
|
||||||
|
USE_MOUSE = false;
|
||||||
|
} else if (avs::game::is_model("LDJ") && !GENERIC_SUB_WINDOW_FULLSIZE) {
|
||||||
|
// overlay subscreen in IIDX
|
||||||
|
// use mouse position as ImGui overlay will block the touch window
|
||||||
|
log_info("wintouchemu", "use mouse cursor API for ldj overlay subscreen");
|
||||||
|
USE_MOUSE = true;
|
||||||
|
} else if (games::popn::is_pikapika_model()) {
|
||||||
|
// same as iidx case above
|
||||||
|
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
|
||||||
|
USE_MOUSE = true;
|
||||||
|
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
|
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
|
||||||
|
USE_MOUSE = true;
|
||||||
|
} else {
|
||||||
|
// create touch window - create overlay if not yet existing at this point
|
||||||
|
log_info("wintouchemu", "create touch window relative to main game window");
|
||||||
|
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
|
||||||
|
USE_MOUSE = false;
|
||||||
|
}
|
||||||
|
} else if (INJECT_MOUSE_AS_WM_TOUCH) {
|
||||||
|
log_info(
|
||||||
|
"wintouchemu",
|
||||||
|
"using raw mouse cursor API in full screen and injecting them as WM_TOUCH events");
|
||||||
|
USE_MOUSE = true;
|
||||||
} else {
|
} else {
|
||||||
log_info("wintouchemu", "activating DirectX hooks");
|
log_info("wintouchemu", "activating DirectX hooks");
|
||||||
// mouse position based input only
|
// mouse position based input only
|
||||||
touch_attach_dx_hook();
|
touch_attach_dx_hook();
|
||||||
|
USE_MOUSE = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
@@ -267,6 +445,8 @@ namespace wintouchemu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto now = get_performance_milliseconds();
|
||||||
|
|
||||||
// update touch events
|
// update touch events
|
||||||
if (hWnd != nullptr) {
|
if (hWnd != nullptr) {
|
||||||
|
|
||||||
@@ -286,11 +466,64 @@ namespace wintouchemu {
|
|||||||
|
|
||||||
// check if new events are available
|
// check if new events are available
|
||||||
if (event_count > 0) {
|
if (event_count > 0) {
|
||||||
|
last_touch_event = now;
|
||||||
|
|
||||||
// send fake event to make the game update it's touch inputs
|
// send fake event to make the game update it's touch inputs
|
||||||
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
||||||
wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook);
|
wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update frame logging
|
||||||
|
if (LOG_FPS) {
|
||||||
|
static int log_frames = 0;
|
||||||
|
static uint64_t log_time = 0;
|
||||||
|
log_frames++;
|
||||||
|
if (log_time < get_system_seconds()) {
|
||||||
|
if (log_time > 0) {
|
||||||
|
log_info("wintouchemu", "polling at {} touch frames per second", log_frames);
|
||||||
|
}
|
||||||
|
log_frames = 0;
|
||||||
|
log_time = get_system_seconds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send separate WM_TOUCH event for mouse
|
||||||
|
// this must be separate from actual touch events because some games will ignore the return
|
||||||
|
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
|
||||||
|
// send empty events when the mouse button is not clicked/released
|
||||||
|
if (hWnd != nullptr && USE_MOUSE) {
|
||||||
|
bool button_pressed = get_async_primary_mouse();
|
||||||
|
|
||||||
|
// while the subscreen overlay cannot accept the mouse, treat the button as
|
||||||
|
// released; this ends any active touch and prevents new mouse touch contacts
|
||||||
|
if (overlay::OVERLAY &&
|
||||||
|
overlay::OVERLAY->has_subscreen_touch_transform() &&
|
||||||
|
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
|
||||||
|
button_pressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there was a touch event in the last 500 ms, don't insert new button presses
|
||||||
|
if (button_pressed && (now - last_touch_event) < 500) {
|
||||||
|
button_pressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out what kind of touch event to simulate
|
||||||
|
if (button_pressed && !mouse_state.last_button_pressed) {
|
||||||
|
mouse_state.touch_event = TOUCHEVENTF_DOWN;
|
||||||
|
} else if (button_pressed && mouse_state.last_button_pressed) {
|
||||||
|
mouse_state.touch_event = TOUCHEVENTF_MOVE;
|
||||||
|
} else if (!button_pressed && mouse_state.last_button_pressed) {
|
||||||
|
mouse_state.touch_event = TOUCHEVENTF_UP;
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse_state.last_button_pressed = button_pressed;
|
||||||
|
if (mouse_state.touch_event) {
|
||||||
|
GetCursorPos(&mouse_state.pos);
|
||||||
|
// send fake event to make the game update it's touch inputs
|
||||||
|
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
||||||
|
wndProc(hWnd, WM_TOUCH, MAKEWORD(1, 0), (LPARAM) GetTouchInputInfoHook);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ namespace wintouchemu {
|
|||||||
|
|
||||||
// settings
|
// settings
|
||||||
extern bool FORCE;
|
extern bool FORCE;
|
||||||
|
extern bool INJECT_MOUSE_AS_WM_TOUCH;
|
||||||
|
extern bool LOG_FPS;
|
||||||
extern bool ADD_TOUCH_FLAG_PRIMARY;
|
extern bool ADD_TOUCH_FLAG_PRIMARY;
|
||||||
|
|
||||||
void hook(const char *window_title, HMODULE module = nullptr);
|
void hook(const char *window_title, HMODULE module = nullptr, int delay_in_s=0);
|
||||||
|
void hook_title_ends(
|
||||||
|
const char *window_title_start,
|
||||||
|
const char *window_title_ends,
|
||||||
|
HMODULE module = nullptr);
|
||||||
void update();
|
void update();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,11 @@ void overlay::destroy(HWND hWnd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool overlay::global_hotkeys_triggered() {
|
||||||
|
const std::lock_guard<std::mutex> lock(OVERLAY_MUTEX);
|
||||||
|
return !overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered();
|
||||||
|
}
|
||||||
|
|
||||||
overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device)
|
overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device)
|
||||||
: renderer(OverlayRenderer::D3D9), hWnd(hWnd), d3d(d3d), device(device) {
|
: renderer(OverlayRenderer::D3D9), hWnd(hWnd), d3d(d3d), device(device) {
|
||||||
log_info("overlay", "initializing (D3D9)");
|
log_info("overlay", "initializing (D3D9)");
|
||||||
@@ -908,11 +913,25 @@ bool overlay::SpiceOverlay::get_active() {
|
|||||||
return this->active;
|
return this->active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool overlay::SpiceOverlay::is_subscreen_overlay_visible() {
|
||||||
|
return this->get_active() &&
|
||||||
|
this->window_sub != nullptr &&
|
||||||
|
this->window_sub->get_active();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool overlay::SpiceOverlay::accepts_subscreen_mouse_input() {
|
||||||
|
return this->is_subscreen_overlay_visible() &&
|
||||||
|
this->window_sub->is_mouse_hovered();
|
||||||
|
}
|
||||||
|
|
||||||
bool overlay::SpiceOverlay::has_focus() {
|
bool overlay::SpiceOverlay::has_focus() {
|
||||||
return this->get_active() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow);
|
return this->get_active() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool overlay::SpiceOverlay::hotkeys_triggered() {
|
bool overlay::SpiceOverlay::hotkeys_triggered() {
|
||||||
|
const std::lock_guard<std::mutex> lock(this->hotkeys_mutex);
|
||||||
|
|
||||||
|
// this query also consumes the shared HotkeyToggle edge; the mutex gives all callers one latch
|
||||||
// prevent hotkeys in spicecfg
|
// prevent hotkeys in spicecfg
|
||||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ namespace overlay {
|
|||||||
void show_main_menu();
|
void show_main_menu();
|
||||||
void set_active(bool active);
|
void set_active(bool active);
|
||||||
bool get_active();
|
bool get_active();
|
||||||
|
bool accepts_subscreen_mouse_input();
|
||||||
bool has_focus();
|
bool has_focus();
|
||||||
bool hotkeys_triggered();
|
bool hotkeys_triggered();
|
||||||
|
|
||||||
@@ -143,20 +144,20 @@ namespace overlay {
|
|||||||
return this->hWnd;
|
return this->hWnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool can_transform_touch_input() {
|
bool has_subscreen_touch_transform() {
|
||||||
return (this->subscreen_mouse_handler != nullptr);
|
return (this->subscreen_touch_transform != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool transform_touch_point(LONG *x, LONG *y) {
|
bool transform_touch_point(LONG *x, LONG *y) {
|
||||||
if (this->get_active() && this->subscreen_mouse_handler) {
|
if (this->get_active() && this->subscreen_touch_transform) {
|
||||||
return this->subscreen_mouse_handler(x, y);
|
return this->subscreen_touch_transform(x, y);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_subscreen_mouse_handler(const std::function<bool(LONG *, LONG *)> &f) {
|
void set_subscreen_touch_transform(const std::function<bool(LONG *, LONG *)> &f) {
|
||||||
this->subscreen_mouse_handler = f;
|
this->subscreen_touch_transform = f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderer
|
// renderer
|
||||||
@@ -203,7 +204,7 @@ namespace overlay {
|
|||||||
|
|
||||||
std::vector<std::unique_ptr<Window>> windows;
|
std::vector<std::unique_ptr<Window>> windows;
|
||||||
|
|
||||||
std::function<bool(LONG *, LONG *)> subscreen_mouse_handler = nullptr;
|
std::function<bool(LONG *, LONG *)> subscreen_touch_transform = nullptr;
|
||||||
|
|
||||||
bool active = false;
|
bool active = false;
|
||||||
bool toggle_down = false;
|
bool toggle_down = false;
|
||||||
@@ -211,11 +212,13 @@ namespace overlay {
|
|||||||
bool fps_down = false;
|
bool fps_down = false;
|
||||||
bool hotkey_toggle = false;
|
bool hotkey_toggle = false;
|
||||||
bool hotkey_toggle_last = false;
|
bool hotkey_toggle_last = false;
|
||||||
|
std::mutex hotkeys_mutex;
|
||||||
|
|
||||||
// true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(),
|
// true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(),
|
||||||
// so render() never runs without a matching NewFrame.
|
// so render() never runs without a matching NewFrame.
|
||||||
bool has_pending_frame = false;
|
bool has_pending_frame = false;
|
||||||
|
|
||||||
|
bool is_subscreen_overlay_visible();
|
||||||
void init();
|
void init();
|
||||||
void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs);
|
void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs);
|
||||||
};
|
};
|
||||||
@@ -233,4 +236,5 @@ namespace overlay {
|
|||||||
#endif
|
#endif
|
||||||
void create_software(HWND hWnd);
|
void create_software(HWND hWnd);
|
||||||
void destroy(HWND hWnd = nullptr);
|
void destroy(HWND hWnd = nullptr);
|
||||||
|
bool global_hotkeys_triggered();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ void overlay::Window::build() {
|
|||||||
|
|
||||||
// check if active
|
// check if active
|
||||||
if (!this->active) {
|
if (!this->active) {
|
||||||
|
this->mouse_hovered = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,10 +107,18 @@ void overlay::Window::build() {
|
|||||||
// top spot. suppress that so only an explicit focus request (see below)
|
// top spot. suppress that so only an explicit focus request (see below)
|
||||||
// decides what comes to the front.
|
// decides what comes to the front.
|
||||||
const std::string window_id = this->title + "###" + to_string(this);
|
const std::string window_id = this->title + "###" + to_string(this);
|
||||||
if (ImGui::Begin(
|
const bool build_contents = ImGui::Begin(
|
||||||
window_id.c_str(),
|
window_id.c_str(),
|
||||||
&this->active,
|
&this->active,
|
||||||
this->flags | ImGuiWindowFlags_NoFocusOnAppearing)) {
|
this->flags | ImGuiWindowFlags_NoFocusOnAppearing);
|
||||||
|
|
||||||
|
// keep the window hovered while its invisible input button is held during a drag
|
||||||
|
this->mouse_hovered =
|
||||||
|
ImGui::IsWindowHovered(
|
||||||
|
ImGuiHoveredFlags_RootAndChildWindows |
|
||||||
|
ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
||||||
|
|
||||||
|
if (build_contents) {
|
||||||
|
|
||||||
// window attributes - init_pos / init_size are only honored once
|
// window attributes - init_pos / init_size are only honored once
|
||||||
// (ImGuiCond_Once), so compute them a single time instead of every frame
|
// (ImGuiCond_Once), so compute them a single time instead of every frame
|
||||||
@@ -154,6 +163,9 @@ void overlay::Window::build() {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
// raw content does not have an ImGui window to hover
|
||||||
|
this->mouse_hovered = false;
|
||||||
|
|
||||||
// add raw content
|
// add raw content
|
||||||
this->build_content();
|
this->build_content();
|
||||||
}
|
}
|
||||||
@@ -201,3 +213,7 @@ bool overlay::Window::get_active() {
|
|||||||
// now it depends on us
|
// now it depends on us
|
||||||
return this->active;
|
return this->active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool overlay::Window::is_mouse_hovered() {
|
||||||
|
return this->mouse_hovered;
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace overlay {
|
|||||||
void toggle_active();
|
void toggle_active();
|
||||||
void set_active(bool active);
|
void set_active(bool active);
|
||||||
bool get_active();
|
bool get_active();
|
||||||
|
bool is_mouse_hovered();
|
||||||
|
|
||||||
// raise this window to the top of the z-order on its next build. only
|
// raise this window to the top of the z-order on its next build. only
|
||||||
// call in response to an explicit user action (keyboard toggle, UI
|
// call in response to an explicit user action (keyboard toggle, UI
|
||||||
@@ -37,6 +38,7 @@ namespace overlay {
|
|||||||
// set when the window transitions to visible so build() raises it to the
|
// set when the window transitions to visible so build() raises it to the
|
||||||
// top of the z-order on the next frame
|
// top of the z-order on the next frame
|
||||||
bool request_focus = false;
|
bool request_focus = false;
|
||||||
|
bool mouse_hovered = false;
|
||||||
std::vector<Window*> children;
|
std::vector<Window*> children;
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
|
|||||||
@@ -82,9 +82,14 @@ namespace overlay::windows {
|
|||||||
ImGui::Text("Rendering");
|
ImGui::Text("Rendering");
|
||||||
|
|
||||||
// Media Type Selector
|
// Media Type Selector
|
||||||
int selectedMediaTypeIndex = selectedCamera->m_selectedMediaTypeIndex;
|
int selectedMediaTypeIndex = selectedCamera->GetSelectedMediaTypeIndex();
|
||||||
auto numMediaTypes = selectedCamera->m_mediaTypeInfos.size();
|
auto numMediaTypes = selectedCamera->m_mediaTypeInfos.size();
|
||||||
|
|
||||||
|
if (numMediaTypes == 0) {
|
||||||
|
ImGui::TextColored(ImVec4(1.f, 1.f, 0.f, 1.f), "%s", "No supported media types");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto selectedMediaTypeIndexChanged = ImGui::BeginCombo(
|
auto selectedMediaTypeIndexChanged = ImGui::BeginCombo(
|
||||||
"Media Type", selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).description.c_str()
|
"Media Type", selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).description.c_str()
|
||||||
);
|
);
|
||||||
@@ -103,9 +108,9 @@ namespace overlay::windows {
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedMediaTypeIndexChanged && selectedMediaTypeIndex != selectedCamera->m_selectedMediaTypeIndex) {
|
if (selectedMediaTypeIndexChanged && selectedMediaTypeIndex != selectedCamera->GetSelectedMediaTypeIndex()) {
|
||||||
selectedCamera->m_useAutoMediaType = false;
|
selectedCamera->m_useAutoMediaType = false;
|
||||||
selectedCamera->ChangeMediaType(selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).p_mediaType);
|
selectedCamera->RequestMediaType(selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).p_mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto media type
|
// Auto media type
|
||||||
@@ -114,13 +119,14 @@ namespace overlay::windows {
|
|||||||
if (ImGui::Checkbox("Auto##MediaType", &isAutoMediaType)) {
|
if (ImGui::Checkbox("Auto##MediaType", &isAutoMediaType)) {
|
||||||
selectedCamera->m_useAutoMediaType = isAutoMediaType;
|
selectedCamera->m_useAutoMediaType = isAutoMediaType;
|
||||||
if (isAutoMediaType) {
|
if (isAutoMediaType) {
|
||||||
selectedCamera->ChangeMediaType(selectedCamera->m_pAutoMediaType);
|
selectedCamera->RequestMediaType(selectedCamera->m_pAutoMediaType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw mode
|
// Draw mode
|
||||||
int selectedDrawModeIndex = selectedCamera->m_drawMode;
|
const auto drawMode = selectedCamera->m_drawMode.load();
|
||||||
if (ImGui::BeginCombo("Draw Mode", DRAW_MODE_LABELS[selectedCamera->m_drawMode].c_str())) {
|
int selectedDrawModeIndex = drawMode;
|
||||||
|
if (ImGui::BeginCombo("Draw Mode", DRAW_MODE_LABELS[drawMode].c_str())) {
|
||||||
for (size_t i = 0; i < DRAW_MODE_SIZE; i++) {
|
for (size_t i = 0; i < DRAW_MODE_SIZE; i++) {
|
||||||
const bool is_selected = (selectedDrawModeIndex == (int) i);
|
const bool is_selected = (selectedDrawModeIndex == (int) i);
|
||||||
if (ImGui::Selectable(DRAW_MODE_LABELS[i].c_str(), is_selected)) {
|
if (ImGui::Selectable(DRAW_MODE_LABELS[i].c_str(), is_selected)) {
|
||||||
@@ -142,15 +148,25 @@ namespace overlay::windows {
|
|||||||
"Letterbox to 4:3: Like Letterbox, but target 4:3"
|
"Letterbox to 4:3: Like Letterbox, but target 4:3"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedDrawModeIndex != selectedCamera->m_drawMode) {
|
if (selectedDrawModeIndex != selectedCamera->m_drawMode.load()) {
|
||||||
selectedCamera->m_drawMode = (LocalCameraDrawMode)selectedDrawModeIndex;
|
selectedCamera->m_drawMode.store((LocalCameraDrawMode)selectedDrawModeIndex);
|
||||||
selectedCamera->UpdateDrawRect();
|
selectedCamera->UpdateDrawRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::Checkbox("Horizontal Flip", &selectedCamera->m_flipHorizontal);
|
bool flipHorizontal = selectedCamera->m_flipHorizontal.load();
|
||||||
|
if (ImGui::Checkbox("Horizontal Flip", &flipHorizontal)) {
|
||||||
|
selectedCamera->m_flipHorizontal.store(flipHorizontal);
|
||||||
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::Checkbox("Vertical Flip", &selectedCamera->m_flipVertical);
|
bool flipVertical = selectedCamera->m_flipVertical.load();
|
||||||
|
if (ImGui::Checkbox("Vertical Flip", &flipVertical)) {
|
||||||
|
selectedCamera->m_flipVertical.store(flipVertical);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::HelpMarker(
|
||||||
|
"Flips are done by the CPU and can be very expensive, potentially causing frame drops! "
|
||||||
|
"Prefer to use camera's built-in flip if available.");
|
||||||
|
|
||||||
// Camera control parameters
|
// Camera control parameters
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ namespace overlay::windows {
|
|||||||
const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f);
|
const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f);
|
||||||
const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f);
|
const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f);
|
||||||
|
|
||||||
|
// a subscreen is an already-composited backbuffer, so its alpha bits are not
|
||||||
|
// meaningful for display. Use AddImage's diffuse alpha (opaque by default)
|
||||||
|
// instead of the texture alpha while preserving ImGui's normal blend state.
|
||||||
|
static void ignore_texture_alpha(const ImDrawList *, const ImDrawCmd *command) {
|
||||||
|
auto device = static_cast<IDirect3DDevice9 *>(command->UserCallbackData);
|
||||||
|
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
|
||||||
|
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2);
|
||||||
|
}
|
||||||
|
|
||||||
GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) {
|
GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) {
|
||||||
this->remove_window_padding = true;
|
this->remove_window_padding = true;
|
||||||
// ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image
|
// ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image
|
||||||
@@ -42,7 +51,7 @@ namespace overlay::windows {
|
|||||||
this->texture_width = 0;
|
this->texture_width = 0;
|
||||||
this->texture_height = 0;
|
this->texture_height = 0;
|
||||||
|
|
||||||
overlay->set_subscreen_mouse_handler([this](LONG *x, LONG *y) -> bool {
|
overlay->set_subscreen_touch_transform([this](LONG *x, LONG *y) -> bool {
|
||||||
// convert to normalized form (relative window coordinates 0.f-1.f)
|
// convert to normalized form (relative window coordinates 0.f-1.f)
|
||||||
ImVec2 xy;
|
ImVec2 xy;
|
||||||
|
|
||||||
@@ -239,10 +248,13 @@ namespace overlay::windows {
|
|||||||
auto draw_list = this->draws_window
|
auto draw_list = this->draws_window
|
||||||
? ImGui::GetWindowDrawList()
|
? ImGui::GetWindowDrawList()
|
||||||
: ImGui::GetBackgroundDrawList();
|
: ImGui::GetBackgroundDrawList();
|
||||||
|
draw_list->AddCallback(ignore_texture_alpha, this->device);
|
||||||
draw_list->AddImage(
|
draw_list->AddImage(
|
||||||
reinterpret_cast<ImTextureID>(this->texture),
|
reinterpret_cast<ImTextureID>(this->texture),
|
||||||
overlay_content_top_left,
|
overlay_content_top_left,
|
||||||
bottom_right);
|
bottom_right);
|
||||||
|
// following ImGui draws should resume normal texture-alpha modulation
|
||||||
|
draw_list->AddCallback(ImGui::GetPlatformIO().DrawCallback_ResetRenderState);
|
||||||
|
|
||||||
if (this->draws_window) {
|
if (this->draws_window) {
|
||||||
// draw an invisible button so that it swallows mouse input
|
// draw an invisible button so that it swallows mouse input
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user