diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..2ff24654 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,830 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) + +# Print a message that using the Makefiles is recommended. +message(NOTICE: " The CMakeLists.txt is unmaintained. Use the Makefile if possible.") + +# Enforce "Debug" as standard build type. +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE) +endif() + +# CMake project configuration. +project(yquake2 C) + +# Cmake module search path. +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/stuff/cmake/modules ${CMAKE_MODULE_PATH}) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED OFF) + +if(YQUAKE2LIBS) + if(CMAKE_CROSSCOMPILING) + set(CMAKE_FIND_ROOT_PATH ${YQUAKE2LIBS}) + else() + set(ENV{CMAKE_PREFIX_PATH} ${YQUAKE2LIBS}) + endif() + + set(ENV{OPENALDIR} ${YQUAKE2LIBS}) + set(ENV{SDL2DIR} ${YQUAKE2LIBS}) +endif() + +# Add extended path for FreeBSD and Homebrew on OS X. +list(APPEND CMAKE_PREFIX_PATH /usr/local) + +if (MSVC) + add_compile_options(/MP) # parallel build (use all cores, or as many as configured in VS) + + # ignore some compiler warnings + add_compile_options(/wd4244 /wd4305) # possible loss of data/truncation (double to float etc; ignore) + add_compile_options(/wd4018) # signed/unsigned mismatch + add_compile_options(/wd4996) # 'function': was declared deprecated (like all that secure CRT stuff) + # don't show me warnings for system headers, why the fuck isn't this default + add_compile_options(/experimental:external /external:W0) +else() # GCC/clang/mingw +# Enforce compiler flags: +# -Wall -> More warnings +# -fno-strict-aliasing -> Quake 2 is far away from strict aliasing +# -fwrapv -> Make signed integer overflows defined +# -fvisibility=hidden -> Force defaultsymbol visibility to hidden +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -fno-strict-aliasing -fwrapv -fvisibility=hidden") + +# Use -O2 as maximum optimization level. -O3 has it's problems with yquake2. +string(REPLACE "-O3" "-O2" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") +endif() # MSVC'S else-case + +# Switch off some annoying warnings +if (${CMAKE_C_COMPILER_ID} STREQUAL "Clang") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-missing-braces") +elseif (${CMAKE_C_COMPILER_ID} STREQUAL "GNU") + if (CMAKE_C_COMPILER_VERSION GREATER 7.99) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-format-truncation -Wno-format-overflow") + endif() +endif() + +# Compilation time options. +option(CURL_SUPPORT "cURL support" ON) +option(OPENAL_SUPPORT "OpenAL support" ON) +option(SYSTEMWIDE_SUPPORT "Enable systemwide installation of game assets" OFF) +option(SDL3_SUPPORT "Build against SDL 3 instead of SDL2" OFF) + +set(SYSTEMDIR "" CACHE STRING "Override the system default directory") + +# These variables will act as our list of include folders and linker flags. +set(yquake2IncludeDirectories) +set(yquake2LinkerDirectories) +set(yquake2LinkerFlags) +set(yquake2ClientLinkerFlags) +set(yquake2ServerLinkerFlags) +set(yquake2OpenGLLinkerFlags) +set(yquake2VulkanLinkerFlags) +set(yquake2SDLLinkerFlags) +set(yquake2ZLibLinkerFlags) + +# Set directory locations (allowing us to move directories easily) +set(SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/src) +set(BACKENDS_SRC_DIR ${SOURCE_DIR}/backends) +set(COMMON_SRC_DIR ${SOURCE_DIR}/common) +set(GAME_SRC_DIR ${SOURCE_DIR}/game) +set(SERVER_SRC_DIR ${SOURCE_DIR}/server) +set(CLIENT_SRC_DIR ${SOURCE_DIR}/client) +set(REF_SRC_DIR ${SOURCE_DIR}/client/refresh) + +# Operating system. +set(YQ2OSTYPE "${CMAKE_SYSTEM_NAME}" CACHE STRING "Override operation system type") +add_definitions(-DYQ2OSTYPE="${YQ2OSTYPE}") + +# Architecture string +# work around CMake's useless/broken CMAKE_SYSTEM_PROCESSOR (taken from dhewm3) + +set(cpu ${CMAKE_SYSTEM_PROCESSOR}) + +# Originally, ${CMAKE_SYSTEM_PROCESSOR} was supposed to contain the *target* CPU, according to CMake's documentation. +# As far as I can tell this has always been broken (always returns host CPU) at least on Windows +# (see e.g. https://cmake.org/pipermail/cmake-developers/2014-September/011405.html) and wasn't reliable on +# other systems either, for example on Linux with 32bit userland but 64bit kernel it returned the kernel CPU type +# (e.g. x86_64 instead of i686). Instead of fixing this, CMake eventually updated their documentation in 3.20, +# now it's officially the same as CMAKE_HOST_SYSTEM_PROCESSOR except when cross-compiling (where it's explicitly set) +# So we gotta figure out the actual target CPU type ourselves.. +if(NOT (CMAKE_SYSTEM_PROCESSOR STREQUAL CMAKE_HOST_SYSTEM_PROCESSOR)) + # special case: cross-compiling, here CMAKE_SYSTEM_PROCESSOR should be correct, hopefully + # (just leave cpu at ${CMAKE_SYSTEM_PROCESSOR}) +elseif(MSVC) + # because all this wasn't ugly enough, it turned out that, unlike standalone CMake, Visual Studio's + # integrated CMake doesn't set CMAKE_GENERATOR_PLATFORM, so I gave up on guessing the CPU arch here + # and moved the CPU detection to MSVC-specific code in neo/sys/platform.h +else() # not MSVC and not cross-compiling, assume GCC or clang (-compatible), seems to work for MinGW as well + execute_process(COMMAND ${CMAKE_C_COMPILER} "-dumpmachine" + RESULT_VARIABLE cc_dumpmachine_res + OUTPUT_VARIABLE cc_dumpmachine_out) + if(cc_dumpmachine_res EQUAL 0) + string(STRIP ${cc_dumpmachine_out} cc_dumpmachine_out) # get rid of trailing newline + message(DEBUG "`${CMAKE_C_COMPILER} -dumpmachine` says: \"${cc_dumpmachine_out}\"") + # gcc -dumpmachine and clang -dumpmachine seem to print something like "x86_64-linux-gnu" (gcc) + # or "x64_64-pc-linux-gnu" (clang) or "i686-w64-mingw32" (32bit mingw-w64) i.e. starting with the CPU, + # then "-" and then OS or whatever - so use everything up to first "-" + string(REGEX MATCH "^[^-]+" cpu ${cc_dumpmachine_out}) + message(DEBUG " => CPU architecture extracted from that: \"${cpu}\"") + else() + message(WARNING "${CMAKE_C_COMPILER} -dumpmachine failed with error (code) ${cc_dumpmachine_res}") + message(WARNING "will use the (sometimes incorrect) CMAKE_SYSTEM_PROCESSOR (${cpu}) to determine YQ2ARCH") + endif() +endif() + +if(cpu STREQUAL "powerpc") + set(cpu "ppc") +elseif(cpu STREQUAL "aarch64") + # "arm64" is more obvious, and some operating systems (like macOS) use it instead of "aarch64" + set(cpu "arm64") +elseif(cpu MATCHES "[aA][mM][dD]64" OR cpu MATCHES "[xX].*64") + set(cpu "x86_64") +elseif(cpu MATCHES "i.86" OR cpu MATCHES "[xX]86") + set(cpu "i386") +elseif(cpu MATCHES "[aA][rR][mM].*") # some kind of arm.. + # On 32bit Raspbian gcc -dumpmachine returns sth starting with "arm-", + # while clang -dumpmachine says "arm6k-..." - try to unify that to "arm" + if(CMAKE_SIZEOF_VOID_P EQUAL 8) # sizeof(void*) == 8 => must be arm64 + set(cpu "arm64") + else() # should be 32bit arm then (probably "armv7l" "armv6k" or sth like that) + set(cpu "arm") + endif() +endif() + +if(MSVC) + # for MSVC YQ2ARCH is set in code (in src/common/header/common.h) + message(STATUS "Setting YQ2OSTYPE to \"${YQ2OSTYPE}\" - NOT setting YQ2ARCH, because we're targeting MSVC (VisualC++)") +else() + set(ARCH "${cpu}") + add_definitions(-DYQ2ARCH="${ARCH}") + message(STATUS "Setting YQ2OSTYPE to \"${YQ2OSTYPE}\" and YQ2ARCH to \"${ARCH}\".") +endif() +# make sure that ${cpu} isn't used below - if at all use ${ARCH}, but not when compiling with MSVC! +unset(cpu) + +# END OF workarounds for CMake's poor choices regarding CPU architecture detection + + +# Systemwide installation of game assets. +if(${SYSTEMWIDE_SUPPORT}) + add_definitions(-DSYSTEMWIDE) + if(NOT ${SYSTEMDIR} STREQUAL "") + add_definitions(-DSYSTEMDIR="${SYSTEMDIR}") + endif() +endif() + +# We need to pass some options to minizip / unzip. +add_definitions(-DNOUNCRYPT) + +if(NOT (CMAKE_SYSTEM_NAME MATCHES "Linux") AND NOT (CMAKE_SYSTEM_NAME MATCHES "Windows")) + add_definitions(-DIOAPI_NO_64) +endif() + +# Required libraries to build the different components of the binaries. Find +# them and add the include/linker directories and flags (in case the package +# manager find it in a weird place). +if (SDL3_SUPPORT) + find_package(SDL3 REQUIRED) + add_definitions(-DUSE_SDL3) +else() + find_package(SDL2 REQUIRED) + list(APPEND yquake2IncludeDirectories "${SDL2_INCLUDE_DIR}/..") + list(APPEND yquake2SDLLinkerFlags ${SDL2_LIBRARY}) +endif() + +# We need an OpenGL implementation. +set(OpenGL_GL_PREFERENCE GLVND) +find_package(OpenGL REQUIRED) +list(APPEND yquake2IncludeDirectories ${OPENGL_INCLUDE_DIR}) +list(APPEND yquake2OpenGLLinkerFlags ${OPENGL_LIBRARIES}) + +# backtrace lookup +# Some systems like Linux has it within the libc some like the BSD, Haiku ... +# into an external libexecinfo library +include(CheckFunctionExists) +include(CheckLibraryExists) +check_function_exists(backtrace HAVE_EXECINFO_SYS) +IF (NOT HAVE_EXECINFO_SYS) + check_library_exists(execinfo backtrace "" HAVE_EXECINFO_LIB) + if (HAVE_EXECINFO_LIB) + list(APPEND yquake2ClientLinkerFlags execinfo) + list(APPEND yquake2ServerLinkerFlags execinfo) + add_definitions(-DHAVE_EXECINFO) + endif() +else() + add_definitions(-DHAVE_EXECINFO) +endif() + +# cURL support. +if (${CURL_SUPPORT}) + find_package(CURL REQUIRED) + add_definitions(-DUSE_CURL) +endif() + +# OpenAL support. +if(${OPENAL_SUPPORT}) + find_package(OpenAL) + + if(${OPENAL_FOUND}) + list(APPEND yquake2IncludeDirectories "${OPENAL_INCLUDE_DIR}") + list(APPEND yquake2ClientLinkerFlags ${OPENAL_LIBRARY}) + + if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + add_definitions(-DUSE_OPENAL -DDEFAULT_OPENAL_DRIVER="openal32.dll") + elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + add_definitions(-DUSE_OPENAL -DDEFAULT_OPENAL_DRIVER="libopenal.dylib") + elseif((${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") OR (${CMAKE_SYSTEM_NAME} MATCHES "OpenBSD")) + add_definitions(-DUSE_OPENAL -DDEFAULT_OPENAL_DRIVER="libopenal.so") + else() + add_definitions(-DUSE_OPENAL -DDEFAULT_OPENAL_DRIVER="libopenal.so.1") + endif() + endif() +endif() + +# General linker flags. +if(NOT MSVC) + list(APPEND yquake2LinkerFlags m) +endif() +list(APPEND yquake2LinkerFlags ${CMAKE_DL_LIBS}) + +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + if(!MSVC) + list(APPEND yquake2LinkerFlags "-static-libgcc") + endif() +else() + if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Haiku") + list(APPEND yquake2LinkerFlags "-rdynamic") + else() + list(APPEND yquake2LinkerFlags "-lnetwork") + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + endif() + if (${CMAKE_SYSTEM_NAME} MATCHES "SunOS") + list(APPEND yquake2LinkerFlags "-lsocket -lnsl") + endif() +endif() + +if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin" AND NOT ${CMAKE_SYSTEM_NAME} MATCHES "OpenBSD" AND NOT WIN32) + list(APPEND yquake2LinkerFlags "-Wl,--no-undefined") +endif() + +# With all of those libraries and user defined paths +# added, lets give them to the compiler and linker. +include_directories(${yquake2IncludeDirectories}) +link_directories(${yquake2LinkerDirectories}) + +# these settings only work for GCC and clang +if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_C_COMPILER_ID STREQUAL "Clang") + # If we're building with gcc for i386 let's define -ffloat-store. + # This helps the old and crappy x87 FPU to produce correct values. + # Would be nice if Clang had something comparable. + if ("${ARCH}" STREQUAL "i386" AND ${CMAKE_C_COMPILER_ID} STREQUAL "GNU") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffloat-store") + endif() + + # Force SSE math on x86_64. All sane compilers should do this + # anyway, just to protect us from broken Linux distros. + if ("${ARCH}" STREQUAL "x86_64") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mfpmath=sse") + endif() + + if ("${ARCH}" STREQUAL "arm") + if (CMAKE_SIZEOF_VOID_P EQUAL 4) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv6k") + endif() + endif() +endif() + +set(Backends-Generic-Source + ${BACKENDS_SRC_DIR}/generic/misc.c + ) + +set(Backends-Unix-Source + ${BACKENDS_SRC_DIR}/unix/main.c + ${BACKENDS_SRC_DIR}/unix/network.c + ${BACKENDS_SRC_DIR}/unix/signalhandler.c + ${BACKENDS_SRC_DIR}/unix/system.c + ${BACKENDS_SRC_DIR}/unix/shared/hunk.c + ) + +set(Backends-Windows-Source + ${BACKENDS_SRC_DIR}/windows/icon.rc + ${BACKENDS_SRC_DIR}/windows/main.c + ${BACKENDS_SRC_DIR}/windows/network.c + ${BACKENDS_SRC_DIR}/windows/system.c + ${BACKENDS_SRC_DIR}/windows/shared/hunk.c + ) + +set(Backends-Windows-Header + ${BACKENDS_SRC_DIR}/windows/header/resource.h + ) + +set(REF-Windows-Source + ${BACKENDS_SRC_DIR}/windows/shared/hunk.c + ) + +set(REF-Unix-Source + ${BACKENDS_SRC_DIR}/unix/shared/hunk.c + ) + +# Set the nessesary platform specific source +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + set(Platform-Specific-Source ${Backends-Windows-Source} ${Backends-Windows-Header}) + set(REF-Platform-Specific-Source ${REF-Windows-Source}) +else() + set(Platform-Specific-Source ${Backends-Unix-Source}) + set(REF-Platform-Specific-Source ${REF-Unix-Source}) +endif() + +set(Game-Source + ${COMMON_SRC_DIR}/shared/flash.c + ${COMMON_SRC_DIR}/shared/rand.c + ${COMMON_SRC_DIR}/shared/shared.c + ${GAME_SRC_DIR}/g_ai.c + ${GAME_SRC_DIR}/g_chase.c + ${GAME_SRC_DIR}/g_cmds.c + ${GAME_SRC_DIR}/g_combat.c + ${GAME_SRC_DIR}/g_func.c + ${GAME_SRC_DIR}/g_items.c + ${GAME_SRC_DIR}/g_main.c + ${GAME_SRC_DIR}/g_misc.c + ${GAME_SRC_DIR}/g_monster.c + ${GAME_SRC_DIR}/g_phys.c + ${GAME_SRC_DIR}/g_spawn.c + ${GAME_SRC_DIR}/g_svcmds.c + ${GAME_SRC_DIR}/g_target.c + ${GAME_SRC_DIR}/g_trigger.c + ${GAME_SRC_DIR}/g_turret.c + ${GAME_SRC_DIR}/g_utils.c + ${GAME_SRC_DIR}/g_weapon.c + ${GAME_SRC_DIR}/monster/berserker/berserker.c + ${GAME_SRC_DIR}/monster/boss2/boss2.c + ${GAME_SRC_DIR}/monster/boss3/boss3.c + ${GAME_SRC_DIR}/monster/boss3/boss31.c + ${GAME_SRC_DIR}/monster/boss3/boss32.c + ${GAME_SRC_DIR}/monster/brain/brain.c + ${GAME_SRC_DIR}/monster/chick/chick.c + ${GAME_SRC_DIR}/monster/flipper/flipper.c + ${GAME_SRC_DIR}/monster/float/float.c + ${GAME_SRC_DIR}/monster/flyer/flyer.c + ${GAME_SRC_DIR}/monster/gladiator/gladiator.c + ${GAME_SRC_DIR}/monster/gunner/gunner.c + ${GAME_SRC_DIR}/monster/hover/hover.c + ${GAME_SRC_DIR}/monster/infantry/infantry.c + ${GAME_SRC_DIR}/monster/insane/insane.c + ${GAME_SRC_DIR}/monster/medic/medic.c + ${GAME_SRC_DIR}/monster/misc/move.c + ${GAME_SRC_DIR}/monster/mutant/mutant.c + ${GAME_SRC_DIR}/monster/parasite/parasite.c + ${GAME_SRC_DIR}/monster/soldier/soldier.c + ${GAME_SRC_DIR}/monster/supertank/supertank.c + ${GAME_SRC_DIR}/monster/tank/tank.c + ${GAME_SRC_DIR}/player/client.c + ${GAME_SRC_DIR}/player/hud.c + ${GAME_SRC_DIR}/player/trail.c + ${GAME_SRC_DIR}/player/view.c + ${GAME_SRC_DIR}/player/weapon.c + ${GAME_SRC_DIR}/savegame/savegame.c + ) + +set(Game-Header + ${GAME_SRC_DIR}/header/game.h + ${GAME_SRC_DIR}/header/local.h + ${GAME_SRC_DIR}/monster/berserker/berserker.h + ${GAME_SRC_DIR}/monster/boss2/boss2.h + ${GAME_SRC_DIR}/monster/boss3/boss31.h + ${GAME_SRC_DIR}/monster/boss3/boss32.h + ${GAME_SRC_DIR}/monster/brain/brain.h + ${GAME_SRC_DIR}/monster/chick/chick.h + ${GAME_SRC_DIR}/monster/flipper/flipper.h + ${GAME_SRC_DIR}/monster/float/float.h + ${GAME_SRC_DIR}/monster/flyer/flyer.h + ${GAME_SRC_DIR}/monster/gladiator/gladiator.h + ${GAME_SRC_DIR}/monster/gunner/gunner.h + ${GAME_SRC_DIR}/monster/hover/hover.h + ${GAME_SRC_DIR}/monster/infantry/infantry.h + ${GAME_SRC_DIR}/monster/insane/insane.h + ${GAME_SRC_DIR}/monster/medic/medic.h + ${GAME_SRC_DIR}/monster/misc/player.h + ${GAME_SRC_DIR}/monster/mutant/mutant.h + ${GAME_SRC_DIR}/monster/parasite/parasite.h + ${GAME_SRC_DIR}/monster/soldier/soldier.h + ${GAME_SRC_DIR}/monster/supertank/supertank.h + ${GAME_SRC_DIR}/monster/tank/tank.h + ${GAME_SRC_DIR}/savegame/tables/clientfields.h + ${GAME_SRC_DIR}/savegame/tables/fields.h + ${GAME_SRC_DIR}/savegame/tables/gamefunc_decs.h + ${GAME_SRC_DIR}/savegame/tables/gamefunc_list.h + ${GAME_SRC_DIR}/savegame/tables/gamemmove_decs.h + ${GAME_SRC_DIR}/savegame/tables/gamemmove_list.h + ${GAME_SRC_DIR}/savegame/tables/levelfields.h + ) + +set(Client-Source + ${CLIENT_SRC_DIR}/cl_cin.c + ${CLIENT_SRC_DIR}/cl_console.c + ${CLIENT_SRC_DIR}/cl_download.c + ${CLIENT_SRC_DIR}/cl_effects.c + ${CLIENT_SRC_DIR}/cl_entities.c + ${CLIENT_SRC_DIR}/cl_input.c + ${CLIENT_SRC_DIR}/cl_inventory.c + ${CLIENT_SRC_DIR}/cl_keyboard.c + ${CLIENT_SRC_DIR}/cl_lights.c + ${CLIENT_SRC_DIR}/cl_main.c + ${CLIENT_SRC_DIR}/cl_network.c + ${CLIENT_SRC_DIR}/cl_parse.c + ${CLIENT_SRC_DIR}/cl_particles.c + ${CLIENT_SRC_DIR}/cl_prediction.c + ${CLIENT_SRC_DIR}/cl_screen.c + ${CLIENT_SRC_DIR}/cl_tempentities.c + ${CLIENT_SRC_DIR}/cl_view.c + ${CLIENT_SRC_DIR}/curl/download.c + ${CLIENT_SRC_DIR}/curl/qcurl.c + ${CLIENT_SRC_DIR}/menu/menu.c + ${CLIENT_SRC_DIR}/menu/qmenu.c + ${CLIENT_SRC_DIR}/menu/videomenu.c + ${CLIENT_SRC_DIR}/sound/ogg.c + ${CLIENT_SRC_DIR}/sound/openal.c + ${CLIENT_SRC_DIR}/sound/qal.c + ${CLIENT_SRC_DIR}/sound/sdl.c + ${CLIENT_SRC_DIR}/sound/sound.c + ${CLIENT_SRC_DIR}/sound/wave.c + ${CLIENT_SRC_DIR}/vid/vid.c + ${COMMON_SRC_DIR}/argproc.c + ${COMMON_SRC_DIR}/clientserver.c + ${COMMON_SRC_DIR}/collision.c + ${COMMON_SRC_DIR}/crc.c + ${COMMON_SRC_DIR}/cmdparser.c + ${COMMON_SRC_DIR}/cvar.c + ${COMMON_SRC_DIR}/filesystem.c + ${COMMON_SRC_DIR}/glob.c + ${COMMON_SRC_DIR}/md4.c + ${COMMON_SRC_DIR}/movemsg.c + ${COMMON_SRC_DIR}/frame.c + ${COMMON_SRC_DIR}/netchan.c + ${COMMON_SRC_DIR}/pmove.c + ${COMMON_SRC_DIR}/szone.c + ${COMMON_SRC_DIR}/zone.c + ${COMMON_SRC_DIR}/shared/flash.c + ${COMMON_SRC_DIR}/shared/rand.c + ${COMMON_SRC_DIR}/shared/shared.c + ${COMMON_SRC_DIR}/unzip/ioapi.c + ${COMMON_SRC_DIR}/unzip/unzip.c + ${COMMON_SRC_DIR}/unzip/miniz/miniz.c + ${COMMON_SRC_DIR}/unzip/miniz/miniz_tdef.c + ${COMMON_SRC_DIR}/unzip/miniz/miniz_tinfl.c + ${SERVER_SRC_DIR}/sv_cmd.c + ${SERVER_SRC_DIR}/sv_conless.c + ${SERVER_SRC_DIR}/sv_entities.c + ${SERVER_SRC_DIR}/sv_game.c + ${SERVER_SRC_DIR}/sv_init.c + ${SERVER_SRC_DIR}/sv_main.c + ${SERVER_SRC_DIR}/sv_save.c + ${SERVER_SRC_DIR}/sv_send.c + ${SERVER_SRC_DIR}/sv_user.c + ${SERVER_SRC_DIR}/sv_world.c + ) + +if(SDL3_SUPPORT) + set(Client-SDL-Source + ${CLIENT_SRC_DIR}/input/sdl3.c + ${CLIENT_SRC_DIR}/vid/glimp_sdl3.c + ) +else() + set(Client-SDL-Source + ${CLIENT_SRC_DIR}/input/sdl2.c + ${CLIENT_SRC_DIR}/vid/glimp_sdl2.c + ) +endif() + +set(Client-Header + ${CLIENT_SRC_DIR}/header/client.h + ${CLIENT_SRC_DIR}/header/console.h + ${CLIENT_SRC_DIR}/header/keyboard.h + ${CLIENT_SRC_DIR}/header/screen.h + ${CLIENT_SRC_DIR}/curl/header/download.h + ${CLIENT_SRC_DIR}/curl/header/qcurl.h + ${CLIENT_SRC_DIR}/input/header/input.h + ${CLIENT_SRC_DIR}/menu/header/qmenu.h + ${CLIENT_SRC_DIR}/sound/header/local.h + ${CLIENT_SRC_DIR}/sound/header/qal.h + ${CLIENT_SRC_DIR}/sound/header/sound.h + ${CLIENT_SRC_DIR}/sound/header/stb_vorbis.h + ${CLIENT_SRC_DIR}/sound/header/vorbis.h + ${CLIENT_SRC_DIR}/vid/header/ref.h + ${CLIENT_SRC_DIR}/vid/header/stb_image_write.h + ${CLIENT_SRC_DIR}/vid/header/vid.h + ${COMMON_SRC_DIR}/header/common.h + ${COMMON_SRC_DIR}/header/crc.h + ${COMMON_SRC_DIR}/header/files.h + ${COMMON_SRC_DIR}/header/glob.h + ${COMMON_SRC_DIR}/header/shared.h + ${COMMON_SRC_DIR}/header/zone.h + ${COMMON_SRC_DIR}/unzip/ioapi.h + ${COMMON_SRC_DIR}/unzip/unzip.h + ${COMMON_SRC_DIR}/unzip/miniz/miniz.h + ${COMMON_SRC_DIR}/unzip/miniz/miniz_tdef.h + ${COMMON_SRC_DIR}/unzip/miniz/miniz_tinfl.h + ${COMMON_SRC_DIR}/unzip/miniz/minizconf.h + ${SERVER_SRC_DIR}/header/server.h + ) + +set(Server-Source + ${COMMON_SRC_DIR}/argproc.c + ${COMMON_SRC_DIR}/clientserver.c + ${COMMON_SRC_DIR}/collision.c + ${COMMON_SRC_DIR}/crc.c + ${COMMON_SRC_DIR}/cmdparser.c + ${COMMON_SRC_DIR}/cvar.c + ${COMMON_SRC_DIR}/filesystem.c + ${COMMON_SRC_DIR}/glob.c + ${COMMON_SRC_DIR}/md4.c + ${COMMON_SRC_DIR}/frame.c + ${COMMON_SRC_DIR}/movemsg.c + ${COMMON_SRC_DIR}/netchan.c + ${COMMON_SRC_DIR}/pmove.c + ${COMMON_SRC_DIR}/szone.c + ${COMMON_SRC_DIR}/zone.c + ${COMMON_SRC_DIR}/shared/rand.c + ${COMMON_SRC_DIR}/shared/shared.c + ${COMMON_SRC_DIR}/unzip/ioapi.c + ${COMMON_SRC_DIR}/unzip/unzip.c + ${COMMON_SRC_DIR}/unzip/miniz/miniz.c + ${COMMON_SRC_DIR}/unzip/miniz/miniz_tdef.c + ${COMMON_SRC_DIR}/unzip/miniz/miniz_tinfl.c + ${SERVER_SRC_DIR}/sv_cmd.c + ${SERVER_SRC_DIR}/sv_conless.c + ${SERVER_SRC_DIR}/sv_entities.c + ${SERVER_SRC_DIR}/sv_game.c + ${SERVER_SRC_DIR}/sv_init.c + ${SERVER_SRC_DIR}/sv_main.c + ${SERVER_SRC_DIR}/sv_save.c + ${SERVER_SRC_DIR}/sv_send.c + ${SERVER_SRC_DIR}/sv_user.c + ${SERVER_SRC_DIR}/sv_world.c + ) + +set(Server-Header + ${COMMON_SRC_DIR}/header/common.h + ${COMMON_SRC_DIR}/header/crc.h + ${COMMON_SRC_DIR}/header/files.h + ${COMMON_SRC_DIR}/header/glob.h + ${COMMON_SRC_DIR}/header/shared.h + ${COMMON_SRC_DIR}/header/zone.h + ${COMMON_SRC_DIR}/unzip/ioapi.h + ${COMMON_SRC_DIR}/unzip/unzip.h + ${COMMON_SRC_DIR}/unzip/miniz/miniz.h + ${COMMON_SRC_DIR}/unzip/miniz/miniz_tdef.h + ${COMMON_SRC_DIR}/unzip/miniz/miniz_tinfl.h + ${COMMON_SRC_DIR}/unzip/miniz/minizconf.h + ${SERVER_SRC_DIR}/header/server.h + ) + +set(GL1-Source + ${REF_SRC_DIR}/gl1/qgl.c + ${REF_SRC_DIR}/gl1/gl1_draw.c + ${REF_SRC_DIR}/gl1/gl1_image.c + ${REF_SRC_DIR}/gl1/gl1_light.c + ${REF_SRC_DIR}/gl1/gl1_lightmap.c + ${REF_SRC_DIR}/gl1/gl1_main.c + ${REF_SRC_DIR}/gl1/gl1_mesh.c + ${REF_SRC_DIR}/gl1/gl1_misc.c + ${REF_SRC_DIR}/gl1/gl1_model.c + ${REF_SRC_DIR}/gl1/gl1_scrap.c + ${REF_SRC_DIR}/gl1/gl1_surf.c + ${REF_SRC_DIR}/gl1/gl1_warp.c + ${REF_SRC_DIR}/gl1/gl1_sdl.c + ${REF_SRC_DIR}/files/models.c + ${REF_SRC_DIR}/files/pcx.c + ${REF_SRC_DIR}/files/stb.c + ${REF_SRC_DIR}/files/surf.c + ${REF_SRC_DIR}/files/wal.c + ${REF_SRC_DIR}/files/pvs.c + ${COMMON_SRC_DIR}/shared/shared.c + ${COMMON_SRC_DIR}/md4.c + ) + +set(GL1-Header + ${REF_SRC_DIR}/ref_shared.h + ${REF_SRC_DIR}/constants/anorms.h + ${REF_SRC_DIR}/constants/anormtab.h + ${REF_SRC_DIR}/constants/warpsin.h + ${REF_SRC_DIR}/files/stb_image.h + ${REF_SRC_DIR}/files/surf.c + ${REF_SRC_DIR}/gl1/header/local.h + ${REF_SRC_DIR}/gl1/header/model.h + ${REF_SRC_DIR}/gl1/header/qgl.h + ${COMMON_SRC_DIR}/header/shared.h + ) + +set(GL3-Source + ${REF_SRC_DIR}/gl3/gl3_draw.c + ${REF_SRC_DIR}/gl3/gl3_image.c + ${REF_SRC_DIR}/gl3/gl3_light.c + ${REF_SRC_DIR}/gl3/gl3_lightmap.c + ${REF_SRC_DIR}/gl3/gl3_main.c + ${REF_SRC_DIR}/gl3/gl3_mesh.c + ${REF_SRC_DIR}/gl3/gl3_misc.c + ${REF_SRC_DIR}/gl3/gl3_model.c + ${REF_SRC_DIR}/gl3/gl3_sdl.c + ${REF_SRC_DIR}/gl3/gl3_surf.c + ${REF_SRC_DIR}/gl3/gl3_warp.c + ${REF_SRC_DIR}/gl3/gl3_shaders.c + ${REF_SRC_DIR}/files/models.c + ${REF_SRC_DIR}/files/pcx.c + ${REF_SRC_DIR}/files/stb.c + ${REF_SRC_DIR}/files/surf.c + ${REF_SRC_DIR}/files/wal.c + ${REF_SRC_DIR}/files/pvs.c + ${COMMON_SRC_DIR}/shared/shared.c + ${COMMON_SRC_DIR}/md4.c + ) + +set(Glad-GL3-Source ${REF_SRC_DIR}/gl3/glad/src/glad.c) +set(Glad-GLES3-Source ${REF_SRC_DIR}/gl3/glad-gles3/src/glad.c) + +set(GL3-Header + ${REF_SRC_DIR}/ref_shared.h + ${REF_SRC_DIR}/constants/anorms.h + ${REF_SRC_DIR}/constants/anormtab.h + ${REF_SRC_DIR}/constants/warpsin.h + ${REF_SRC_DIR}/files/stb_image.h + ${REF_SRC_DIR}/gl3/header/DG_dynarr.h + ${REF_SRC_DIR}/gl3/header/HandmadeMath.h + ${REF_SRC_DIR}/gl3/header/local.h + ${REF_SRC_DIR}/gl3/header/model.h + ${COMMON_SRC_DIR}/header/shared.h + ) + +set(Glad-GL3-Header + ${REF_SRC_DIR}/gl3/glad/include/glad/glad.h + ${REF_SRC_DIR}/gl3/glad/include/KHR/khrplatform.h + ) + +set(Glad-GLES3-Header + ${REF_SRC_DIR}/gl3/glad-gles3/include/glad/glad.h + ${REF_SRC_DIR}/gl3/glad-gles3/include/KHR/khrplatform.h + ) + +set(SOFT-Source + ${REF_SRC_DIR}/soft/sw_aclip.c + ${REF_SRC_DIR}/soft/sw_alias.c + ${REF_SRC_DIR}/soft/sw_bsp.c + ${REF_SRC_DIR}/soft/sw_draw.c + ${REF_SRC_DIR}/soft/sw_edge.c + ${REF_SRC_DIR}/soft/sw_image.c + ${REF_SRC_DIR}/soft/sw_light.c + ${REF_SRC_DIR}/soft/sw_main.c + ${REF_SRC_DIR}/soft/sw_misc.c + ${REF_SRC_DIR}/soft/sw_model.c + ${REF_SRC_DIR}/soft/sw_part.c + ${REF_SRC_DIR}/soft/sw_poly.c + ${REF_SRC_DIR}/soft/sw_polyset.c + ${REF_SRC_DIR}/soft/sw_rast.c + ${REF_SRC_DIR}/soft/sw_scan.c + ${REF_SRC_DIR}/soft/sw_sprite.c + ${REF_SRC_DIR}/soft/sw_surf.c + ${REF_SRC_DIR}/files/models.c + ${REF_SRC_DIR}/files/pcx.c + ${REF_SRC_DIR}/files/stb.c + ${REF_SRC_DIR}/files/surf.c + ${REF_SRC_DIR}/files/wal.c + ${REF_SRC_DIR}/files/pvs.c + ${COMMON_SRC_DIR}/shared/shared.c + ${COMMON_SRC_DIR}/md4.c + ) + +set(SOFT-Header + ${REF_SRC_DIR}/ref_shared.h + ${REF_SRC_DIR}/files/stb_image.h + ${REF_SRC_DIR}/files/stb_image_resize.h + ${REF_SRC_DIR}/soft/header/local.h + ${REF_SRC_DIR}/soft/header/model.h + ${COMMON_SRC_DIR}/header/shared.h + ) + +# Main Quake 2 executable + +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + add_executable(yquake2 WIN32 ${Client-Source} ${Client-SDL-Source} ${Client-Header} + ${Platform-Specific-Source} ${Backends-Generic-Source}) + set_target_properties(yquake2 PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + ) + target_link_libraries(yquake2 ${yquake2LinkerFlags} ${yquake2ClientLinkerFlags} + ${yquake2SDLLinkerFlags} ${yquake2ZLibLinkerFlags} ws2_32 winmm) + if(SDL3_SUPPORT) + target_link_libraries(yquake2 SDL3::SDL3) + endif() + + if(MSVC AND CMAKE_MAJOR_VERSION GREATER 3 OR ( CMAKE_MAJOR_VERSION EQUAL 3 AND CMAKE_MINOR_VERSION GREATER_EQUAL 6 )) + # CMake >= 3.6 supports setting the default project started for debugging (instead of trying to launch ALL_BUILD ...) + set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT yquake2) + set_target_properties(yquake2 PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/release) + endif() + + # Wrapper for the Windows binary + set(Wrapper-Source + src/win-wrapper/wrapper.c + ${BACKENDS_SRC_DIR}/windows/icon.rc + ) + add_executable(quake2 WIN32 ${Wrapper-Source}) + set_target_properties(quake2 PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release) +else() + add_executable(quake2 ${Client-Source} ${Client-SDL-Source} ${Client-Header} + ${Platform-Specific-Source} ${Backends-Generic-Source}) + set_target_properties(quake2 PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + ) + target_link_libraries(quake2 ${yquake2LinkerFlags} ${yquake2ClientLinkerFlags} + ${yquake2SDLLinkerFlags} ${yquake2ZLibLinkerFlags}) + if(SDL3_SUPPORT) + target_link_libraries(quake2 SDL3::SDL3) + endif() +endif() + +# Quake 2 Dedicated Server +add_executable(q2ded ${Server-Source} ${Server-Header} ${Platform-Specific-Source} + ${Backends-Generic-Source}) +set_target_properties(q2ded PROPERTIES + COMPILE_DEFINITIONS "DEDICATED_ONLY" + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + ) +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + target_link_libraries(q2ded ${yquake2LinkerFlags}) +else() + target_link_libraries(q2ded ${yquake2LinkerFlags} ${yquake2ServerLinkerFlags} ${yquake2ZLibLinkerFlags}) +endif() + +# Build the game dynamic library +add_library(game MODULE ${Game-Source} ${Game-Header}) +set_target_properties(game PROPERTIES + PREFIX "" + SUFFIX ${CMAKE_SHARED_LIBRARY_SUFFIX} + ) +get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(isMultiConfig) # multi-config, like Visual Studio solution + set_target_properties(game PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release/$/baseq2 + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release/$/baseq2 + ) +else() # single-config, like normal Makefiles + set_target_properties(game PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release/baseq2 + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release/baseq2 + ) +endif() +target_link_libraries(game ${yquake2LinkerFlags}) + +# Build the GL1 dynamic library +add_library(ref_gl1 MODULE ${GL1-Source} ${GL1-Header} ${REF-Platform-Specific-Source}) +set_target_properties(ref_gl1 PROPERTIES + PREFIX "" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + SUFFIX ${CMAKE_SHARED_LIBRARY_SUFFIX} + ) +target_link_libraries(ref_gl1 ${yquake2LinkerFlags} ${yquake2OpenGLLinkerFlags} + ${yquake2SDLLinkerFlags}) +if(SDL3_SUPPORT) + target_link_libraries(ref_gl1 SDL3::SDL3) +endif() + +# Build the GL3 dynamic library +add_library(ref_gl3 MODULE ${GL3-Source} ${Glad-GL3-Source} ${GL3-Header} ${Glad-GL3-Header} ${REF-Platform-Specific-Source}) +set_target_properties(ref_gl3 PROPERTIES + PREFIX "" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + SUFFIX ${CMAKE_SHARED_LIBRARY_SUFFIX} + ) +target_include_directories(ref_gl3 PRIVATE ${CMAKE_SOURCE_DIR}/src/client/refresh/gl3/glad/include) +target_link_libraries(ref_gl3 ${yquake2LinkerFlags} ${yquake2SDLLinkerFlags}) +if(SDL3_SUPPORT) + target_link_libraries(ref_gl3 SDL3::SDL3) +endif() + +# Build the GLES3 dynamic library +add_library(ref_gles3 MODULE ${GL3-Source} ${Glad-GLES3-Source} ${GL3-Header} ${Glad-GLES3-Header} ${REF-Platform-Specific-Source}) +set_target_properties(ref_gles3 PROPERTIES + PREFIX "" + #COMPILE_DEFINITIONS "YQ2_GL3_GLES3=1;YQ2_GL3_GLES=1" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + SUFFIX ${CMAKE_SHARED_LIBRARY_SUFFIX} + ) +target_include_directories(ref_gles3 PRIVATE ${CMAKE_SOURCE_DIR}/src/client/refresh/gl3/glad-gles3/include) +target_compile_definitions(ref_gles3 PRIVATE YQ2_GL3_GLES3=1 YQ2_GL3_GLES=1) +target_link_libraries(ref_gles3 ${yquake2LinkerFlags} ${yquake2SDLLinkerFlags}) +if(SDL3_SUPPORT) + target_link_libraries(ref_gles3 SDL3::SDL3) +endif() + +# Build the soft renderer dynamic library +add_library(ref_soft MODULE ${SOFT-Source} ${SOFT-Header} ${REF-Platform-Specific-Source}) +set_target_properties(ref_soft PROPERTIES + PREFIX "" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/release + SUFFIX ${CMAKE_SHARED_LIBRARY_SUFFIX} + ) +target_link_libraries(ref_soft ${yquake2LinkerFlags} ${yquake2SDLLinkerFlags}) +if(SDL3_SUPPORT) + target_link_libraries(ref_soft SDL3::SDL3) +endif() diff --git a/Makefile b/Makefile index e0d24226..3fa3c7d9 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ # - Renderer libraries (gl1, gl3, soft) # # # # Base dependencies: # -# - SDL 2.0 # +# - SDL 2 or SDL 3 # # - libGL # # - Vulkan headers # # # @@ -56,6 +56,9 @@ WITH_AVCODEC:=yes # or libopenal.so. Not supported on Windows. WITH_RPATH:=yes +# Builds with SDL 3 instead of SDL 2. +WITH_SDL3:=no + # Enable systemwide installation of game assets. WITH_SYSTEMWIDE:=no @@ -279,7 +282,12 @@ endif # ---------- # Extra CFLAGS for SDL. +ifeq ($(WITH_SDL3),yes) +SDLCFLAGS := $(shell pkgconf --cflags sdl3) +SDLCFLAGS += -DUSE_SDL3 +else SDLCFLAGS := $(shell sdl2-config --cflags) +endif # ---------- @@ -356,11 +364,19 @@ endif # ---------- # Extra LDFLAGS for SDL +ifeq ($(WITH_SDL3),yes) +ifeq ($(YQ2_OSTYPE), Darwin) +SDLLDFLAGS := -lSDL3 +else +SDLLDFLAGS := $(shell pkgconf --libs sdl3) +endif +else ifeq ($(YQ2_OSTYPE), Darwin) SDLLDFLAGS := -lSDL2 -else # not Darwin +else SDLLDFLAGS := $(shell sdl2-config --libs) -endif # Darwin +endif +endif # The renderer libs don't need libSDL2main, libmingw32 or -mwindows. ifeq ($(YQ2_OSTYPE), Windows) @@ -398,6 +414,7 @@ config: @echo "WITH_OPENAL = $(WITH_OPENAL)" @echo "WITH_AVCODEC = $(WITH_AVCODEC)" @echo "WITH_RPATH = $(WITH_RPATH)" + @echo "WITH_SDL3 = $(WITH_SDL3)" @echo "WITH_SYSTEMWIDE = $(WITH_SYSTEMWIDE)" @echo "WITH_SYSTEMDIR = $(WITH_SYSTEMDIR)" @echo "============================" @@ -971,17 +988,15 @@ CLIENT_OBJS_ := \ src/client/cl_view.o \ src/client/curl/download.o \ src/client/curl/qcurl.o \ - src/client/input/sdl.o \ src/client/menu/menu.o \ src/client/menu/qmenu.o \ src/client/menu/videomenu.o \ - src/client/sound/sdl.o \ src/client/sound/ogg.o \ src/client/sound/openal.o \ src/client/sound/qal.o \ + src/client/sound/sdl.o \ src/client/sound/sound.o \ src/client/sound/wave.o \ - src/client/vid/glimp_sdl.o \ src/client/vid/vid.o \ src/common/argproc.o \ src/common/clientserver.o \ @@ -1020,6 +1035,16 @@ CLIENT_OBJS_ := \ src/server/sv_user.o \ src/server/sv_world.o +ifeq ($(WITH_SDL3),yes) +CLIENT_OBJS_ += \ + src/client/input/sdl3.o \ + src/client/vid/glimp_sdl3.o +else +CLIENT_OBJS_ += \ + src/client/input/sdl2.o \ + src/client/vid/glimp_sdl2.o +endif + ifeq ($(YQ2_OSTYPE), Windows) CLIENT_OBJS_ += \ src/backends/windows/main.o \ @@ -1368,7 +1393,7 @@ endif ifeq ($(YQ2_OSTYPE), Windows) release/q2ded.exe : $(SERVER_OBJS) icon @echo "===> LD $@" - ${Q}$(CC) $(LDFLAGS) build/icon/icon.res $(SERVER_OBJS) $(LDLIBS) $(SDLLDFLAGS) -o $@ + ${Q}$(CC) $(LDFLAGS) build/icon/icon.res $(SERVER_OBJS) $(LDLIBS) -o $@ $(Q)strip $@ else release/q2ded : $(SERVER_OBJS) diff --git a/doc/040_cvarlist.md b/doc/040_cvarlist.md index 65c7136c..0b97846e 100644 --- a/doc/040_cvarlist.md +++ b/doc/040_cvarlist.md @@ -408,12 +408,14 @@ Set `0` by default. has 59.95hz. * **vid_gamma**: The value used for gamma correction. Higher values look - brighter. The OpenGL 1.4 and software renderers use "Hardware Gamma", - setting the Gamma of the whole screen to this value in realtime - (except on MacOS where it's applied to textures on load and thus needs - a `vid_restart` after changing). The OpenGL 3.2 and Vulkan renderers - apply this to the window in realtime via shaders (on all platforms). - This is also set by the brightness slider in the video menu. + brighter. The OpenGL 3.2 OpenGL ES3 and Vulkan renderers apply this to + the window in realtime via shaders (on all platforms). When the game + is build against SDL2, the OpenGL 1.4 renderer uses "hardware gamma" + when available, increasing the brightness of the whole screen. On + MacOS the gamma is applied only at renderer start, so a `vid_restart` + is required. When the game is build against SDL3, the OpenGL 1.4 + renderer doesn't support gamma. Have a look at `gl1_overbrightbits` + instead. This is also set by the brightness slider in the video menu. * **vid_fullscreen**: Sets the fullscreen mode. When set to `0` (the default) the game runs in window mode. When set to `1` the games @@ -428,10 +430,12 @@ Set `0` by default. scaling factor of the underlying display. Example: The displays scaling factor is 1.25 and the user requests 1920x1080. The client will render at 1920\*1.25x1080\*1.25=2400x1350. - When set to `0` (the default) the client leaves the decision if the - window should be scaled to the underlying compositor. Scaling applied - by the compositor may introduce blur and sluggishness. + When set to `0` the client leaves the decision if the window should + be scaled to the underlying compositor. Scaling applied by the + compositor may introduce blur and sluggishness. Currently high dpi awareness is only supported under Wayland. + Defaults to `0` when build against SDL2 and to `1` when build against + SDL3. * **vid_maxfps**: The maximum framerate. *Note* that vsync (`r_vsync`) also restricts the framerate to the monitor refresh rate, so if vsync @@ -444,9 +448,9 @@ Set `0` by default. game can be paused, e.g. not in multiplayer games. Defaults to `0`. * **vid_renderer**: Selects the renderer library. Possible options are - `gl1` (the default) for the old OpenGL 1.4 renderer, `gl3` for the - OpenGL 3.2 renderer, `gles3` for the OpenGL ES3 renderer - and `soft` for the software renderer. + `gl3` (the default) for the OpenGL 3.2 renderer, `gles3` for the + OpenGL ES3 renderer, gl1 for the original OpenGL 1.4 renderer and + `soft` for the software renderer. * **r_dynamic**: Enamble dynamic light in gl1 and vk renders. diff --git a/src/backends/windows/main.c b/src/backends/windows/main.c index 43abafe9..c8eb9fca 100644 --- a/src/backends/windows/main.c +++ b/src/backends/windows/main.c @@ -26,8 +26,16 @@ */ #include + +#ifndef DEDICATED_ONLY +#ifdef USE_SDL3 +#include +#include +#else #include #include +#endif +#endif #include "../../common/header/common.h" diff --git a/src/client/input/sdl.c b/src/client/input/sdl2.c similarity index 99% rename from src/client/input/sdl.c rename to src/client/input/sdl2.c index cb5b7f5f..38f08743 100644 --- a/src/client/input/sdl.c +++ b/src/client/input/sdl2.c @@ -701,7 +701,7 @@ IN_Update(void) { // make sure GLimp_GetRefreshRate() will query from SDL again - the window might // be on another display now! - glimp_refreshRate = -1; + glimp_refreshRate = -1.0f; } else if (event.window.event == SDL_WINDOWEVENT_SHOWN) { diff --git a/src/client/input/sdl3.c b/src/client/input/sdl3.c new file mode 100644 index 00000000..96a85a02 --- /dev/null +++ b/src/client/input/sdl3.c @@ -0,0 +1,2389 @@ +/* + * Copyright (C) 2010 Yamagi Burmeister + * Copyright (C) 1997-2005 Id Software, Inc. + * + * 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., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Joystick reading and deadzone handling is based on: + * http://joshsutphin.com/2013/04/12/doing-thumbstick-dead-zones-right.html + * ...and implementation is partially based on code from: + * - http://quakespasm.sourceforge.net + * - https://github.com/Minimuino/thumbstick-deadzones + * + * Flick Stick handling is based on: + * http://gyrowiki.jibbsmart.com/blog:good-gyro-controls-part-2:the-flick-stick + * + * ======================================================================= + * + * This is the Quake II input system backend, implemented with SDL. + * + * ======================================================================= + */ + +#include + +#include "SDL3/SDL_gamepad.h" +#include "SDL3/SDL_properties.h" +#include "SDL3/SDL_stdinc.h" +#include "header/input.h" +#include "../header/keyboard.h" +#include "../header/client.h" + +// ---- + +// Maximal mouse move per frame +#define MOUSE_MAX 3000 + +// Minimal mouse move per frame +#define MOUSE_MIN 40 + +// ---- + +enum { + LAYOUT_DEFAULT = 0, + LAYOUT_SOUTHPAW, + LAYOUT_LEGACY, + LAYOUT_LEGACY_SOUTHPAW, + LAYOUT_FLICK_STICK, + LAYOUT_FLICK_STICK_SOUTHPAW +}; + +typedef struct +{ + float x; + float y; +} thumbstick_t; + +typedef enum +{ + REASON_NONE, + REASON_CONTROLLERINIT, + REASON_GYROCALIBRATION +} updates_countdown_reasons; + +// ---- + +// These are used to communicate the events collected by +// IN_Update() called at the beginning of a frame to the +// actual movement functions called at a later time. +static float mouse_x, mouse_y; +static unsigned char sdl_back_button = SDL_GAMEPAD_BUTTON_BACK; +static int joystick_left_x, joystick_left_y, joystick_right_x, joystick_right_y; +static float gyro_yaw, gyro_pitch; +static qboolean mlooking; + +// The last time input events were processed. +// Used throughout the client. +int sys_frame_time; + +// the joystick altselector that turns K_BTN_X into K_BTN_X_ALT +// is pressed +qboolean joy_altselector_pressed = false; + +// Console Variables +cvar_t *freelook; +cvar_t *lookstrafe; +cvar_t *m_forward; +cvar_t *m_pitch; +cvar_t *m_side; +cvar_t *m_up; +cvar_t *m_yaw; +static cvar_t *sensitivity; + +static cvar_t *exponential_speedup; +static cvar_t *in_grab; +static cvar_t *m_filter; +static cvar_t *windowed_pauseonfocuslost; +static cvar_t *windowed_mouse; +static cvar_t *haptic_feedback_filter; + +// ---- + +typedef struct haptic_effects_cache { + int effect_volume; + int effect_duration; + int effect_delay; + int effect_attack; + int effect_fade; + int effect_id; + int effect_x; + int effect_y; + int effect_z; +} haptic_effects_cache_t; + +qboolean show_gamepad = false, show_haptic = false, show_gyro = false; + +static SDL_Haptic *joystick_haptic = NULL; +static SDL_Gamepad *controller = NULL; + +#define HAPTIC_EFFECT_LIST_SIZE 16 + +static int last_haptic_volume = 0; +static int last_haptic_effect_size = HAPTIC_EFFECT_LIST_SIZE; +static int last_haptic_effect_pos = 0; +static haptic_effects_cache_t last_haptic_effect[HAPTIC_EFFECT_LIST_SIZE]; + +// Joystick sensitivity +static cvar_t *joy_yawsensitivity; +static cvar_t *joy_pitchsensitivity; +static cvar_t *joy_forwardsensitivity; +static cvar_t *joy_sidesensitivity; + +// Joystick's analog sticks configuration +cvar_t *joy_layout; +static cvar_t *joy_left_expo; +static cvar_t *joy_left_snapaxis; +static cvar_t *joy_left_deadzone; +static cvar_t *joy_right_expo; +static cvar_t *joy_right_snapaxis; +static cvar_t *joy_right_deadzone; +static cvar_t *joy_flick_threshold; +static cvar_t *joy_flick_smoothed; + +// Joystick haptic +static cvar_t *joy_haptic_magnitude; +static cvar_t *joy_haptic_distance; + +// Gyro mode (0=off, 3=on, 1-2=uses button to enable/disable) +cvar_t *gyro_mode; +cvar_t *gyro_turning_axis; // yaw or roll + +// Gyro sensitivity +static cvar_t *gyro_yawsensitivity; +static cvar_t *gyro_pitchsensitivity; + +// Gyro is being used in this very moment +static qboolean gyro_active = false; + +// Gyro calibration +static float gyro_accum[3]; +static cvar_t *gyro_calibration_x; +static cvar_t *gyro_calibration_y; +static cvar_t *gyro_calibration_z; +static unsigned int num_samples; +#define NATIVE_SDL_GYRO // uses SDL_CONTROLLERSENSORUPDATE to read gyro + +// To ignore SDL_JOYDEVICEADDED at game init. Allows for hot plugging of game controller afterwards. +static qboolean first_init = true; + +// Countdown of calls to IN_Update(), needed for controller init and gyro calibration +static unsigned short int updates_countdown = 30; + +// Reason for the countdown +static updates_countdown_reasons countdown_reason = REASON_CONTROLLERINIT; + +// Flick Stick +#define FLICK_TIME 6 // number of frames it takes for a flick to execute +static float target_angle; // angle to end up facing at the end of a flick +static unsigned short int flick_progress = FLICK_TIME; + +// Flick Stick's rotation input samples to smooth out +#define MAX_SMOOTH_SAMPLES 8 +static float flick_samples[MAX_SMOOTH_SAMPLES]; +static unsigned short int front_sample = 0; + +extern void CalibrationFinishedCallback(void); + +/* ------------------------------------------------------------------ */ + +/* + * This creepy function translates SDL keycodes into + * the id Tech 2 engines interal representation. + */ +static int +IN_TranslateSDLtoQ2Key(unsigned int keysym) +{ + int key = 0; + + /* These must be translated */ + switch (keysym) + { + case SDLK_TAB: + key = K_TAB; + break; + case SDLK_RETURN: + key = K_ENTER; + break; + case SDLK_ESCAPE: + key = K_ESCAPE; + break; + case SDLK_BACKSPACE: + key = K_BACKSPACE; + break; + case SDLK_LGUI: + case SDLK_RGUI: + key = K_COMMAND; // Win key + break; + case SDLK_CAPSLOCK: + key = K_CAPSLOCK; + break; + case SDLK_POWER: + key = K_POWER; + break; + case SDLK_PAUSE: + key = K_PAUSE; + break; + + case SDLK_UP: + key = K_UPARROW; + break; + case SDLK_DOWN: + key = K_DOWNARROW; + break; + case SDLK_LEFT: + key = K_LEFTARROW; + break; + case SDLK_RIGHT: + key = K_RIGHTARROW; + break; + + case SDLK_RALT: + case SDLK_LALT: + key = K_ALT; + break; + case SDLK_LCTRL: + case SDLK_RCTRL: + key = K_CTRL; + break; + case SDLK_LSHIFT: + case SDLK_RSHIFT: + key = K_SHIFT; + break; + case SDLK_INSERT: + key = K_INS; + break; + case SDLK_DELETE: + key = K_DEL; + break; + case SDLK_PAGEDOWN: + key = K_PGDN; + break; + case SDLK_PAGEUP: + key = K_PGUP; + break; + case SDLK_HOME: + key = K_HOME; + break; + case SDLK_END: + key = K_END; + break; + + case SDLK_F1: + key = K_F1; + break; + case SDLK_F2: + key = K_F2; + break; + case SDLK_F3: + key = K_F3; + break; + case SDLK_F4: + key = K_F4; + break; + case SDLK_F5: + key = K_F5; + break; + case SDLK_F6: + key = K_F6; + break; + case SDLK_F7: + key = K_F7; + break; + case SDLK_F8: + key = K_F8; + break; + case SDLK_F9: + key = K_F9; + break; + case SDLK_F10: + key = K_F10; + break; + case SDLK_F11: + key = K_F11; + break; + case SDLK_F12: + key = K_F12; + break; + case SDLK_F13: + key = K_F13; + break; + case SDLK_F14: + key = K_F14; + break; + case SDLK_F15: + key = K_F15; + break; + + case SDLK_KP_7: + key = K_KP_HOME; + break; + case SDLK_KP_8: + key = K_KP_UPARROW; + break; + case SDLK_KP_9: + key = K_KP_PGUP; + break; + case SDLK_KP_4: + key = K_KP_LEFTARROW; + break; + case SDLK_KP_5: + key = K_KP_5; + break; + case SDLK_KP_6: + key = K_KP_RIGHTARROW; + break; + case SDLK_KP_1: + key = K_KP_END; + break; + case SDLK_KP_2: + key = K_KP_DOWNARROW; + break; + case SDLK_KP_3: + key = K_KP_PGDN; + break; + case SDLK_KP_ENTER: + key = K_KP_ENTER; + break; + case SDLK_KP_0: + key = K_KP_INS; + break; + case SDLK_KP_PERIOD: + key = K_KP_DEL; + break; + case SDLK_KP_DIVIDE: + key = K_KP_SLASH; + break; + case SDLK_KP_MINUS: + key = K_KP_MINUS; + break; + case SDLK_KP_PLUS: + key = K_KP_PLUS; + break; + case SDLK_NUMLOCKCLEAR: + key = K_KP_NUMLOCK; + break; + case SDLK_KP_MULTIPLY: + key = K_KP_STAR; + break; + case SDLK_KP_EQUALS: + key = K_KP_EQUALS; + break; + + // TODO: K_SUPER ? Win Key is already K_COMMAND + + case SDLK_APPLICATION: + key = K_COMPOSE; + break; + case SDLK_MODE: + key = K_MODE; + break; + case SDLK_HELP: + key = K_HELP; + break; + case SDLK_PRINTSCREEN: + key = K_PRINT; + break; + case SDLK_SYSREQ: + key = K_SYSREQ; + break; + case SDLK_SCROLLLOCK: + key = K_SCROLLOCK; + break; + case SDLK_MENU: + key = K_MENU; + break; + case SDLK_UNDO: + key = K_UNDO; + break; + + default: + break; + } + + return key; +} + +static int +IN_TranslateScancodeToQ2Key(SDL_Scancode sc) +{ + +#define MY_SC_CASE(X) case SDL_SCANCODE_ ## X : return K_SC_ ## X; + + switch( (int)sc ) // cast to int to shut -Wswitch up + { + // case SDL_SCANCODE_A : return K_SC_A; + MY_SC_CASE(A) + MY_SC_CASE(B) + MY_SC_CASE(C) + MY_SC_CASE(D) + MY_SC_CASE(E) + MY_SC_CASE(F) + MY_SC_CASE(G) + MY_SC_CASE(H) + MY_SC_CASE(I) + MY_SC_CASE(J) + MY_SC_CASE(K) + MY_SC_CASE(L) + MY_SC_CASE(M) + MY_SC_CASE(N) + MY_SC_CASE(O) + MY_SC_CASE(P) + MY_SC_CASE(Q) + MY_SC_CASE(R) + MY_SC_CASE(S) + MY_SC_CASE(T) + MY_SC_CASE(U) + MY_SC_CASE(V) + MY_SC_CASE(W) + MY_SC_CASE(X) + MY_SC_CASE(Y) + MY_SC_CASE(Z) + MY_SC_CASE(MINUS) + MY_SC_CASE(EQUALS) + MY_SC_CASE(LEFTBRACKET) + MY_SC_CASE(RIGHTBRACKET) + MY_SC_CASE(BACKSLASH) + MY_SC_CASE(NONUSHASH) + MY_SC_CASE(SEMICOLON) + MY_SC_CASE(APOSTROPHE) + MY_SC_CASE(GRAVE) + MY_SC_CASE(COMMA) + MY_SC_CASE(PERIOD) + MY_SC_CASE(SLASH) + MY_SC_CASE(NONUSBACKSLASH) + MY_SC_CASE(INTERNATIONAL1) + MY_SC_CASE(INTERNATIONAL2) + MY_SC_CASE(INTERNATIONAL3) + MY_SC_CASE(INTERNATIONAL4) + MY_SC_CASE(INTERNATIONAL5) + MY_SC_CASE(INTERNATIONAL6) + MY_SC_CASE(INTERNATIONAL7) + MY_SC_CASE(INTERNATIONAL8) + MY_SC_CASE(INTERNATIONAL9) + MY_SC_CASE(THOUSANDSSEPARATOR) + MY_SC_CASE(DECIMALSEPARATOR) + MY_SC_CASE(CURRENCYUNIT) + MY_SC_CASE(CURRENCYSUBUNIT) + } + +#undef MY_SC_CASE + + return 0; +} + +static void IN_Controller_Init(qboolean notify_user); +static void IN_Controller_Shutdown(qboolean notify_user); + +qboolean IN_NumpadIsOn() +{ + SDL_Keymod mod = SDL_GetModState(); + + if ((mod & SDL_KMOD_NUM) == SDL_KMOD_NUM) + { + return true; + } + + return false; +} + +/* ------------------------------------------------------------------ */ + +/* + * Updates the input queue state. Called every + * frame by the client and does nearly all the + * input magic. + */ +void +IN_Update(void) +{ + qboolean want_grab; + SDL_Event event; + unsigned int key; + + static qboolean left_trigger = false; + static qboolean right_trigger = false; + + static int consoleKeyCode = 0; + + /* Get and process an event */ + while (SDL_PollEvent(&event)) + { + + switch (event.type) + { + case SDL_EVENT_MOUSE_WHEEL : + Key_Event((event.wheel.y > 0 ? K_MWHEELUP : K_MWHEELDOWN), true, true); + Key_Event((event.wheel.y > 0 ? K_MWHEELUP : K_MWHEELDOWN), false, true); + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN : + case SDL_EVENT_MOUSE_BUTTON_UP : + switch (event.button.button) + { + case SDL_BUTTON_LEFT: + key = K_MOUSE1; + break; + case SDL_BUTTON_MIDDLE: + key = K_MOUSE3; + break; + case SDL_BUTTON_RIGHT: + key = K_MOUSE2; + break; + case SDL_BUTTON_X1: + key = K_MOUSE4; + break; + case SDL_BUTTON_X2: + key = K_MOUSE5; + break; + default: + return; + } + + Key_Event(key, + (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN), + true); + break; + + case SDL_EVENT_MOUSE_MOTION : + if (cls.key_dest == key_game && (int) cl_paused->value == 0) + { + mouse_x += event.motion.xrel; + mouse_y += event.motion.yrel; + } + break; + + case SDL_EVENT_TEXT_INPUT : + { + int c = event.text.text[0]; + // also make sure we don't get the char that corresponds to the + // "console key" (like "^" or "`") as text input + if ((c >= ' ') && (c <= '~') && c != consoleKeyCode) + { + Char_Event(c); + } + } + + break; + + case SDL_EVENT_KEY_DOWN : + case SDL_EVENT_KEY_UP : + { + qboolean down = (event.type == SDL_EVENT_KEY_DOWN); + + /* workaround for AZERTY-keyboards, which don't have 1, 2, ..., 9, 0 in first row: + * always map those physical keys (scancodes) to those keycodes anyway + * see also https://bugzilla.libsdl.org/show_bug.cgi?id=3188 */ + SDL_Scancode sc = event.key.keysym.scancode; + + if (sc >= SDL_SCANCODE_1 && sc <= SDL_SCANCODE_0) + { + /* Note that the SDL_SCANCODEs are SDL_SCANCODE_1, _2, ..., _9, SDL_SCANCODE_0 + * while in ASCII it's '0', '1', ..., '9' => handle 0 and 1-9 separately + * (quake2 uses the ASCII values for those keys) */ + int key = '0'; /* implicitly handles SDL_SCANCODE_0 */ + + if (sc <= SDL_SCANCODE_9) + { + key = '1' + (sc - SDL_SCANCODE_1); + } + + Key_Event(key, down, false); + } + else + { + SDL_Keycode kc = event.key.keysym.sym; + if(sc == SDL_SCANCODE_GRAVE && kc != '\'' && kc != '"') + { + // special case/hack: open the console with the "console key" + // (beneath Esc, left of 1, above Tab) + // but not if the keycode for this is a quote (like on Brazilian + // keyboards) - otherwise you couldn't type them in the console + if((event.key.keysym.mod & (SDL_KMOD_CAPS|SDL_KMOD_SHIFT|SDL_KMOD_ALT|SDL_KMOD_CTRL|SDL_KMOD_GUI)) == 0) + { + // also, only do this if no modifiers like shift or AltGr or whatever are pressed + // so kc will most likely be the ascii char generated by this and can be ignored + // in case SDL_TEXTINPUT above (so we don't get ^ or whatever as text in console) + // (can't just check for mod == 0 because numlock is a KMOD too) + Key_Event(K_CONSOLE, down, true); + consoleKeyCode = kc; + } + } + else if ((kc >= SDLK_SPACE) && (kc < SDLK_DELETE)) + { + Key_Event(kc, down, false); + } + else + { + int key = IN_TranslateSDLtoQ2Key(kc); + if(key == 0) + { + // fallback to scancodes if we don't know the keycode + key = IN_TranslateScancodeToQ2Key(sc); + } + if(key != 0) + { + Key_Event(key, down, true); + } + else + { + Com_DPrintf("Pressed unknown key with SDL_Keycode %d, SDL_Scancode %d.\n", kc, (int)sc); + } + } + } + + break; + } + + case SDL_EVENT_WINDOW_FOCUS_LOST: + { + Key_MarkAllUp(); + S_Activate(false); + + if (windowed_pauseonfocuslost->value != 1) + { + Cvar_SetValue("paused", 1); + } + + /* pause music */ + if (Cvar_VariableValue("ogg_pausewithgame") == 1 && + OGG_Status() == PLAY && cl.attractloop == false) + { + Cbuf_AddText("ogg toggle\n"); + } + break; + } + + case SDL_EVENT_WINDOW_FOCUS_GAINED: + { + S_Activate(true); + + if (windowed_pauseonfocuslost->value == 2) + { + Cvar_SetValue("paused", 0); + } + + /* play music */ + if (Cvar_VariableValue("ogg_pausewithgame") == 1 && + OGG_Status() == PAUSE && cl.attractloop == false && + cl_paused->value == 0) + { + Cbuf_AddText("ogg toggle\n"); + } + break; + } + + case SDL_EVENT_WINDOW_MOVED: + { + // make sure GLimp_GetRefreshRate() will query from SDL again - the window might + // be on another display now! + glimp_refreshRate = -1.0; + break; + } + + case SDL_EVENT_WINDOW_SHOWN: + { + if (cl_unpaused_scvis->value > 0) + { + Cvar_SetValue("paused", 0); + } + + /* play music */ + if (Cvar_VariableValue("ogg_pausewithgame") == 1 && + OGG_Status() == PAUSE && cl.attractloop == false && + cl_paused->value == 0) + { + Cbuf_AddText("ogg toggle\n"); + } + break; + } + + case SDL_EVENT_GAMEPAD_BUTTON_UP : + case SDL_EVENT_GAMEPAD_BUTTON_DOWN : + { + qboolean down = (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN); + unsigned char btn = event.gbutton.button; + + // Handle Back Button first, to override its original key + if (btn == sdl_back_button) + { + Key_Event(K_JOY_BACK, down, true); + break; + } + + Key_Event(K_BTN_A + btn, down, true); + break; + } + + case SDL_EVENT_GAMEPAD_AXIS_MOTION : /* Handle Controller Motion */ + { + int axis_value = event.gaxis.value; + + switch (event.gaxis.axis) + { + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER : + { + qboolean new_left_trigger = axis_value > 8192; + if (new_left_trigger != left_trigger) + { + left_trigger = new_left_trigger; + Key_Event(K_TRIG_LEFT, left_trigger, true); + } + break; + } + + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER : + { + qboolean new_right_trigger = axis_value > 8192; + if (new_right_trigger != right_trigger) + { + right_trigger = new_right_trigger; + Key_Event(K_TRIG_RIGHT, right_trigger, true); + } + break; + } + } + + if (!cl_paused->value && cls.key_dest == key_game) + { + switch (event.gaxis.axis) + { + case SDL_GAMEPAD_AXIS_LEFTX : + joystick_left_x = axis_value; + break; + case SDL_GAMEPAD_AXIS_LEFTY : + joystick_left_y = axis_value; + break; + case SDL_GAMEPAD_AXIS_RIGHTX : + joystick_right_x = axis_value; + break; + case SDL_GAMEPAD_AXIS_RIGHTY : + joystick_right_y = axis_value; + break; + } + } + break; + } + +#ifdef NATIVE_SDL_GYRO // controller sensors' reading supported (gyro, accelerometer) + case SDL_EVENT_GAMEPAD_SENSOR_UPDATE : + if (event.gsensor.sensor != SDL_SENSOR_GYRO) + { + break; + } + if (countdown_reason == REASON_GYROCALIBRATION && updates_countdown) + { + gyro_accum[0] += event.gsensor.data[0]; + gyro_accum[1] += event.gsensor.data[1]; + gyro_accum[2] += event.gsensor.data[2]; + num_samples++; + break; + } + +#else // gyro read as "secondary joystick" + case SDL_EVENT_JOYSTICK_AXIS_MOTION : + if ( !imu_joystick || event.gdevice.which != SDL_GetJoystickInstanceID(imu_joystick) ) + { + break; // controller axes handled by SDL_CONTROLLERAXISMOTION + } + + int axis_value = event.gaxis.value; + if (countdown_reason == REASON_GYROCALIBRATION && updates_countdown) + { + switch (event.gaxis.axis) + { + case IMU_JOY_AXIS_GYRO_PITCH: + gyro_accum[0] += axis_value; + num_samples[0]++; + break; + case IMU_JOY_AXIS_GYRO_YAW: + gyro_accum[1] += axis_value; + num_samples[1]++; + break; + case IMU_JOY_AXIS_GYRO_ROLL: + gyro_accum[2] += axis_value; + num_samples[2]++; + } + break; + } + +#endif // NATIVE_SDL_GYRO + + if (gyro_active && gyro_mode->value && + !cl_paused->value && cls.key_dest == key_game) + { +#ifdef NATIVE_SDL_GYRO + if (!gyro_turning_axis->value) + { + gyro_yaw = event.gsensor.data[1] - gyro_calibration_y->value; // yaw + } + else + { + gyro_yaw = -(event.gsensor.data[2] - gyro_calibration_z->value); // roll + } + gyro_pitch = event.gsensor.data[0] - gyro_calibration_x->value; +#else // old "joystick" gyro + switch (event.gaxis.axis) // inside "case SDL_JOYAXISMOTION" here + { + case IMU_JOY_AXIS_GYRO_PITCH: + gyro_pitch = -(axis_value - gyro_calibration_x->value); + break; + case IMU_JOY_AXIS_GYRO_YAW: + if (!gyro_turning_axis->value) + { + gyro_yaw = axis_value - gyro_calibration_y->value; + } + break; + case IMU_JOY_AXIS_GYRO_ROLL: + if (gyro_turning_axis->value) + { + gyro_yaw = axis_value - gyro_calibration_z->value; + } + } +#endif // NATIVE_SDL_GYRO + } + else + { + gyro_yaw = gyro_pitch = 0; + } + break; + + case SDL_EVENT_GAMEPAD_REMOVED : + if (!controller) + { + break; + } + if (event.gdevice.which == SDL_GetJoystickInstanceID(SDL_GetGamepadJoystick(controller))) { + Cvar_SetValue("paused", 1); + IN_Controller_Shutdown(true); + IN_Controller_Init(false); + } + break; + + case SDL_EVENT_JOYSTICK_ADDED : + if (!controller) + { + // This should be lower, but some controllers just don't want to get detected by the OS + updates_countdown = 100; + countdown_reason = REASON_CONTROLLERINIT; + } + break; + + case SDL_EVENT_JOYSTICK_BATTERY_UPDATED : + if (!controller || event.jbattery.which != SDL_GetJoystickInstanceID(SDL_GetGamepadJoystick(controller))) + { + break; + } + if (event.jbattery.percent <= 20) + { + Com_Printf("WARNING: Gamepad battery Low, it is recommended to connect it by cable.\n"); + } + else if (event.jbattery.percent <= 1) + { + SCR_CenterPrint("ALERT: Gamepad battery almost Empty.\n"); + } + break; + + case SDL_EVENT_QUIT : + Com_Quit(); + break; + } + } + + /* Grab and ungrab the mouse if the console or the menu is opened */ + if (in_grab->value == 3) + { + want_grab = windowed_mouse->value; + } + else + { + want_grab = (vid_fullscreen->value || in_grab->value == 1 || + (in_grab->value == 2 && windowed_mouse->value)); + } + + // calling GLimp_GrabInput() each frame is a bit ugly but simple and should work. + // The called SDL functions return after a cheap check, if there's nothing to do. + GLimp_GrabInput(want_grab); + + // We need to save the frame time so other subsystems + // know the exact time of the last input events. + sys_frame_time = Sys_Milliseconds(); + + // Hot plugging delay handling, to not be "overwhelmed" because some controllers + // present themselves as two different devices, triggering SDL_JOYDEVICEADDED + // too many times. They could trigger it even at game initialization. + // Also used to keep time of the 'controller gyro calibration' pause. + if (updates_countdown) + { + updates_countdown--; + if (!updates_countdown) // Countdown finished, apply needed action by reason + { + switch (countdown_reason) + { + case REASON_CONTROLLERINIT: + if (!first_init) + { + IN_Controller_Shutdown(false); + IN_Controller_Init(true); + } + else + { + first_init = false; + } + break; + + case REASON_GYROCALIBRATION: // finish and save calibration + { +#ifdef NATIVE_SDL_GYRO + const float inverseSamples = 1.f / num_samples; + Cvar_SetValue("gyro_calibration_x", gyro_accum[0] * inverseSamples); + Cvar_SetValue("gyro_calibration_y", gyro_accum[1] * inverseSamples); + Cvar_SetValue("gyro_calibration_z", gyro_accum[2] * inverseSamples); +#else + if (!num_samples[0] || !num_samples[1] || !num_samples[2]) + { + Com_Printf("Calibration failed, please retry inside a level after having moved your controller a little.\n"); + } + else + { + Cvar_SetValue("gyro_calibration_x", gyro_accum[0] / num_samples[0]); + Cvar_SetValue("gyro_calibration_y", gyro_accum[1] / num_samples[1]); + Cvar_SetValue("gyro_calibration_z", gyro_accum[2] / num_samples[2]); + } +#endif + Com_Printf("Calibration results:\n X=%f Y=%f Z=%f\n", + gyro_calibration_x->value, gyro_calibration_y->value, gyro_calibration_z->value); + CalibrationFinishedCallback(); + break; + } + + default: + break; // avoiding compiler warning + } + countdown_reason = REASON_NONE; + } + } +} + +/* + * Joystick vector magnitude + */ +static float +IN_StickMagnitude(thumbstick_t stick) +{ + return sqrtf((stick.x * stick.x) + (stick.y * stick.y)); +} + +/* + * Scales "v" from [deadzone, 1] range to [0, 1] range, then inherits sign + */ +static float +IN_MapRange(float v, float deadzone, float sign) +{ + return ((v - deadzone) / (1 - deadzone)) * sign; +} + +/* + * Radial deadzone based on github.com/jeremiah-sypult/Quakespasm-Rift + */ +static thumbstick_t +IN_RadialDeadzone(thumbstick_t stick, float deadzone) +{ + thumbstick_t result = {0}; + float magnitude = Q_min(IN_StickMagnitude(stick), 1.0f); + deadzone = Q_min( Q_max(deadzone, 0.0f), 0.9f); // clamp to [0.0, 0.9] + + if ( magnitude > deadzone ) + { + const float scale = ((magnitude - deadzone) / (1.0 - deadzone)) / magnitude; + result.x = stick.x * scale; + result.y = stick.y * scale; + } + + return result; +} + +/* + * Sloped axial deadzone based on github.com/Minimuino/thumbstick-deadzones + * Provides a "snap-to-axis" feeling, without losing precision near the center of the stick + */ +static thumbstick_t +IN_SlopedAxialDeadzone(thumbstick_t stick, float deadzone) +{ + thumbstick_t result = {0}; + float abs_x = fabsf(stick.x); + float abs_y = fabsf(stick.y); + float sign_x = copysignf(1.0f, stick.x); + float sign_y = copysignf(1.0f, stick.y); + deadzone = Q_min(deadzone, 0.5f); + float deadzone_x = deadzone * abs_y; // deadzone of one axis depends... + float deadzone_y = deadzone * abs_x; // ...on the value of the other axis + + if (abs_x > deadzone_x) + { + result.x = IN_MapRange(abs_x, deadzone_x, sign_x); + } + if (abs_y > deadzone_y) + { + result.y = IN_MapRange(abs_y, deadzone_y, sign_y); + } + + return result; +} + +/* + * Exponent applied on stick magnitude + */ +static thumbstick_t +IN_ApplyExpo(thumbstick_t stick, float exponent) +{ + thumbstick_t result = {0}; + float magnitude = IN_StickMagnitude(stick); + if (magnitude == 0) + { + return result; + } + + const float eased = powf(magnitude, exponent) / magnitude; + result.x = stick.x * eased; + result.y = stick.y * eased; + return result; +} + +/* + * Delete flick stick's buffer of angle samples for smoothing + */ +static void +IN_ResetSmoothSamples() +{ + front_sample = 0; + for (int i = 0; i < MAX_SMOOTH_SAMPLES; i++) + { + flick_samples[i] = 0.0f; + } +} + +/* + * Soft tiered smoothing for angle rotations with Flick Stick + * http://gyrowiki.jibbsmart.com/blog:tight-and-smooth:soft-tiered-smoothing + */ +static float +IN_SmoothedStickRotation(float value) +{ + float top_threshold = joy_flick_smoothed->value; + float bottom_threshold = top_threshold / 2.0f; + if (top_threshold == 0) + { + return value; + } + + // sample in the circular smoothing buffer we want to write over + front_sample = (front_sample + 1) % MAX_SMOOTH_SAMPLES; + + // if input > top threshold, it'll all be consumed immediately + // 0 gets put into the smoothing buffer + // if input < bottom threshold, it'll all be put in the smoothing buffer + // 0 for immediate consumption + float immediate_weight = (fabsf(value) - bottom_threshold) + / (top_threshold - bottom_threshold); + immediate_weight = Q_min( Q_max(immediate_weight, 0.0f), 1.0f ); // clamp to [0, 1] range + + // now we can push the smooth sample + float smooth_weight = 1.0f - immediate_weight; + flick_samples[front_sample] = value * smooth_weight; + + // calculate smoothed result + float average = 0; + for (int i = 0; i < MAX_SMOOTH_SAMPLES; i++) + { + average += flick_samples[i]; + } + average /= MAX_SMOOTH_SAMPLES; + + // finally, add immediate portion (original input) + return average + value * immediate_weight; +} + +/* + * Flick Stick handling: detect if the player just started one, or return the + * player rotation if stick was already flicked + */ +static float +IN_FlickStick(thumbstick_t stick, float axial_deadzone) +{ + static qboolean is_flicking; + static float last_stick_angle; + thumbstick_t processed = stick; + float angle_change = 0; + + if (IN_StickMagnitude(stick) > Q_min(joy_flick_threshold->value, 1.0f)) // flick! + { + // Make snap-to-axis only if player wasn't already flicking + if (!is_flicking || flick_progress < FLICK_TIME) + { + processed = IN_SlopedAxialDeadzone(stick, axial_deadzone); + } + + const float stick_angle = (180 / M_PI) * atan2f(-processed.x, -processed.y); + + if (!is_flicking) + { + // Flicking begins now, with a new target + is_flicking = true; + flick_progress = 0; + target_angle = stick_angle; + IN_ResetSmoothSamples(); + } + else + { + // Was already flicking, just turning now + angle_change = stick_angle - last_stick_angle; + + // angle wrap: https://stackoverflow.com/a/11498248/1130520 + angle_change = fmod(angle_change + 180.0f, 360.0f); + if (angle_change < 0) + { + angle_change += 360.0f; + } + angle_change -= 180.0f; + angle_change = IN_SmoothedStickRotation(angle_change); + } + + last_stick_angle = stick_angle; + } + else + { + is_flicking = false; + } + + return angle_change; +} + +/* + * Move handling + */ +void +IN_Move(usercmd_t *cmd) +{ + // Factor used to transform from SDL joystick input ([-32768, 32767]) to [-1, 1] range + static const float normalize_sdl_axis = 1.0f / 32768.0f; + + // Flick Stick's factors to change to the target angle with a feeling of "ease out" + static const float rotation_factor[FLICK_TIME] = + { + 0.305555556f, 0.249999999f, 0.194444445f, 0.138888889f, 0.083333333f, 0.027777778f + }; + + static float old_mouse_x; + static float old_mouse_y; + static float joystick_yaw, joystick_pitch; + static float joystick_forwardmove, joystick_sidemove; + static thumbstick_t left_stick = {0}, right_stick = {0}; + + if (m_filter->value) + { + if ((mouse_x > 1) || (mouse_x < -1)) + { + mouse_x = (mouse_x + old_mouse_x) * 0.5; + } + + if ((mouse_y > 1) || (mouse_y < -1)) + { + mouse_y = (mouse_y + old_mouse_y) * 0.5; + } + } + + old_mouse_x = mouse_x; + old_mouse_y = mouse_y; + + if (mouse_x || mouse_y) + { + if (!exponential_speedup->value) + { + mouse_x *= sensitivity->value; + mouse_y *= sensitivity->value; + } + else + { + if ((mouse_x > MOUSE_MIN) || (mouse_y > MOUSE_MIN) || + (mouse_x < -MOUSE_MIN) || (mouse_y < -MOUSE_MIN)) + { + mouse_x = (mouse_x * mouse_x * mouse_x) / 4; + mouse_y = (mouse_y * mouse_y * mouse_y) / 4; + + if (mouse_x > MOUSE_MAX) + { + mouse_x = MOUSE_MAX; + } + else if (mouse_x < -MOUSE_MAX) + { + mouse_x = -MOUSE_MAX; + } + + if (mouse_y > MOUSE_MAX) + { + mouse_y = MOUSE_MAX; + } + else if (mouse_y < -MOUSE_MAX) + { + mouse_y = -MOUSE_MAX; + } + } + } + + // add mouse X/Y movement to cmd + if ((in_strafe.state & 1) || (lookstrafe->value && mlooking)) + { + cmd->sidemove += m_side->value * mouse_x; + } + else + { + cl.viewangles[YAW] -= m_yaw->value * mouse_x; + } + + if ((mlooking || freelook->value) && !(in_strafe.state & 1)) + { + cl.viewangles[PITCH] += m_pitch->value * mouse_y; + } + else + { + cmd->forwardmove -= m_forward->value * mouse_y; + } + + mouse_x = mouse_y = 0; + } + + // Joystick reading and processing + left_stick.x = joystick_left_x * normalize_sdl_axis; + left_stick.y = joystick_left_y * normalize_sdl_axis; + right_stick.x = joystick_right_x * normalize_sdl_axis; + right_stick.y = joystick_right_y * normalize_sdl_axis; + + if (left_stick.x || left_stick.y) + { + left_stick = IN_RadialDeadzone(left_stick, joy_left_deadzone->value); + if ((int)joy_layout->value == LAYOUT_FLICK_STICK_SOUTHPAW) + { + cl.viewangles[YAW] += IN_FlickStick(left_stick, joy_left_snapaxis->value); + } + else + { + left_stick = IN_SlopedAxialDeadzone(left_stick, joy_left_snapaxis->value); + left_stick = IN_ApplyExpo(left_stick, joy_left_expo->value); + } + } + + if (right_stick.x || right_stick.y) + { + right_stick = IN_RadialDeadzone(right_stick, joy_right_deadzone->value); + if ((int)joy_layout->value == LAYOUT_FLICK_STICK) + { + cl.viewangles[YAW] += IN_FlickStick(right_stick, joy_right_snapaxis->value); + } + else + { + right_stick = IN_SlopedAxialDeadzone(right_stick, joy_right_snapaxis->value); + right_stick = IN_ApplyExpo(right_stick, joy_right_expo->value); + } + } + + switch((int)joy_layout->value) + { + case LAYOUT_SOUTHPAW: + joystick_forwardmove = right_stick.y; + joystick_sidemove = right_stick.x; + joystick_yaw = left_stick.x; + joystick_pitch = left_stick.y; + break; + case LAYOUT_LEGACY: + joystick_forwardmove = left_stick.y; + joystick_sidemove = right_stick.x; + joystick_yaw = left_stick.x; + joystick_pitch = right_stick.y; + break; + case LAYOUT_LEGACY_SOUTHPAW: + joystick_forwardmove = right_stick.y; + joystick_sidemove = left_stick.x; + joystick_yaw = right_stick.x; + joystick_pitch = left_stick.y; + break; + case LAYOUT_FLICK_STICK: // yaw already set by now + joystick_forwardmove = left_stick.y; + joystick_sidemove = left_stick.x; + break; + case LAYOUT_FLICK_STICK_SOUTHPAW: + joystick_forwardmove = right_stick.y; + joystick_sidemove = right_stick.x; + break; + default: // LAYOUT_DEFAULT + joystick_forwardmove = left_stick.y; + joystick_sidemove = left_stick.x; + joystick_yaw = right_stick.x; + joystick_pitch = right_stick.y; + } + + // To make the the viewangles changes independent of framerate we need to scale + // with frametime (assuming the configured values are for 60hz) + // + // For movement this is not needed, as those are absolute values independent of framerate + float joyViewFactor = cls.rframetime/0.01666f; +#ifdef NATIVE_SDL_GYRO + float gyroViewFactor = (1.0f / M_PI) * joyViewFactor; +#else + float gyroViewFactor = (1.0f / 2560.0f) * joyViewFactor; // normalized for Switch gyro +#endif + + if (joystick_yaw) + { + cl.viewangles[YAW] -= (m_yaw->value * joy_yawsensitivity->value + * cl_yawspeed->value * joystick_yaw) * joyViewFactor; + } + + if(joystick_pitch) + { + cl.viewangles[PITCH] += (m_pitch->value * joy_pitchsensitivity->value + * cl_pitchspeed->value * joystick_pitch) * joyViewFactor; + } + + if (joystick_forwardmove) + { + // We need to be twice as fast because with joystick we run... + cmd->forwardmove -= m_forward->value * joy_forwardsensitivity->value + * cl_forwardspeed->value * 2.0f * joystick_forwardmove; + } + + if (joystick_sidemove) + { + // We need to be twice as fast because with joystick we run... + cmd->sidemove += m_side->value * joy_sidesensitivity->value + * cl_sidespeed->value * 2.0f * joystick_sidemove; + } + + if (gyro_yaw) + { + cl.viewangles[YAW] += m_yaw->value * gyro_yawsensitivity->value + * cl_yawspeed->value * gyro_yaw * gyroViewFactor; + } + + if (gyro_pitch) + { + cl.viewangles[PITCH] -= m_pitch->value * gyro_pitchsensitivity->value + * cl_pitchspeed->value * gyro_pitch * gyroViewFactor; + } + + // Flick Stick: flick in progress, changing the yaw angle to the target progressively + if (flick_progress < FLICK_TIME) + { + cl.viewangles[YAW] += target_angle * rotation_factor[flick_progress]; + flick_progress++; + } +} + +/* ------------------------------------------------------------------ */ + +/* + * Look down + */ +static void +IN_MLookDown(void) +{ + mlooking = true; +} + +/* + * Look up + */ +static void +IN_MLookUp(void) +{ + mlooking = false; + IN_CenterView(); +} + +static void +IN_JoyAltSelectorDown(void) +{ + joy_altselector_pressed = true; +} + +static void +IN_JoyAltSelectorUp(void) +{ + joy_altselector_pressed = false; +} + +static void +IN_GyroActionDown(void) +{ + switch ((int)gyro_mode->value) + { + case 1: + gyro_active = true; + return; + case 2: + gyro_active = false; + } +} + +static void +IN_GyroActionUp(void) +{ + switch ((int)gyro_mode->value) + { + case 1: + gyro_active = false; + return; + case 2: + gyro_active = true; + } +} + +/* + * Removes all pending events from SDLs queue. + */ +void +In_FlushQueue(void) +{ + SDL_FlushEvents(SDL_EVENT_FIRST, SDL_EVENT_LAST); + Key_MarkAllUp(); + IN_JoyAltSelectorUp(); +} + +/* ------------------------------------------------------------------ */ + +static void IN_Haptic_Shutdown(void); + +/* + * Init haptic effects + */ +static int +IN_Haptic_Effect_Init(int effect_x, int effect_y, int effect_z, + int period, int magnitude, + int delay, int attack, int fade) +{ + static SDL_HapticEffect haptic_effect; + + /* limit magnitude */ + if (magnitude > SHRT_MAX) + { + magnitude = SHRT_MAX; + } + else if (magnitude < 0) + { + magnitude = 0; + } + + SDL_memset(&haptic_effect, 0, sizeof(SDL_HapticEffect)); // 0 is safe default + + haptic_effect.type = SDL_HAPTIC_SINE; + haptic_effect.periodic.direction.type = SDL_HAPTIC_CARTESIAN; // Cartesian/3d coordinates + haptic_effect.periodic.direction.dir[0] = effect_x; + haptic_effect.periodic.direction.dir[1] = effect_y; + haptic_effect.periodic.direction.dir[2] = effect_z; + haptic_effect.periodic.period = period; + haptic_effect.periodic.magnitude = magnitude; + haptic_effect.periodic.length = period; + haptic_effect.periodic.delay = delay; + haptic_effect.periodic.attack_length = attack; + haptic_effect.periodic.fade_length = fade; + + return SDL_CreateHapticEffect(joystick_haptic, &haptic_effect); +} + +static void +IN_Haptic_Effects_Info(void) +{ + Com_Printf ("Joystick/Mouse haptic:\n"); + Com_Printf (" * %d effects\n", + SDL_GetMaxHapticEffects(joystick_haptic)); + Com_Printf (" * %d haptic effects at the same time\n", + SDL_GetMaxHapticEffectsPlaying(joystick_haptic)); + Com_Printf (" * %d haptic axis\n", + SDL_GetNumHapticAxes(joystick_haptic)); +} + +static void +IN_Haptic_Effects_Init(void) +{ + last_haptic_effect_size = SDL_GetMaxHapticEffectsPlaying(joystick_haptic); + + if (last_haptic_effect_size > HAPTIC_EFFECT_LIST_SIZE) + { + last_haptic_effect_size = HAPTIC_EFFECT_LIST_SIZE; + } + + memset(&last_haptic_effect, 0, sizeof(last_haptic_effect)); + for (int i=0; i= 0) + { + SDL_DestroyHapticEffect(joystick_haptic, *effect_id); + } + + *effect_id = -1; +} + +static void +IN_Haptic_Effects_Shutdown(void) +{ + for (int i=0; ivalue * effect_volume; // 32767 max strength; + + /* + Com_Printf("%d: volume %d: %d ms %d:%d:%d ms speed: %.2f\n", + last_haptic_effect_pos, effect_volume, effect_duration, + effect_delay, effect_attack, effect_fade, + (float)effect_volume / effect_fade); + */ + + // FIFO for effects + last_haptic_effect_pos = (last_haptic_effect_pos + 1) % last_haptic_effect_size; + IN_Haptic_Effect_Shutdown(&last_haptic_effect[last_haptic_effect_pos].effect_id); + last_haptic_effect[last_haptic_effect_pos].effect_volume = effect_volume; + last_haptic_effect[last_haptic_effect_pos].effect_duration = effect_duration; + last_haptic_effect[last_haptic_effect_pos].effect_delay = effect_delay; + last_haptic_effect[last_haptic_effect_pos].effect_attack = effect_attack; + last_haptic_effect[last_haptic_effect_pos].effect_fade = effect_fade; + last_haptic_effect[last_haptic_effect_pos].effect_x = effect_x; + last_haptic_effect[last_haptic_effect_pos].effect_y = effect_y; + last_haptic_effect[last_haptic_effect_pos].effect_z = effect_z; + last_haptic_effect[last_haptic_effect_pos].effect_id = IN_Haptic_Effect_Init( + effect_x, effect_y, effect_z, + effect_duration, haptic_volume, + effect_delay, effect_attack, effect_fade); + + return last_haptic_effect[last_haptic_effect_pos].effect_id; +} + +// Keep it same with rumble rules, look for descriptions to rumble +// filtering in Controller_Rumble +static char *default_haptic_filter = ( + // skipped files should be before wider rule + "!weapons/*grenlb " // bouncing grenades don't have feedback + "!weapons/*hgrenb " // bouncing grenades don't have feedback + "!weapons/*open " // rogue's items don't have feedback + "!weapons/*warn " // rogue's items don't have feedback + // any weapons that are not in previous list + "weapons/ " + // player{,s} effects + "player/*land " // fall without injury + "player/*burn " + "player/*pain " + "player/*fall " + "player/*death " + "players/*burn " + "players/*pain " + "players/*fall " + "players/*death " + // environment effects + "doors/ " + "plats/ " + "world/*dish " + "world/*drill2a " + "world/*dr_ " + "world/*explod1 " + "world/*rocks " + "world/*rumble " + "world/*quake " + "world/*train2 " +); + +/* + * name: sound name + * filter: sound name rule with '*' + * return false for empty filter + */ +static qboolean +Haptic_Feedback_Filtered_Line(const char *name, const char *filter) +{ + const char *current_filter = filter; + + // skip empty filter + if (!*current_filter) + { + return false; + } + + while (*current_filter) + { + char part_filter[MAX_QPATH]; + const char *name_part; + const char *str_end; + + str_end = strchr(current_filter, '*'); + if (!str_end) + { + if (!strstr(name, current_filter)) + { + // no such part in string + return false; + } + // have such part + break; + } + // copy filter line + if ((str_end - current_filter) >= MAX_QPATH) + { + return false; + } + memcpy(part_filter, current_filter, str_end - current_filter); + part_filter[str_end - current_filter] = 0; + // place part in name + name_part = strstr(name, part_filter); + if (!name_part) + { + // no such part in string + return false; + } + // have such part + name = name_part + strlen(part_filter); + // move to next filter + current_filter = str_end + 1; + } + + return true; +} + +/* + * name: sound name + * filter: sound names separated by space, and '!' for skip file + */ +static qboolean +Haptic_Feedback_Filtered(const char *name, const char *filter) +{ + const char *current_filter = filter; + + while (*current_filter) + { + char line_filter[MAX_QPATH]; + const char *str_end; + + str_end = strchr(current_filter, ' '); + // its end of filter + if (!str_end) + { + // check rules inside line + if (Haptic_Feedback_Filtered_Line(name, current_filter)) + { + return true; + } + return false; + } + // copy filter line + if ((str_end - current_filter) >= MAX_QPATH) + { + return false; + } + memcpy(line_filter, current_filter, str_end - current_filter); + line_filter[str_end - current_filter] = 0; + // check rules inside line + if (*line_filter == '!') + { + // has invert rule + if (Haptic_Feedback_Filtered_Line(name, line_filter + 1)) + { + return false; + } + } + else + { + if (Haptic_Feedback_Filtered_Line(name, line_filter)) + { + return true; + } + } + // move to next filter + current_filter = str_end + 1; + } + return false; +} + +/* + * Haptic Feedback: + * effect_volume=0..SHRT_MAX + * effect{x,y,z} - effect direction + * effect{delay,attack,fade} - effect durations + * effect_distance - distance to sound source + * name - sound file name + */ +void +Haptic_Feedback(const char *name, int effect_volume, int effect_duration, + int effect_delay, int effect_attack, int effect_fade, + int effect_x, int effect_y, int effect_z, float effect_distance) +{ + float max_distance = joy_haptic_distance->value; + + if (!joystick_haptic || joy_haptic_magnitude->value <= 0 || + max_distance <= 0 || /* skip haptic if distance is negative */ + effect_distance > max_distance || + effect_volume <= 0 || effect_duration <= 0 || + last_haptic_effect_size <= 0) /* haptic but without slots? */ + { + return; + } + + /* combine distance and volume */ + effect_volume *= (max_distance - effect_distance) / max_distance; + + if (last_haptic_volume != (int)(joy_haptic_magnitude->value * 16)) + { + IN_Haptic_Effects_Shutdown(); + IN_Haptic_Effects_Init(); + } + + last_haptic_volume = joy_haptic_magnitude->value * 16; + + if (Haptic_Feedback_Filtered(name, haptic_feedback_filter->string)) + { + int effect_id; + + effect_id = IN_Haptic_GetEffectId(effect_volume, effect_duration, + effect_delay, effect_attack, effect_fade, + effect_x, effect_y, effect_z); + + if (effect_id == -1) + { + /* have rumble used some slots in haptic effect list?, + * ok, use little bit less haptic effects at the same time*/ + IN_Haptic_Effects_Shutdown(); + last_haptic_effect_size --; + Com_Printf("%d haptic effects at the same time\n", last_haptic_effect_size); + return; + } + + SDL_RunHapticEffect(joystick_haptic, effect_id, 1); + } +} + +/* + * Controller_Rumble: + * name = sound file name + * effect_volume = 0..USHRT_MAX + * effect_duration is in ms + * source = origin of audio + * from_player = if source is the client (player) + */ +void +Controller_Rumble(const char *name, vec3_t source, qboolean from_player, + unsigned int duration, unsigned short int volume) +{ + vec_t intens = 0.0f, low_freq = 1.0f, hi_freq = 1.0f, dist_prop; + unsigned short int max_distance = 4; + unsigned int effect_volume; + + if (!show_haptic || !controller || joy_haptic_magnitude->value <= 0 + || volume == 0 || duration == 0) + { + return; + } + + if (strstr(name, "weapons/")) + { + intens = 1.75f; + + if (strstr(name, "/blastf") || strstr(name, "/hyprbf") || strstr(name, "/nail")) + { + intens = 0.125f; // dampen blasters and nailgun's fire + low_freq = 0.7f; + hi_freq = 1.2f; + } + else if (strstr(name, "/shotgf") || strstr(name, "/rocklf")) + { + low_freq = 1.1f; // shotgun & RL shouldn't feel so weak + duration *= 0.7; + } + else if (strstr(name, "/sshotf")) + { + duration *= 0.6; // the opposite for super shotgun + } + else if (strstr(name, "/machgf") || strstr(name, "/disint")) + { + intens = 1.125f; // machine gun & disruptor fire + } + else if (strstr(name, "/grenlb") || strstr(name, "/hgrenb") // bouncing grenades + || strstr(name, "open") || strstr(name, "warn")) // rogue's items + { + return; // ... don't have feedback + } + else if (strstr(name, "/plasshot")) // phalanx cannon + { + intens = 1.0f; + hi_freq = 0.3f; + duration *= 0.5; + } + else if (strstr(name, "x")) // explosions... + { + low_freq = 1.1f; + hi_freq = 0.9f; + max_distance = 550; // can be felt far away + } + else if (strstr(name, "r")) // reloads & ion ripper fire + { + low_freq = 0.1f; + hi_freq = 0.6f; + } + } + else if (strstr(name, "player/land")) + { + intens = 2.2f; // fall without injury + low_freq = 1.1f; + } + else if (strstr(name, "player/") || strstr(name, "players/")) + { + low_freq = 1.2f; // exaggerate player damage + if (strstr(name, "/burn") || strstr(name, "/pain100") || strstr(name, "/pain75")) + { + intens = 2.4f; + } + else if (strstr(name, "/fall") || strstr(name, "/pain50") || strstr(name, "/pain25")) + { + intens = 2.7f; + } + else if (strstr(name, "/death")) + { + intens = 2.9f; + } + } + else if (strstr(name, "doors/")) + { + intens = 0.125f; + low_freq = 0.4f; + max_distance = 280; + } + else if (strstr(name, "plats/")) + { + intens = 1.0f; // platforms rumble... + max_distance = 200; // when player near them + } + else if (strstr(name, "world/")) + { + max_distance = 3500; // ambient events + if (strstr(name, "/dish") || strstr(name, "/drill2a") || strstr(name, "/dr_") + || strstr(name, "/explod1") || strstr(name, "/rocks") + || strstr(name, "/rumble")) + { + intens = 0.28f; + low_freq = 0.7f; + } + else if (strstr(name, "/quake")) + { + intens = 0.67f; // (earth)quakes are more evident + low_freq = 1.2f; + } + else if (strstr(name, "/train2")) + { + intens = 0.28f; + max_distance = 290; // just machinery + } + } + + if (intens == 0.0f) + { + return; + } + + if (from_player) + { + dist_prop = 1.0f; + } + else + { + dist_prop = VectorLength(source); + if (dist_prop > max_distance) + { + return; + } + dist_prop = (max_distance - dist_prop) / max_distance; + } + + effect_volume = joy_haptic_magnitude->value * intens * dist_prop * volume; + low_freq = Q_min(effect_volume * low_freq, USHRT_MAX); + hi_freq = Q_min(effect_volume * hi_freq, USHRT_MAX); + + // Com_Printf("%-29s: vol %5u - %4u ms - dp %.3f l %5.0f h %5.0f\n", + // name, effect_volume, duration, dist_prop, low_freq, hi_freq); + + if (SDL_RumbleGamepad(controller, low_freq, hi_freq, duration) == -1) + { + if (!joystick_haptic) + { + /* no haptic, some other reason of error */ + return; + } + + /* All haptic/force feedback slots are busy, try to clean up little bit. */ + IN_Haptic_Effects_Shutdown(); + } +} + +/* + * Gyro calibration functions, called from menu + */ +void +StartCalibration(void) +{ +#ifdef NATIVE_SDL_GYRO + num_samples = 0; +#else + num_samples[0] = num_samples[1] = num_samples[2] = 0; +#endif + gyro_accum[0] = 0.0; + gyro_accum[1] = 0.0; + gyro_accum[2] = 0.0; + updates_countdown = 300; + countdown_reason = REASON_GYROCALIBRATION; +} + +qboolean +IsCalibrationZero(void) +{ + return (!gyro_calibration_x->value && !gyro_calibration_y->value && !gyro_calibration_z->value); +} + +/* + * Game Controller + */ +static void +IN_Controller_Init(qboolean notify_user) +{ + cvar_t *cvar; + int nummappings; + char controllerdb[MAX_OSPATH] = {0}; + SDL_Joystick *joystick = NULL; + SDL_bool is_controller = SDL_FALSE; + + cvar = Cvar_Get("in_sdlbackbutton", "0", CVAR_ARCHIVE); + if (cvar) + { + switch ((int)cvar->value) + { + case 1: + sdl_back_button = SDL_GAMEPAD_BUTTON_START; + break; + case 2: + sdl_back_button = SDL_GAMEPAD_BUTTON_GUIDE; + break; + default: + sdl_back_button = SDL_GAMEPAD_BUTTON_BACK; + } + } + + cvar = Cvar_Get("in_initjoy", "1", CVAR_NOSET); + if (!cvar->value) + { + return; + } + + if (notify_user) + { + Com_Printf("- Game Controller init attempt -\n"); + } + + if (!SDL_WasInit(SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC)) + { + +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE // extended input reports on PS controllers (enables gyro thru bluetooth) + SDL_SetHint( SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1" ); +#endif +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE + SDL_SetHint( SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1" ); +#endif + + if (SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC) == -1) + { + Com_Printf ("Couldn't init SDL Game Controller: %s.\n", SDL_GetError()); + return; + } + } + + int numjoysticks; + const SDL_JoystickID *joysticks = SDL_GetJoysticks(&numjoysticks); + + if (joysticks != NULL) + { + Com_Printf ("%i joysticks were found.\n", numjoysticks); + } + + + if (numjoysticks == 0) + { + joystick_haptic = SDL_OpenHapticFromMouse(); + + if (joystick_haptic && + (SDL_GetHapticFeatures(joystick_haptic) & SDL_HAPTIC_SINE) == 0) + { + /* Disable haptic for joysticks without SINE */ + SDL_CloseHaptic(joystick_haptic); + joystick_haptic = NULL; + } + + if (joystick_haptic) + { + IN_Haptic_Effects_Info(); + show_haptic = true; + } + + return; + } + + for (const char* rawPath = FS_GetNextRawPath(NULL); rawPath != NULL; rawPath = FS_GetNextRawPath(rawPath)) + { + snprintf(controllerdb, MAX_OSPATH, "%s/gamecontrollerdb.txt", rawPath); + nummappings = SDL_AddGamepadMappingsFromFile(controllerdb); + if (nummappings > 0) + Com_Printf ("%d mappings loaded from gamecontrollerdb.txt\n", nummappings); + } + + for (int i = 0; i < numjoysticks; i++) + { + joystick = SDL_OpenJoystick(joysticks[i]); + if (!joystick) + { + Com_Printf ("Couldn't open joystick %d: %s.\n", i+1, SDL_GetError()); + continue; // try next joystick + } + + const char* joystick_name = SDL_GetJoystickName(joystick); + const int name_len = strlen(joystick_name); + + Com_Printf ("Trying joystick %d, '%s'\n", i+1, joystick_name); + + // Ugly hack to detect IMU-only devices - works for Switch controllers at least + if (name_len > 4 && !strncmp(joystick_name + name_len - 4, " IMU", 4)) + { + SDL_CloseJoystick(joystick); + joystick = NULL; +#ifdef NATIVE_SDL_GYRO + Com_Printf ("Skipping IMU device.\n"); +#else // if it's not a Left JoyCon, use it as Gyro + Com_Printf ("IMU device found.\n"); + if ( !imu_joystick && name_len > 16 && strncmp(joystick_name + name_len - 16, "Left Joy-Con IMU", 16) != 0 ) + { + imu_joystick = SDL_OpenJoystick(joysticks[i]); + if (imu_joystick) + { + show_gyro = true; + Com_Printf ("Using this device as Gyro sensor.\n"); + } + else + { + Com_Printf ("Couldn't open IMU: %s.\n", SDL_GetError()); + } + } +#endif + continue; + } + + Com_Printf ("Buttons = %d, Axes = %d, Hats = %d\n", SDL_GetNumJoystickButtons(joystick), + SDL_GetNumJoystickAxes(joystick), SDL_GetNumJoystickHats(joystick)); + is_controller = SDL_IsGamepad(joysticks[i]); + + if (!is_controller) + { + char joystick_guid[65] = {0}; + SDL_JoystickGUID guid = SDL_GetJoystickInstanceGUID(joysticks[i]); + + SDL_GetJoystickGUIDString(guid, joystick_guid, 64); + + Com_Printf ("To use joystick as game controller, provide its config by either:\n" + " * Putting 'gamecontrollerdb.txt' file in your game directory.\n" + " * Or setting SDL_GAMECONTROLLERCONFIG environment variable. E.g.:\n"); + Com_Printf ("SDL_GAMECONTROLLERCONFIG='%s,%s,leftx:a0,lefty:a1,rightx:a2,righty:a3,back:b1,...'\n", joystick_guid, joystick_name); + } + + SDL_CloseJoystick(joystick); + joystick = NULL; + + if (is_controller && !controller) + { + controller = SDL_OpenGamepad(joysticks[i]); + if (!controller) + { + Com_Printf("SDL Controller error: %s.\n", SDL_GetError()); + continue; // try next joystick + } + + show_gamepad = true; + Com_Printf("Enabled as Game Controller, settings:\n%s\n", + SDL_GetGamepadMapping(controller)); + +#ifdef NATIVE_SDL_GYRO + + if (SDL_GamepadHasSensor(controller, SDL_SENSOR_GYRO) + && !SDL_SetGamepadSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE) ) + { + show_gyro = true; + Com_Printf( "Gyro sensor enabled at %.2f Hz\n", + SDL_GetGamepadSensorDataRate(controller, SDL_SENSOR_GYRO) ); + } + else + { + Com_Printf("Gyro sensor not found.\n"); + } + + SDL_bool hasLED = SDL_GetBooleanProperty(SDL_GetGamepadProperties(controller), SDL_PROP_JOYSTICK_CAP_RGB_LED_BOOLEAN, SDL_FALSE); + if (hasLED) + { + SDL_SetGamepadLED(controller, 0, 80, 0); // green light + } + +#endif // NATIVE_SDL_GYRO + + joystick_haptic = SDL_OpenHapticFromJoystick(SDL_GetGamepadJoystick(controller)); + + if (joystick_haptic && + (SDL_GetHapticFeatures(joystick_haptic) & SDL_HAPTIC_SINE) == 0) + { + /* Disable haptic for joysticks without SINE */ + SDL_CloseHaptic(joystick_haptic); + joystick_haptic = NULL; + } + + if (joystick_haptic) + { + IN_Haptic_Effects_Info(); + show_haptic = true; + } + + SDL_bool hasRumble = SDL_GetBooleanProperty(SDL_GetGamepadProperties(controller), SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, SDL_FALSE); + if (hasRumble) + { + show_haptic = true; + Com_Printf("Rumble support available.\n"); + } + else + { + Com_Printf("Controller doesn't support rumble.\n"); + } + +#ifdef NATIVE_SDL_GYRO // "native" exits when finding a single working controller + break; +#endif + } + } + + SDL_free((void *)joysticks); +} + +/* + * Initializes the backend + */ +void +IN_Init(void) +{ + Com_Printf("------- input initialization -------\n"); + + mouse_x = mouse_y = 0; + joystick_left_x = joystick_left_y = joystick_right_x = joystick_right_y = 0; + gyro_yaw = gyro_pitch = 0; + + exponential_speedup = Cvar_Get("exponential_speedup", "0", CVAR_ARCHIVE); + freelook = Cvar_Get("freelook", "1", CVAR_ARCHIVE); + in_grab = Cvar_Get("in_grab", "2", CVAR_ARCHIVE); + lookstrafe = Cvar_Get("lookstrafe", "0", CVAR_ARCHIVE); + m_filter = Cvar_Get("m_filter", "0", CVAR_ARCHIVE); + m_up = Cvar_Get("m_up", "1", CVAR_ARCHIVE); + m_forward = Cvar_Get("m_forward", "1", CVAR_ARCHIVE); + m_pitch = Cvar_Get("m_pitch", "0.022", CVAR_ARCHIVE); + m_side = Cvar_Get("m_side", "0.8", CVAR_ARCHIVE); + m_yaw = Cvar_Get("m_yaw", "0.022", CVAR_ARCHIVE); + sensitivity = Cvar_Get("sensitivity", "3", CVAR_ARCHIVE); + + joy_haptic_magnitude = Cvar_Get("joy_haptic_magnitude", "0.0", CVAR_ARCHIVE); + joy_haptic_distance = Cvar_Get("joy_haptic_distance", "100.0", CVAR_ARCHIVE); + haptic_feedback_filter = Cvar_Get("joy_haptic_filter", default_haptic_filter, CVAR_ARCHIVE); + + joy_yawsensitivity = Cvar_Get("joy_yawsensitivity", "1.0", CVAR_ARCHIVE); + joy_pitchsensitivity = Cvar_Get("joy_pitchsensitivity", "1.0", CVAR_ARCHIVE); + joy_forwardsensitivity = Cvar_Get("joy_forwardsensitivity", "1.0", CVAR_ARCHIVE); + joy_sidesensitivity = Cvar_Get("joy_sidesensitivity", "1.0", CVAR_ARCHIVE); + + joy_layout = Cvar_Get("joy_layout", "0", CVAR_ARCHIVE); + joy_left_expo = Cvar_Get("joy_left_expo", "2.0", CVAR_ARCHIVE); + joy_left_snapaxis = Cvar_Get("joy_left_snapaxis", "0.15", CVAR_ARCHIVE); + joy_left_deadzone = Cvar_Get("joy_left_deadzone", "0.16", CVAR_ARCHIVE); + joy_right_expo = Cvar_Get("joy_right_expo", "2.0", CVAR_ARCHIVE); + joy_right_snapaxis = Cvar_Get("joy_right_snapaxis", "0.15", CVAR_ARCHIVE); + joy_right_deadzone = Cvar_Get("joy_right_deadzone", "0.16", CVAR_ARCHIVE); + joy_flick_threshold = Cvar_Get("joy_flick_threshold", "0.65", CVAR_ARCHIVE); + joy_flick_smoothed = Cvar_Get("joy_flick_smoothed", "8.0", CVAR_ARCHIVE); + + gyro_calibration_x = Cvar_Get("gyro_calibration_x", "0.0", CVAR_ARCHIVE); + gyro_calibration_y = Cvar_Get("gyro_calibration_y", "0.0", CVAR_ARCHIVE); + gyro_calibration_z = Cvar_Get("gyro_calibration_z", "0.0", CVAR_ARCHIVE); + + gyro_yawsensitivity = Cvar_Get("gyro_yawsensitivity", "1.0", CVAR_ARCHIVE); + gyro_pitchsensitivity = Cvar_Get("gyro_pitchsensitivity", "1.0", CVAR_ARCHIVE); + gyro_turning_axis = Cvar_Get("gyro_turning_axis", "0", CVAR_ARCHIVE); + + gyro_mode = Cvar_Get("gyro_mode", "2", CVAR_ARCHIVE); + if ((int)gyro_mode->value == 2) + { + gyro_active = true; + } + + windowed_pauseonfocuslost = Cvar_Get("vid_pauseonfocuslost", "0", CVAR_USERINFO | CVAR_ARCHIVE); + windowed_mouse = Cvar_Get("windowed_mouse", "1", CVAR_USERINFO | CVAR_ARCHIVE); + + Cmd_AddCommand("+mlook", IN_MLookDown); + Cmd_AddCommand("-mlook", IN_MLookUp); + + Cmd_AddCommand("+joyaltselector", IN_JoyAltSelectorDown); + Cmd_AddCommand("-joyaltselector", IN_JoyAltSelectorUp); + Cmd_AddCommand("+gyroaction", IN_GyroActionDown); + Cmd_AddCommand("-gyroaction", IN_GyroActionUp); + + if (!SDL_WasInit(SDL_INIT_EVENTS)) + { + if ((SDL_InitSubSystem(SDL_INIT_EVENTS)) != 0) + { + Com_Error(ERR_FATAL, "Couldn't initialize SDL event subsystem:%s\n", SDL_GetError()); + } + } + + SDL_StartTextInput(); + + IN_Controller_Init(false); + + Com_Printf("------------------------------------\n\n"); +} + +/* + * Shuts the backend down + */ +static void +IN_Haptic_Shutdown(void) +{ + if (joystick_haptic) + { + IN_Haptic_Effects_Shutdown(); + + SDL_CloseHaptic(joystick_haptic); + joystick_haptic = NULL; + } +} + +static void +IN_Controller_Shutdown(qboolean notify_user) +{ + if (notify_user) + { + Com_Printf("- Game Controller disconnected -\n"); + } + + IN_Haptic_Shutdown(); + + if (controller) + { + SDL_CloseGamepad(controller); + controller = NULL; + } + show_gamepad = show_gyro = show_haptic = false; + joystick_left_x = joystick_left_y = joystick_right_x = joystick_right_y = 0; + gyro_yaw = gyro_pitch = 0; + +#ifndef NATIVE_SDL_GYRO + if (imu_joystick) + { + SDL_CloseJoystick(imu_joystick); + imu_joystick = NULL; + } +#endif +} + +/* + * Shuts the backend down + */ +void +IN_Shutdown(void) +{ + Cmd_RemoveCommand("force_centerview"); + Cmd_RemoveCommand("+mlook"); + Cmd_RemoveCommand("-mlook"); + + Cmd_RemoveCommand("+joyaltselector"); + Cmd_RemoveCommand("-joyaltselector"); + Cmd_RemoveCommand("+gyroaction"); + Cmd_RemoveCommand("-gyroaction"); + + Com_Printf("Shutting down input.\n"); + + IN_Controller_Shutdown(false); + + const Uint32 subsystems = SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_EVENTS; + if (SDL_WasInit(subsystems) == subsystems) + { + SDL_QuitSubSystem(subsystems); + } +} + +/* ------------------------------------------------------------------ */ diff --git a/src/client/refresh/gl1/gl1_main.c b/src/client/refresh/gl1/gl1_main.c index 84996d09..a3e5295e 100644 --- a/src/client/refresh/gl1/gl1_main.c +++ b/src/client/refresh/gl1/gl1_main.c @@ -1920,6 +1920,7 @@ GetRefAPI(refimport_t imp) ri = imp; refexport.api_version = API_VERSION; + refexport.framework_version = RI_GetSDLVersion(); refexport.Init = RI_Init; refexport.Shutdown = RI_Shutdown; diff --git a/src/client/refresh/gl1/gl1_sdl.c b/src/client/refresh/gl1/gl1_sdl.c index db33ab32..ec41448b 100644 --- a/src/client/refresh/gl1/gl1_sdl.c +++ b/src/client/refresh/gl1/gl1_sdl.c @@ -27,7 +27,11 @@ #include "header/local.h" +#ifdef USE_SDL3 +#include +#else #include +#endif #if defined(__APPLE__) #include @@ -155,7 +159,20 @@ void RI_SetVsync(void) } } +#ifdef USE_SDL3 + int vsyncState; + if (SDL_GL_GetSwapInterval(&vsyncState) != 0) + { + R_Printf(PRINT_ALL, "Failed to get vsync state, assuming vsync inactive.\n"); + vsyncActive = false; + } + else + { + vsyncActive = vsyncState ? true : false; + } +#else vsyncActive = SDL_GL_GetSwapInterval() != 0; +#endif } /* @@ -164,6 +181,10 @@ void RI_SetVsync(void) void RI_UpdateGamma(void) { +// TODO SDL3: Hardware gamma / gamma ramps are no longer supported with +// SDL3. There's no replacement and sdl2-compat won't support it either. +// See https://github.com/libsdl-org/SDL/pull/6617 for the rational. +#ifndef USE_SDL3 float gamma = (vid_gamma->value); Uint16 ramp[256]; @@ -173,6 +194,7 @@ RI_UpdateGamma(void) { R_Printf(PRINT_ALL, "Setting gamma failed: %s\n", SDL_GetError()); } +#endif } /* @@ -251,7 +273,11 @@ int RI_InitContext(void* win) #if SDL_VERSION_ATLEAST(2, 26, 0) // Figure out if we are high dpi aware. int flags = SDL_GetWindowFlags(win); +#ifdef USE_SDL3 + IsHighDPIaware = (flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) ? true : false; +#else IsHighDPIaware = (flags & SDL_WINDOW_ALLOW_HIGHDPI) ? true : false; +#endif #endif return true; @@ -262,7 +288,11 @@ int RI_InitContext(void* win) */ void RI_GetDrawableSize(int* width, int* height) { +#ifdef USE_SDL3 + SDL_GetWindowSizeInPixels(window, width, height); +#else SDL_GL_GetDrawableSize(window, width, height); +#endif } /* @@ -280,3 +310,21 @@ RI_ShutdownContext(void) } } } + +/* + * Returns the SDL major version. Implemented + * here to not polute gl1_main.c with the SDL + * headers. + */ +int RI_GetSDLVersion() +{ +#ifdef USE_SDL3 + SDL_Version ver; +#else + SDL_version ver; +#endif + + SDL_VERSION(&ver); + + return ver.major; +} diff --git a/src/client/refresh/gl1/header/local.h b/src/client/refresh/gl1/header/local.h index 733f04f1..e354814d 100644 --- a/src/client/refresh/gl1/header/local.h +++ b/src/client/refresh/gl1/header/local.h @@ -426,6 +426,13 @@ void *RI_GetProcAddress (const char* proc); */ void RI_GetDrawableSize(int* width, int* height); +/* + * Returns the SDL major version. Implemented + * here to not polute gl1_main.c with the SDL + * headers. + */ +int RI_GetSDLVersion(); + /* g11_draw */ extern image_t * RDraw_FindPic(const char *name); extern void RDraw_GetPicSize(int *w, int *h, const char *pic); diff --git a/src/client/refresh/gl3/gl3_main.c b/src/client/refresh/gl3/gl3_main.c index 13a344e7..ebe1ce6e 100644 --- a/src/client/refresh/gl3/gl3_main.c +++ b/src/client/refresh/gl3/gl3_main.c @@ -1995,6 +1995,7 @@ GetRefAPI(refimport_t imp) ri = imp; re.api_version = API_VERSION; + re.framework_version = GL3_GetSDLVersion(); re.Init = GL3_Init; re.Shutdown = GL3_Shutdown; diff --git a/src/client/refresh/gl3/gl3_sdl.c b/src/client/refresh/gl3/gl3_sdl.c index 0c18c365..f345f677 100644 --- a/src/client/refresh/gl3/gl3_sdl.c +++ b/src/client/refresh/gl3/gl3_sdl.c @@ -29,7 +29,11 @@ #include "header/local.h" +#ifdef USE_SDL3 +#include +#else #include +#endif static SDL_Window* window = NULL; static SDL_GLContext context = NULL; @@ -162,7 +166,20 @@ void GL3_SetVsync(void) } } +#ifdef USE_SDL3 + int vsyncState; + if (SDL_GL_GetSwapInterval(&vsyncState) != 0) + { + R_Printf(PRINT_ALL, "Failed to get vsync state, assuming vsync inactive.\n"); + vsyncActive = false; + } + else + { + vsyncActive = vsyncState ? true : false; + } +#else vsyncActive = SDL_GL_GetSwapInterval() != 0; +#endif } /* @@ -345,9 +362,9 @@ int GL3_InitContext(void* win) // Load GL pointers through GLAD and check context. #ifdef YQ2_GL3_GLES - if( !gladLoadGLES2Loader(SDL_GL_GetProcAddress)) + if( !gladLoadGLES2Loader((void *)SDL_GL_GetProcAddress)) #else // Desktop GL - if( !gladLoadGLLoader(SDL_GL_GetProcAddress)) + if( !gladLoadGLLoader((void *)SDL_GL_GetProcAddress)) #endif { R_Printf(PRINT_ALL, "GL3_InitContext(): ERROR: loading OpenGL function pointers failed!\n"); @@ -406,7 +423,11 @@ int GL3_InitContext(void* win) #if SDL_VERSION_ATLEAST(2, 26, 0) // Figure out if we are high dpi aware. int flags = SDL_GetWindowFlags(win); +#ifdef USE_SDL3 + IsHighDPIaware = (flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) ? true : false; +#else IsHighDPIaware = (flags & SDL_WINDOW_ALLOW_HIGHDPI) ? true : false; +#endif #endif return true; @@ -417,7 +438,11 @@ int GL3_InitContext(void* win) */ void GL3_GetDrawableSize(int* width, int* height) { +#ifdef USE_SDL3 + SDL_GetWindowSizeInPixels(window, width, height); +#else SDL_GL_GetDrawableSize(window, width, height); +#endif } /* @@ -434,3 +459,21 @@ void GL3_ShutdownContext() } } } + +/* + * Returns the SDL major version. Implemented + * here to not polute gl3_main.c with the SDL + * headers. + */ +int GL3_GetSDLVersion() +{ +#ifdef USE_SDL3 + SDL_Version ver; +#else + SDL_version ver; +#endif + + SDL_VERSION(&ver); + + return ver.major; +} diff --git a/src/client/refresh/gl3/header/local.h b/src/client/refresh/gl3/header/local.h index fb71a9d6..beaa345c 100644 --- a/src/client/refresh/gl3/header/local.h +++ b/src/client/refresh/gl3/header/local.h @@ -393,6 +393,7 @@ extern qboolean GL3_IsVsyncActive(void); extern void GL3_EndFrame(void); extern void GL3_SetVsync(void); extern void GL3_ShutdownContext(void); +extern int GL3_GetSDLVersion(void); // gl3_misc.c extern void GL3_InitParticleTexture(void); diff --git a/src/client/refresh/soft/sw_main.c b/src/client/refresh/soft/sw_main.c index 7374ad7f..384a4fe4 100644 --- a/src/client/refresh/soft/sw_main.c +++ b/src/client/refresh/soft/sw_main.c @@ -22,8 +22,12 @@ #include #include +#ifdef USE_SDL3 +#include +#else #include #include +#endif #include "header/local.h" @@ -1802,10 +1806,19 @@ GetRefAPI(refimport_t imp) // used different variable name for prevent confusion and cppcheck warnings refexport_t refexport; + // Need to communicate the SDL major version to the client. +#ifdef USE_SDL3 + SDL_Version ver; +#else + SDL_version ver; +#endif + SDL_VERSION(&ver); + memset(&refexport, 0, sizeof(refexport_t)); ri = imp; refexport.api_version = API_VERSION; + refexport.framework_version = ver.major; refexport.BeginRegistration = RE_BeginRegistration; refexport.RegisterModel = RE_RegisterModel; @@ -1890,11 +1903,19 @@ RE_InitContext(void *win) if (r_vsync->value) { +#ifdef USE_SDL3 + renderer = SDL_CreateRenderer(window, NULL, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); +#else renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); +#endif } else { +#ifdef USE_SDL3 + renderer = SDL_CreateRenderer(window, NULL, SDL_RENDERER_ACCELERATED); +#else renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); +#endif } /* Select the color for drawing. It is set to black here. */ @@ -1910,7 +1931,11 @@ RE_InitContext(void *win) #if SDL_VERSION_ATLEAST(2, 26, 0) // Figure out if we are high dpi aware. int flags = SDL_GetWindowFlags(win); +#ifdef USE_SDL3 + IsHighDPIaware = (flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) ? true : false; +#else IsHighDPIaware = (flags & SDL_WINDOW_ALLOW_HIGHDPI) ? true : false; +#endif #endif /* We can't rely on vid, because the context is created @@ -1949,7 +1974,11 @@ RE_InitContext(void *win) */ void RE_GetDrawableSize(int* width, int* height) { +#ifdef USE_SDL3 + SDL_GetCurrentRenderOutputSize(renderer, width, height); +#else SDL_GetRendererOutputSize(renderer, width, height); +#endif } @@ -2225,7 +2254,12 @@ RE_FlushFrame(int vmin, int vmax) SDL_UnlockTexture(texture); +#ifdef USE_SDL3 + SDL_RenderTexture(renderer, texture, NULL, NULL); +#else SDL_RenderCopy(renderer, texture, NULL, NULL); +#endif + SDL_RenderPresent(renderer); // replace use next buffer diff --git a/src/client/refresh/soft/sw_misc.c b/src/client/refresh/soft/sw_misc.c index 7b5cc390..ab7d1c9e 100644 --- a/src/client/refresh/soft/sw_misc.c +++ b/src/client/refresh/soft/sw_misc.c @@ -19,7 +19,11 @@ * */ +#ifdef USE_SDL3 +#include +#else #include +#endif #include "header/local.h" diff --git a/src/client/sound/sdl.c b/src/client/sound/sdl.c index e5256a97..7bdf46fc 100644 --- a/src/client/sound/sdl.c +++ b/src/client/sound/sdl.c @@ -34,7 +34,11 @@ */ /* SDL includes */ +#ifdef USE_SDL3 +#include +#else #include +#endif /* Local includes */ #include "../../client/header/client.h" @@ -811,7 +815,9 @@ SDL_ClearBuffer(void) clear = 0; } +#ifndef USE_SDL3 SDL_LockAudio(); +#endif if (sound.buffer) { @@ -827,7 +833,9 @@ SDL_ClearBuffer(void) } } +#ifndef USE_SDL3 SDL_UnlockAudio(); +#endif } /* @@ -1191,7 +1199,9 @@ SDL_Update(void) } /* Mix the samples */ +#ifndef USE_SDL3 SDL_LockAudio(); +#endif /* Updates SDL time */ SDL_UpdateSoundtime(); @@ -1221,7 +1231,9 @@ SDL_Update(void) } SDL_PaintChannels(endtime); +#ifndef USE_SDL3 SDL_UnlockAudio(); +#endif } /* ------------------------------------------------------------------ */ @@ -1297,6 +1309,193 @@ SDL_Callback(void *data, Uint8 *stream, int length) } } +#ifdef USE_SDL3 +/* Global stream handle. */ +static SDL_AudioStream *stream; + +/* Wrapper function, ties the old existing callback logic + * from the SDL 1.2 days and later fiddled into SDL 2 to + * a SDL 3 compatible callback... + */ +static void +SDL_SDL3Callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) +{ + if (additional_amount > 0) { + Uint8 *data = SDL_stack_alloc(Uint8, additional_amount); + if (data) { + SDL_Callback(userdata, data, additional_amount); + SDL_PutAudioStreamData(stream, data, additional_amount); + SDL_stack_free(data); + } + } +} + +/* + * Initializes the SDL sound + * backend and sets up SDL. + */ +qboolean +SDL_BackendInit(void) +{ + char reqdriver[128]; + SDL_AudioSpec spec; + int samples, tmp, val; + + /* This should never happen, + but this is Quake 2 ... */ + if (snd_inited) + { + return 1; + } + + int sndbits = (Cvar_Get("sndbits", "16", CVAR_ARCHIVE))->value; + int sndfreq = (Cvar_Get("s_khz", "44", CVAR_ARCHIVE))->value; + int sndchans = (Cvar_Get("sndchannels", "2", CVAR_ARCHIVE))->value; + +#ifdef _WIN32 + s_sdldriver = (Cvar_Get("s_sdldriver", "directsound", CVAR_ARCHIVE)); +#elif __linux__ + s_sdldriver = (Cvar_Get("s_sdldriver", "alsa", CVAR_ARCHIVE)); +#elif __APPLE__ + s_sdldriver = (Cvar_Get("s_sdldriver", "CoreAudio", CVAR_ARCHIVE)); +#else + s_sdldriver = (Cvar_Get("s_sdldriver", "dsp", CVAR_ARCHIVE)); +#endif + + snprintf(reqdriver, sizeof(reqdriver), "%s=%s", "SDL_AUDIODRIVER", s_sdldriver->string); + putenv(reqdriver); + + Com_Printf("Starting SDL audio callback.\n"); + + if (!SDL_WasInit(SDL_INIT_AUDIO)) + { + if (SDL_Init(SDL_INIT_AUDIO) == -1) + { + Com_Printf ("Couldn't init SDL audio: %s.\n", SDL_GetError ()); + return 0; + } + } + const char* drivername = SDL_GetCurrentAudioDriver(); + if(drivername == NULL) + { + drivername = "(UNKNOWN)"; + } + + Com_Printf("SDL audio driver is \"%s\".\n", drivername); + + memset(&samples, '\0', sizeof(samples)); + + /* Users are stupid */ + if ((sndbits != 16) && (sndbits != 8)) + { + sndbits = 16; + } + + if (sndfreq == 48) + { + spec.freq = 48000; + } + else if (sndfreq == 44) + { + spec.freq = 44100; + } + else if (sndfreq == 22) + { + spec.freq = 22050; + } + else if (sndfreq == 11) + { + spec.freq = 11025; + } + + spec.format = ((sndbits == 16) ? SDL_AUDIO_S16 : SDL_AUDIO_U8); + + if (spec.freq <= 11025) + { + samples = 256; + } + else if (spec.freq <= 22050) + { + samples = 512; + } + else if (spec.freq <= 44100) + { + samples = 1024; + } + else + { + samples = 2048; + } + + spec.channels = sndchans; + + /* Okay, let's try our luck */ + stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_OUTPUT, &spec, SDL_SDL3Callback, NULL); + if (stream == NULL) + { + Com_Printf("SDL_OpenAudio() failed: %s\n", SDL_GetError()); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + return 0; + } + + /* This points to the frontend */ + backend = &sound; + + playpos = 0; + backend->samplebits = spec.format & 0xFF; + backend->channels = spec.channels; + + tmp = (samples * spec.channels) * 10; + + if (tmp & (tmp - 1)) + { /* make it a power of two */ + val = 1; + while (val < tmp) + val <<= 1; + + tmp = val; + } + + backend->samples = tmp; + + backend->submission_chunk = 1; + backend->speed = spec.freq; + samplesize = (backend->samples * (backend->samplebits / 8)); + backend->buffer = calloc(1, samplesize); + s_numchannels = MAX_CHANNELS; + + s_underwater->modified = true; + s_underwater_gain_hf->modified = true; + lpf_initialize(&lpf_context, lpf_default_gain_hf, backend->speed); + + SDL_UpdateScaletable(); + SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(stream)); + + Com_Printf("SDL audio initialized.\n"); + + soundtime = 0; + snd_inited = 1; + + return 1; +} + +/* + * Shuts the SDL backend down. + */ +void +SDL_BackendShutdown(void) +{ + Com_Printf("Closing SDL audio device...\n"); + SDL_PauseAudioDevice(SDL_GetAudioStreamDevice(stream)); + SDL_DestroyAudioStream(stream); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + free(backend->buffer); + backend->buffer = NULL; + playpos = samplesize = 0; + snd_inited = 0; + Com_Printf("SDL audio device shut down.\n"); +} +#else /* * Initializes the SDL sound * backend and sets up SDL. @@ -1464,4 +1663,4 @@ SDL_BackendShutdown(void) snd_inited = 0; Com_Printf("SDL audio device shut down.\n"); } - +#endif diff --git a/src/client/vid/glimp_sdl.c b/src/client/vid/glimp_sdl2.c old mode 100755 new mode 100644 similarity index 95% rename from src/client/vid/glimp_sdl.c rename to src/client/vid/glimp_sdl2.c index c8535f53..d2a72591 --- a/src/client/vid/glimp_sdl.c +++ b/src/client/vid/glimp_sdl2.c @@ -33,7 +33,7 @@ #include #include -int glimp_refreshRate = -1; +float glimp_refreshRate = -1.0f; static cvar_t *vid_displayrefreshrate; static cvar_t *vid_displayindex; @@ -733,35 +733,19 @@ GLimp_GrabInput(qboolean grab) } /* - * Returns the current display refresh rate. There're 2 limitations: - * - * * The timing code in frame.c only understands full integers, so - * values given by vid_displayrefreshrate are always round up. For - * example 59.95 become 60. Rounding up is the better choice for - * most users because assuming a too high display refresh rate - * avoids micro stuttering caused by missed frames if the vsync - * is enabled. The price are small and hard to notice timing - * problems. - * - * * SDL returns only full integers. In most cases they're rounded - * up, but in some cases - likely depending on the GPU driver - - * they're rounded down. If the value is rounded up, we'll see - * some small and nard to notice timing problems. If the value - * is rounded down frames will be missed. Both is only relevant - * if the vsync is enabled. + * Returns the current display refresh rate. */ -int +float GLimp_GetRefreshRate(void) { if (vid_displayrefreshrate->value > -1 || vid_displayrefreshrate->modified) { - glimp_refreshRate = ceil(vid_displayrefreshrate->value); + glimp_refreshRate = vid_displayrefreshrate->value; vid_displayrefreshrate->modified = false; } - - if (glimp_refreshRate == -1) + else if (glimp_refreshRate == -1) { SDL_DisplayMode mode; @@ -835,3 +819,12 @@ GLimp_GetWindowDisplayIndex(void) { return last_display; } + +int +GLimp_GetFrameworkVersion(void) +{ + SDL_version ver; + SDL_VERSION(&ver); + + return ver.major; +} diff --git a/src/client/vid/glimp_sdl3.c b/src/client/vid/glimp_sdl3.c new file mode 100644 index 00000000..7d1023ff --- /dev/null +++ b/src/client/vid/glimp_sdl3.c @@ -0,0 +1,913 @@ +/* + * Copyright (C) 2010 Yamagi Burmeister + * Copyright (C) 1997-2001 Id Software, Inc. + * + * 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., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * ======================================================================= + * + * This is the client side of the render backend, implemented trough SDL. + * The SDL window and related functrion (mouse grap, fullscreen switch) + * are implemented here, everything else is in the renderers. + * + * ======================================================================= + */ + +/* TODO SDL3: + * * Bump copyright. + * * Do we need to request High DPI modes when vid_highdpiaware > 0? + * * `fullscreen` should be an enum to make the code more readable. + * * Debug fullscreen handling, maybe refactor it further. + * * Check if window size handling is correct. + * * Check pointers returned by SDL functions for memory leaks. + */ + +#include "../../common/header/common.h" +#include "header/ref.h" + +#include + +float glimp_refreshRate = -1.0f; + +static cvar_t *vid_displayrefreshrate; +static cvar_t *vid_displayindex; +static cvar_t *vid_highdpiaware; +static cvar_t *vid_rate; + +static int last_flags = 0; +static int last_display = 0; +static int last_position_x = SDL_WINDOWPOS_UNDEFINED; +static int last_position_y = SDL_WINDOWPOS_UNDEFINED; +static SDL_Window* window = NULL; +static qboolean initSuccessful = false; +static char **displayindices = NULL; +static int num_displays = 0; + +/* Fullscreen modes */ +enum +{ + FULLSCREEN_OFF = 0, + FULLSCREEN_EXCLUSIVE = 1, + FULLSCREEN_DESKTOP = 2 +}; + +/* + * Resets the display index Cvar if out of bounds + */ +static void +ClampDisplayIndexCvar(void) +{ + if (!vid_displayindex) + { + // uninitialized render? + return; + } + + if (vid_displayindex->value < 0 || vid_displayindex->value >= num_displays) + { + Cvar_SetValue("vid_displayindex", 0); + } +} + +static void +ClearDisplayIndices(void) +{ + if ( displayindices ) + { + for ( int i = 0; i < num_displays; i++ ) + { + free( displayindices[ i ] ); + } + + free( displayindices ); + displayindices = NULL; + } +} + +static qboolean +CreateSDLWindow(int flags, int fullscreen, int w, int h) +{ + if (SDL_WINDOWPOS_ISUNDEFINED(last_position_x) || SDL_WINDOWPOS_ISUNDEFINED(last_position_y) || last_position_x < 0 ||last_position_y < 24) + { + last_position_x = last_position_y = SDL_WINDOWPOS_UNDEFINED_DISPLAY((int)vid_displayindex->value); + } + + /* Force the window to minimize when focus is lost. This was the + * default behavior until SDL 2.0.12 and changed with 2.0.14. + * The windows staying maximized has some odd implications for + * window ordering under Windows and some X11 window managers + * like kwin. See: + * * https://github.com/libsdl-org/SDL/issues/4039 + * * https://github.com/libsdl-org/SDL/issues/3656 */ + SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1"); + + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, "Yamagi Quake II"); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, last_position_x); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, last_position_y); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, w); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, h); + SDL_SetNumberProperty(props, "flags", flags); + + window = SDL_CreateWindowWithProperties(props); + SDL_DestroyProperties(props); + + if (window) + { + /* save current display as default */ + if ((last_display = SDL_GetDisplayForWindow(window)) == 0) + { + /* There are some obscure setups were SDL is + unable to get the current display,one X11 + server with several screen is one of these, + so add a fallback to the first display. */ + last_display = 1; + } + + /* Set requested fullscreen mode. */ + if (flags & SDL_WINDOW_FULLSCREEN) + { + /* SDLs behavior changed between SDL 2 and SDL 3: In SDL 2 + the fullscreen window could be set with whatever mode + was requested. In SDL 3 the fullscreen window is always + created at desktop resolution. If a fullscreen window + is requested, we can't do anything else and are done here. */ + if (fullscreen == FULLSCREEN_DESKTOP) + { + return true; + } + + /* Otherwise try to find a mode near the requested one and + switch to it in exclusive fullscreen mode. */ + /* TODO SDL3: Leak? */ + const SDL_DisplayMode *closestMode = SDL_GetClosestFullscreenDisplayMode(last_display, w, h, vid_rate->value, false); + + if (closestMode == NULL) + { + Com_Printf("SDL was unable to find a mode close to %ix%i@%f\n", w, h, vid_rate->value); + + if (vid_rate->value != 0) + { + Com_Printf("Retrying with desktop refresh rate\n"); + closestMode = SDL_GetClosestFullscreenDisplayMode(last_display, w, h, 0, false); + + if (closestMode != NULL) + { + Cvar_SetValue("vid_rate", 0); + } + else + { + Com_Printf("SDL was unable to find a mode close to %ix%i@0\n", w, h); + return false; + } + } + } + + Com_Printf("User requested %ix%i@%f, setting closest mode %ix%i@%f\n", + w, h, vid_rate->value, closestMode->w, closestMode->h , closestMode->refresh_rate); + + + /* TODO SDL3: Same code is in InitGraphics(), refactor into + * a function? */ + if (SDL_SetWindowFullscreenMode(window, closestMode) < 0) + { + Com_Printf("Couldn't set closest mode: %s\n", SDL_GetError()); + return false; + } + + if (SDL_SetWindowFullscreen(window, true) < 0) + { + Com_Printf("Couldn't switch to exclusive fullscreen: %s\n", SDL_GetError()); + return false; + } + + int ret = SDL_SyncWindow(window); + + if (ret > 0) + { + Com_Printf("Synchronizing window state timed out\n"); + return false; + } + else if (ret < 0) + { + Com_Printf("Couldn't synchronize window state: %s\n", SDL_GetError()); + return false; + } + } + } + else + { + Com_Printf("Creating window failed: %s\n", SDL_GetError()); + return false; + } + + return true; +} + +static int +GetFullscreenType() +{ + if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) + { + /* TODO SDL3: Leak? */ + const SDL_DisplayMode *fsmode = SDL_GetWindowFullscreenMode(window); + + if (fsmode != NULL) + { + return FULLSCREEN_EXCLUSIVE; + } + else + { + return FULLSCREEN_DESKTOP; + } + } + + return FULLSCREEN_OFF; +} + +static qboolean +GetWindowSize(int* w, int* h) +{ + if (window == NULL || w == NULL || h == NULL) + { + return false; + } + + if (SDL_GetWindowSize(window, w, h) < 0) + { + Com_Printf("Couldn't get window size: %s\n", SDL_GetError()); + return false; + } + + return true; +} + +static void +InitDisplayIndices() +{ + displayindices = malloc((num_displays + 1) * sizeof(char *)); + + for ( int i = 0; i < num_displays; i++ ) + { + /* There are a maximum of 10 digits in 32 bit int + 1 for the NULL terminator. */ + displayindices[ i ] = malloc(11 * sizeof( char )); + YQ2_COM_CHECK_OOM(displayindices[i], "malloc()", 11 * sizeof( char )) + + snprintf( displayindices[ i ], 11, "%d", i ); + } + + /* The last entry is NULL to indicate the list of strings ends. */ + displayindices[ num_displays ] = 0; +} + +/* + * Lists all available display modes. + */ +static void +PrintDisplayModes(void) +{ + int curdisplay; + + if (window == NULL) + { + /* Called without a windows, list modes + from the first display. This is the + primary display and likely the one the + game will run on. */ + curdisplay = SDL_GetPrimaryDisplay(); + } + else + { + /* Otherwise use the window were the window + is displayed. There are some obscure + setups were this can fail - one X11 server + with several screen is one of these - so + add a fallback to the first display. */ + if ((curdisplay = SDL_GetDisplayForWindow(window)) == 0) + { + curdisplay = SDL_GetPrimaryDisplay(); + } + } + + int nummodes = 0; + const SDL_DisplayMode **modes = SDL_GetFullscreenDisplayModes(curdisplay, &nummodes); + + if (modes) + { + for (int i = 0; i < nummodes; ++i) + { + const SDL_DisplayMode *mode = modes[i]; + Com_Printf(" - Mode %2i: %ix%i@%.2f\n", i, mode->w, mode->h, mode->refresh_rate); + } + + SDL_free(modes); + } + else + { + Com_Printf("Couldn't get display modes: %s\n", SDL_GetError()); + } +} + +/* + * Sets the window icon + */ +static void +SetSDLIcon() +{ + #include "icon/q2icon64.h" // 64x64 32 Bit + + /* these masks are needed to tell SDL_CreateRGBSurface(From) + to assume the data it gets is byte-wise RGB(A) data */ + Uint32 rmask, gmask, bmask, amask; + +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + int shift = (q2icon64.bytes_per_pixel == 3) ? 8 : 0; + rmask = 0xff000000 >> shift; + gmask = 0x00ff0000 >> shift; + bmask = 0x0000ff00 >> shift; + amask = 0x000000ff >> shift; +#else /* little endian, like x86 */ + rmask = 0x000000ff; + gmask = 0x0000ff00; + bmask = 0x00ff0000; + amask = (q2icon64.bytes_per_pixel == 3) ? 0 : 0xff000000; +#endif + + SDL_Surface* icon = SDL_CreateSurfaceFrom((void *)q2icon64.pixel_data, q2icon64.width, q2icon64.height, q2icon64.bytes_per_pixel * q2icon64.width, SDL_GetPixelFormatEnumForMasks(q2icon64.bytes_per_pixel * 8, rmask, gmask, bmask, amask)); + SDL_SetWindowIcon(window, icon); + SDL_DestroySurface(icon); +} + +// FIXME: We need a header for this. +// Maybe we could put it in vid.h. +void GLimp_GrabInput(qboolean grab); + +/* + * Shuts the SDL render backend down + */ +static void +ShutdownGraphics(void) +{ + ClampDisplayIndexCvar(); + + if (window) + { + /* save current display as default */ + last_display = SDL_GetDisplayForWindow(window); + + /* or if current display isn't the desired default */ + if (last_display != vid_displayindex->value) { + last_position_x = last_position_y = SDL_WINDOWPOS_UNDEFINED; + last_display = vid_displayindex->value; + } + else { + SDL_GetWindowPosition(window, + &last_position_x, &last_position_y); + } + + /* cleanly ungrab input (needs window) */ + GLimp_GrabInput(false); + SDL_DestroyWindow(window); + + window = NULL; + } + + // make sure that after vid_restart the refreshrate will be queried from SDL2 again. + glimp_refreshRate = -1; + + initSuccessful = false; // not initialized anymore +} +// -------- + +/* + * Initializes the SDL video subsystem. Must + * be called before anything else. + */ +qboolean +GLimp_Init(void) +{ + vid_displayrefreshrate = Cvar_Get("vid_displayrefreshrate", "-1", CVAR_ARCHIVE); + vid_displayindex = Cvar_Get("vid_displayindex", "0", CVAR_ARCHIVE); + vid_highdpiaware = Cvar_Get("vid_highdpiaware", "1", CVAR_ARCHIVE); + vid_rate = Cvar_Get("vid_rate", "-1", CVAR_ARCHIVE); + + if (!SDL_WasInit(SDL_INIT_VIDEO)) + { + if (SDL_Init(SDL_INIT_VIDEO) == -1) + { + Com_Printf("Couldn't init SDL video: %s.\n", SDL_GetError()); + + return false; + } + + SDL_Version version; + + SDL_GetVersion(&version); + Com_Printf("-------- vid initialization --------\n"); + Com_Printf("SDL version is: %i.%i.%i\n", (int)version.major, (int)version.minor, (int)version.patch); + Com_Printf("SDL video driver is \"%s\".\n", SDL_GetCurrentVideoDriver()); + + SDL_DisplayID *displays; + if ((displays = SDL_GetDisplays(&num_displays)) == NULL) + { + Com_Printf("Couldn't get number of displays: %s\n", SDL_GetError()); + } + else + { + SDL_free(displays); + } + + InitDisplayIndices(); + ClampDisplayIndexCvar(); + Com_Printf("SDL display modes:\n"); + + PrintDisplayModes(); + Com_Printf("------------------------------------\n\n"); + } + + return true; +} + +/* + * Shuts the SDL video subsystem down. Must + * be called after evrything's finished and + * clean up. + */ +void +GLimp_Shutdown(void) +{ + ShutdownGraphics(); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + ClearDisplayIndices(); +} + +/* + * Determine if we want to be high dpi aware. If + * we are we must scale ourself. If we are not the + * compositor might scale us. + */ +static int +Glimp_DetermineHighDPISupport(int flags) +{ + /* Make sure that high dpi is never set when we don't want it. */ + flags &= ~SDL_WINDOW_HIGH_PIXEL_DENSITY; + + if (vid_highdpiaware->value == 0) + { + return flags; + } + + /* Handle high dpi awareness based on the render backend. + SDL doesn't support high dpi awareness for all backends + and the quality and behavior differs between them. */ + if ((strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0)) + { + flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY; + } + + return flags; +} + +/* + * (Re)initializes the actual window. + */ +qboolean +GLimp_InitGraphics(int fullscreen, int *pwidth, int *pheight) +{ + int flags; + int curWidth, curHeight; + int width = *pwidth; + int height = *pheight; + unsigned int fs_flag = 0; + + if (fullscreen == FULLSCREEN_EXCLUSIVE || fullscreen == FULLSCREEN_DESKTOP) + { + fs_flag = SDL_WINDOW_FULLSCREEN; + } + + /* Only do this if we already have a working window and a fully + initialized rendering backend GLimp_InitGraphics() is also + called when recovering if creating GL context fails or the + one we got is unusable. */ + if (initSuccessful && GetWindowSize(&curWidth, &curHeight) + && (curWidth == width) && (curHeight == height)) + { + /* TODO SDL3: Leak? */ + const SDL_DisplayMode *closestMode = NULL; + + /* If we want fullscreen, but aren't */ + if (GetFullscreenType()) + { + if (fullscreen == FULLSCREEN_EXCLUSIVE) + { + closestMode = SDL_GetClosestFullscreenDisplayMode(last_display, width, height, vid_rate->value, false); + + if (closestMode == NULL) + { + Com_Printf("SDL was unable to find a mode close to %ix%i@%f\n", width, height, vid_rate->value); + + if (vid_rate->value != 0) + { + Com_Printf("Retrying with desktop refresh rate\n"); + closestMode = SDL_GetClosestFullscreenDisplayMode(last_display, width, height, 0, false); + + if (closestMode != NULL) + { + Cvar_SetValue("vid_rate", 0); + } + else + { + Com_Printf("SDL was unable to find a mode close to %ix%i@0\n", width, height); + return false; + } + } + } + } + else if (fullscreen == FULLSCREEN_DESKTOP) + { + /* Fullscreen window */ + closestMode = NULL; + } + + if (SDL_SetWindowFullscreenMode(window, closestMode) < 0) + { + Com_Printf("Couldn't set fullscreen modmode: %s\n", SDL_GetError()); + Cvar_SetValue("vid_fullscreen", 0); + } + else + { + if (SDL_SetWindowFullscreen(window, true) < 0) + { + Com_Printf("Couldn't switch to exclusive fullscreen: %s\n", SDL_GetError()); + Cvar_SetValue("vid_fullscreen", 0); + } + else + { + int ret = SDL_SyncWindow(window); + + if (ret > 0) + { + Com_Printf("Synchronizing window state timed out\n"); + Cvar_SetValue("vid_fullscreen", 0); + } + else if (ret < 0) + { + Com_Printf("Couldn't synchronize window state: %s\n", SDL_GetError()); + Cvar_SetValue("vid_fullscreen", 0); + } + } + } + + Cvar_SetValue("vid_fullscreen", fullscreen); + } + + /* Are we now? */ + if (GetFullscreenType()) + { + return true; + } + } + + /* Is the surface used? */ + if (window) + { + re.ShutdownContext(); + ShutdownGraphics(); + + window = NULL; + } + + if(last_flags != -1 && (last_flags & SDL_WINDOW_OPENGL)) + { + /* Reset SDL. */ + SDL_GL_ResetAttributes(); + } + + /* Let renderer prepare things (set OpenGL attributes). + FIXME: This is no longer necessary, the renderer + could and should pass the flags when calling this + function. */ + flags = re.PrepareForWindow(); + + if (flags == -1) + { + /* It's PrepareForWindow() job to log an error */ + return false; + } + + if (fs_flag) + { + flags |= fs_flag; + } + + /* Check for high dpi support. */ + flags = Glimp_DetermineHighDPISupport(flags); + + /* Mkay, now the hard work. Let's create the window. */ + cvar_t *gl_msaa_samples = Cvar_Get("r_msaa_samples", "0", CVAR_ARCHIVE); + + while (1) + { + if (!CreateSDLWindow(flags, fullscreen, width, height)) + { + if((flags & SDL_WINDOW_OPENGL) && gl_msaa_samples->value) + { + int msaa_samples = gl_msaa_samples->value; + + if (msaa_samples > 0) + { + msaa_samples /= 2; + } + + Com_Printf("SDL SetVideoMode failed: %s\n", SDL_GetError()); + Com_Printf("Reverting to %s r_mode %i (%ix%i) with %dx MSAA.\n", + (flags & fs_flag) ? "fullscreen" : "windowed", + (int) Cvar_VariableValue("r_mode"), width, height, + msaa_samples); + + /* Try to recover */ + Cvar_SetValue("r_msaa_samples", msaa_samples); + + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, + msaa_samples > 0 ? 1 : 0); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, + msaa_samples); + } + else if (width != 640 || height != 480 || (flags & fs_flag)) + { + Com_Printf("SDL SetVideoMode failed: %s\n", SDL_GetError()); + Com_Printf("Reverting to windowed r_mode 4 (640x480).\n"); + + /* Try to recover */ + Cvar_SetValue("r_mode", 4); + Cvar_SetValue("vid_fullscreen", 0); + Cvar_SetValue("vid_rate", -1); + + fullscreen = FULLSCREEN_OFF; + *pwidth = width = 640; + *pheight = height = 480; + flags &= ~fs_flag; + } + else + { + Com_Printf("Failed to revert to r_mode 4. Will try another render backend...\n"); + return false; + } + } + else + { + break; + } + } + + last_flags = flags; + + /* Now that we've got a working window print it's mode. */ + int curdisplay; + if ((curdisplay = SDL_GetDisplayForWindow(window)) == 0) + { + /* There are some obscure setups were SDL is + unable to get the current display,one X11 + server with several screen is one of these, + so add a fallback to the first display. */ + curdisplay = SDL_GetPrimaryDisplay(); + } + + const SDL_DisplayMode *mode; + if ((mode = SDL_GetCurrentDisplayMode(curdisplay)) == NULL) + { + Com_Printf("Couldn't get current display mode: %s\n", SDL_GetError()); + } + else + { + Com_Printf("Real display mode: %ix%i@%.2f\n", mode->w, mode->h, mode->refresh_rate); + } + + + /* Initialize rendering context. */ + if (!re.InitContext(window)) + { + /* InitContext() should have logged an error. */ + return false; + } + + /* We need the actual drawable size for things like the + console, the menus, etc. This might be different to + the resolution due to high dpi awareness. + + The fullscreen window is special. We want it to fill + the screen when native resolution is requestes, all + other cases should look broken. */ + if (flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) + { + if (fullscreen != FULLSCREEN_DESKTOP) + { + re.GetDrawableSize(&viddef.width, &viddef.height); + } + else + { + cvar_t *r_mode = Cvar_Get("r_mode", "4", 0); + + if (r_mode->value == -2 ) + { + re.GetDrawableSize(&viddef.width, &viddef.height); + } + else + { + /* User likes it broken. */ + viddef.width = *pwidth; + viddef.height = *pheight; + } + } + } + else + { + /* Another bug or design failure in SDL: When we are + not high dpi aware the drawable size returned by + SDL may be too small. It seems like the window + decoration are taken into account when they shouldn't. + It can be seen when creating a fullscreen window. + + Work around that by always using the resolution and + not the drawable size when we are not high dpi aware. */ + viddef.width = *pwidth; + viddef.height = *pheight; + } + + Com_Printf("Drawable size: %ix%i\n", viddef.width, viddef.height); + + /* Set the window icon - For SDL2, this must be done after creating the window */ + SetSDLIcon(); + + /* No cursor */ + SDL_ShowCursor(); + + initSuccessful = true; + + return true; +} + +/* + * Shuts the window down. + */ +void +GLimp_ShutdownGraphics(void) +{ + SDL_GL_ResetAttributes(); + ShutdownGraphics(); +} + +/* + * (Un)grab Input + */ +void +GLimp_GrabInput(qboolean grab) +{ + if(window != NULL) + { + SDL_SetWindowMouseGrab(window, grab ? SDL_TRUE : SDL_FALSE); + } + + if(SDL_SetRelativeMouseMode(grab ? SDL_TRUE : SDL_FALSE) < 0) + { + Com_Printf("WARNING: Setting Relative Mousemode failed, reason: %s\n", SDL_GetError()); + Com_Printf(" You should probably update to SDL 2.0.3 or newer!\n"); + } +} + +/* + * Returns the current display refresh rate. + */ +float +GLimp_GetRefreshRate(void) +{ + + if (vid_displayrefreshrate->value > -1 || + vid_displayrefreshrate->modified) + { + glimp_refreshRate = vid_displayrefreshrate->value; + vid_displayrefreshrate->modified = false; + } + else if (glimp_refreshRate == -1) + { + const SDL_DisplayMode *mode; + int curdisplay; + + if (window == NULL) + { + /* This is paranoia. This function should only be + called if there is a working window. Otherwise + things will likely break somewhere else in the + client. */ + curdisplay = SDL_GetPrimaryDisplay(); + } + else + { + if ((curdisplay = SDL_GetDisplayForWindow(window)) == 0) + { + /* There are some obscure setups were SDL is + unable to get the current display,one X11 + server with several screen is one of these, + so add a fallback to the first display. */ + curdisplay = SDL_GetPrimaryDisplay(); + } + + } + + if ((mode = SDL_GetCurrentDisplayMode(curdisplay)) == NULL) + { + printf("Couldn't get display refresh rate: %s\n", SDL_GetError()); + } + else + { + glimp_refreshRate = mode->refresh_rate; + } + } + + return glimp_refreshRate; +} + +/* + * Detect current desktop mode + */ +qboolean +GLimp_GetDesktopMode(int *pwidth, int *pheight) +{ + if (window == NULL) + { + /* Renderers call into this function before the + window is created. This could be refactored + by passing the mode and not the geometry from + the renderer to GLimp_InitGraphics(), however + that would break the renderer API. */ + last_display = SDL_GetPrimaryDisplay(); + } + else + { + /* save current display as default */ + if ((last_display = SDL_GetDisplayForWindow(window)) == 0) + { + /* There are some obscure setups were SDL is + unable to get the current display,one X11 + server with several screen is one of these, + so add a fallback to the first display. */ + last_display = SDL_GetPrimaryDisplay(); + } + + SDL_GetWindowPosition(window, &last_position_x, &last_position_y); + } + + const SDL_DisplayMode *mode; + + if ((mode = SDL_GetCurrentDisplayMode(last_display)) == NULL) + { + Com_Printf("Couldn't detect default desktop mode: %s\n", SDL_GetError()); + return false; + } + + *pwidth = mode->w; + *pheight = mode->h; + + return true; +} + +const char** +GLimp_GetDisplayIndices(void) +{ + return (const char**)displayindices; +} + +int +GLimp_GetNumVideoDisplays(void) +{ + return num_displays; +} + +int +GLimp_GetWindowDisplayIndex(void) +{ + return last_display; +} + +int +GLimp_GetFrameworkVersion(void) +{ + SDL_Version ver; + SDL_VERSION(&ver); + + return ver.major; +} diff --git a/src/client/vid/header/ref.h b/src/client/vid/header/ref.h index fa29e587..b0a667e3 100644 --- a/src/client/vid/header/ref.h +++ b/src/client/vid/header/ref.h @@ -125,7 +125,7 @@ typedef enum { } ref_restart_t; // FIXME: bump API_VERSION? -#define API_VERSION 6 +#define API_VERSION 7 #define EXPORT #define IMPORT @@ -137,6 +137,11 @@ typedef struct // if api_version is different, the dll cannot be used int api_version; + // if framework_version is different, the dll cannot be used + // necessary because differend SDL major version cannot be + // mixed. + int framework_version; + // called when the library is loaded qboolean (EXPORT *Init) (void); diff --git a/src/client/vid/header/vid.h b/src/client/vid/header/vid.h index 845b95af..a176ae86 100644 --- a/src/client/vid/header/vid.h +++ b/src/client/vid/header/vid.h @@ -54,7 +54,7 @@ void VID_MenuDraw(void); const char *VID_MenuKey(int); // Stuff provided by platform backend. -extern int glimp_refreshRate; +extern float glimp_refreshRate; const char **GLimp_GetDisplayIndices(void); int GLimp_GetWindowDisplayIndex(void); @@ -64,7 +64,8 @@ void GLimp_Shutdown(void); qboolean GLimp_InitGraphics(int fullscreen, int *pwidth, int *pheight); void GLimp_ShutdownGraphics(void); void GLimp_GrabInput(qboolean grab); -int GLimp_GetRefreshRate(void); +float GLimp_GetRefreshRate(void); qboolean GLimp_GetDesktopMode(int *pwidth, int *pheight); +int GLimp_GetFrameworkVersion(void); #endif diff --git a/src/client/vid/vid.c b/src/client/vid/vid.c index 8d091ada..d7b2e17d 100644 --- a/src/client/vid/vid.c +++ b/src/client/vid/vid.c @@ -456,9 +456,17 @@ VID_LoadRenderer(void) // Let's check if we've got a compatible renderer. if (re.api_version != API_VERSION) { + Com_Printf("%s has incompatible api_version %d!\n", reflib_name, re.api_version); + VID_ShutdownRenderer(); - Com_Printf("%s has incompatible api_version %d!\n", reflib_name, re.api_version); + return false; + } + else if (re.framework_version != GLimp_GetFrameworkVersion()) + { + Com_Printf("%s has incompatible sdl_version %d!\n", reflib_name, re.framework_version); + + VID_ShutdownRenderer(); return false; } @@ -525,8 +533,9 @@ VID_CheckChanges(void) // Mkay, let's try our luck. while (!VID_LoadRenderer()) { - // We try: custom -> gl3 -> gl1 -> soft. + // We try: custom -> gl3 -> gles3 -> gl1 -> soft. if ((strcmp(vid_renderer->string, "gl3") != 0) && + (strcmp(vid_renderer->string, "gles3") != 0) && (strcmp(vid_renderer->string, "gl1") != 0) && (strcmp(vid_renderer->string, "soft") != 0)) { @@ -534,6 +543,11 @@ VID_CheckChanges(void) Cvar_Set("vid_renderer", "gl3"); } else if (strcmp(vid_renderer->string, "gl3") == 0) + { + Com_Printf("Retrying with gles3...\n"); + Cvar_Set("vid_renderer", "gles"); + } + else if (strcmp(vid_renderer->string, "gles3") == 0) { Com_Printf("Retrying with gl1...\n"); Cvar_Set("vid_renderer", "gl1"); @@ -569,7 +583,7 @@ VID_Init(void) // Console variables vid_gamma = Cvar_Get("vid_gamma", "1.0", CVAR_ARCHIVE); vid_fullscreen = Cvar_Get("vid_fullscreen", "0", CVAR_ARCHIVE); - vid_renderer = Cvar_Get("vid_renderer", "gl1", CVAR_ARCHIVE); + vid_renderer = Cvar_Get("vid_renderer", "gl3", CVAR_ARCHIVE); // Commands Cmd_AddCommand("vid_restart", VID_Restart_f); diff --git a/src/common/frame.c b/src/common/frame.c index 77aa5f8f..f08db6c7 100644 --- a/src/common/frame.c +++ b/src/common/frame.c @@ -53,7 +53,7 @@ cvar_t *showtrace; // Forward declarations #ifndef DEDICATED_ONLY -int GLimp_GetRefreshRate(void); +float GLimp_GetRefreshRate(void); qboolean R_IsVSyncActive(void); #endif @@ -570,7 +570,7 @@ Qcommon_Frame(int usec) // Calculate target and renderframerate. if (R_IsVSyncActive()) { - int refreshrate = GLimp_GetRefreshRate(); + float refreshrate = GLimp_GetRefreshRate(); // using refreshRate - 2, because targeting a value slightly below the // (possibly not 100% correctly reported) refreshRate would introduce jittering, so only