From a79c943d36778c334d074a256e7c328aa565b790 Mon Sep 17 00:00:00 2001 From: Matthew Kilgore Date: Tue, 22 Nov 2022 23:30:20 -0500 Subject: [PATCH 1/7] Add GLUT initialization tests These tests cover all the commands that generally interact with GLUT. The ensure that these functions can be used at the very beginning of a program with no issues. Additionally they verify the behavior of these functions in the presence of `$SCREENHIDE`, and also `_ScreenHide`. --- tests/compile_tests/glut/README.md | 6 ++ tests/compile_tests/glut/desktopheight.bas | 5 ++ tests/compile_tests/glut/desktopheight.output | 1 + tests/compile_tests/glut/desktopwidth.bas | 5 ++ tests/compile_tests/glut/desktopwidth.output | 1 + tests/compile_tests/glut/icon.bas | 6 ++ tests/compile_tests/glut/icon.output | 1 + tests/compile_tests/glut/mousehide.bas | 6 ++ tests/compile_tests/glut/mousehide.output | 1 + tests/compile_tests/glut/mouseshow.bas | 6 ++ tests/compile_tests/glut/mouseshow.output | 1 + tests/compile_tests/glut/qb64pe.ico | Bin 0 -> 68348 bytes tests/compile_tests/glut/screenexists.bas | 5 ++ tests/compile_tests/glut/screenexists.output | 1 + tests/compile_tests/glut/screenhide.bas | 6 ++ tests/compile_tests/glut/screenhide.output | 1 + .../glut/screenhide_commands.bas | 58 ++++++++++++++++++ .../glut/screenhide_commands.output | 14 +++++ tests/compile_tests/glut/screenhide_sub.bas | 30 +++++++++ .../compile_tests/glut/screenhide_sub.output | 13 ++++ tests/compile_tests/glut/screenicon.bas | 5 ++ tests/compile_tests/glut/screenicon.output | 1 + tests/compile_tests/glut/screenshow.bas | 6 ++ tests/compile_tests/glut/screenshow.output | 1 + tests/compile_tests/glut/screenx.bas | 5 ++ tests/compile_tests/glut/screenx.output | 1 + tests/compile_tests/glut/screeny.bas | 5 ++ tests/compile_tests/glut/screeny.output | 1 + tests/compile_tests/glut/title.bas | 6 ++ tests/compile_tests/glut/title.output | 1 + tests/compile_tests/glut/title_func.bas | 5 ++ tests/compile_tests/glut/title_func.output | 1 + tests/compile_tests/glut/windowhandle.bas | 11 ++++ tests/compile_tests/glut/windowhandle.output | 1 + tests/compile_tests/glut/windowhasfocus.bas | 5 ++ .../compile_tests/glut/windowhasfocus.output | 1 + 36 files changed, 223 insertions(+) create mode 100644 tests/compile_tests/glut/README.md create mode 100644 tests/compile_tests/glut/desktopheight.bas create mode 100644 tests/compile_tests/glut/desktopheight.output create mode 100644 tests/compile_tests/glut/desktopwidth.bas create mode 100644 tests/compile_tests/glut/desktopwidth.output create mode 100644 tests/compile_tests/glut/icon.bas create mode 100644 tests/compile_tests/glut/icon.output create mode 100644 tests/compile_tests/glut/mousehide.bas create mode 100644 tests/compile_tests/glut/mousehide.output create mode 100644 tests/compile_tests/glut/mouseshow.bas create mode 100644 tests/compile_tests/glut/mouseshow.output create mode 100644 tests/compile_tests/glut/qb64pe.ico create mode 100644 tests/compile_tests/glut/screenexists.bas create mode 100644 tests/compile_tests/glut/screenexists.output create mode 100644 tests/compile_tests/glut/screenhide.bas create mode 100644 tests/compile_tests/glut/screenhide.output create mode 100644 tests/compile_tests/glut/screenhide_commands.bas create mode 100644 tests/compile_tests/glut/screenhide_commands.output create mode 100644 tests/compile_tests/glut/screenhide_sub.bas create mode 100644 tests/compile_tests/glut/screenhide_sub.output create mode 100644 tests/compile_tests/glut/screenicon.bas create mode 100644 tests/compile_tests/glut/screenicon.output create mode 100644 tests/compile_tests/glut/screenshow.bas create mode 100644 tests/compile_tests/glut/screenshow.output create mode 100644 tests/compile_tests/glut/screenx.bas create mode 100644 tests/compile_tests/glut/screenx.output create mode 100644 tests/compile_tests/glut/screeny.bas create mode 100644 tests/compile_tests/glut/screeny.output create mode 100644 tests/compile_tests/glut/title.bas create mode 100644 tests/compile_tests/glut/title.output create mode 100644 tests/compile_tests/glut/title_func.bas create mode 100644 tests/compile_tests/glut/title_func.output create mode 100644 tests/compile_tests/glut/windowhandle.bas create mode 100644 tests/compile_tests/glut/windowhandle.output create mode 100644 tests/compile_tests/glut/windowhasfocus.bas create mode 100644 tests/compile_tests/glut/windowhasfocus.output diff --git a/tests/compile_tests/glut/README.md b/tests/compile_tests/glut/README.md new file mode 100644 index 000000000..a8fa9e9f5 --- /dev/null +++ b/tests/compile_tests/glut/README.md @@ -0,0 +1,6 @@ +Glut +==== + +These tests cover the initialization of GLUT, and verify that even when these +statements which make use of GLUT are the first thing in the program they still +execute correctly. diff --git a/tests/compile_tests/glut/desktopheight.bas b/tests/compile_tests/glut/desktopheight.bas new file mode 100644 index 000000000..05c2419cc --- /dev/null +++ b/tests/compile_tests/glut/desktopheight.bas @@ -0,0 +1,5 @@ +$CONSOLE +_Dest _Console + +Print _DesktopHeight > 0 +System diff --git a/tests/compile_tests/glut/desktopheight.output b/tests/compile_tests/glut/desktopheight.output new file mode 100644 index 000000000..30610d132 --- /dev/null +++ b/tests/compile_tests/glut/desktopheight.output @@ -0,0 +1 @@ +-1 diff --git a/tests/compile_tests/glut/desktopwidth.bas b/tests/compile_tests/glut/desktopwidth.bas new file mode 100644 index 000000000..4c92785a0 --- /dev/null +++ b/tests/compile_tests/glut/desktopwidth.bas @@ -0,0 +1,5 @@ +$CONSOLE +_Dest _Console + +Print _DesktopWidth > 0 +System diff --git a/tests/compile_tests/glut/desktopwidth.output b/tests/compile_tests/glut/desktopwidth.output new file mode 100644 index 000000000..30610d132 --- /dev/null +++ b/tests/compile_tests/glut/desktopwidth.output @@ -0,0 +1 @@ +-1 diff --git a/tests/compile_tests/glut/icon.bas b/tests/compile_tests/glut/icon.bas new file mode 100644 index 000000000..1d78af21c --- /dev/null +++ b/tests/compile_tests/glut/icon.bas @@ -0,0 +1,6 @@ +$CONSOLE +_Dest _Console + +_Icon +Print "Got Past Icon!" +System diff --git a/tests/compile_tests/glut/icon.output b/tests/compile_tests/glut/icon.output new file mode 100644 index 000000000..d1596b35b --- /dev/null +++ b/tests/compile_tests/glut/icon.output @@ -0,0 +1 @@ +Got Past Icon! diff --git a/tests/compile_tests/glut/mousehide.bas b/tests/compile_tests/glut/mousehide.bas new file mode 100644 index 000000000..e18e388a4 --- /dev/null +++ b/tests/compile_tests/glut/mousehide.bas @@ -0,0 +1,6 @@ +$CONSOLE +_Dest _Console + +_MouseHide +Print "Got Past MouseHide!" +System diff --git a/tests/compile_tests/glut/mousehide.output b/tests/compile_tests/glut/mousehide.output new file mode 100644 index 000000000..deecf500d --- /dev/null +++ b/tests/compile_tests/glut/mousehide.output @@ -0,0 +1 @@ +Got Past MouseHide! diff --git a/tests/compile_tests/glut/mouseshow.bas b/tests/compile_tests/glut/mouseshow.bas new file mode 100644 index 000000000..f97d5f667 --- /dev/null +++ b/tests/compile_tests/glut/mouseshow.bas @@ -0,0 +1,6 @@ +$CONSOLE +_Dest _Console + +_MouseShow +Print "Got Past MouseShow!" +System diff --git a/tests/compile_tests/glut/mouseshow.output b/tests/compile_tests/glut/mouseshow.output new file mode 100644 index 000000000..8dda79e92 --- /dev/null +++ b/tests/compile_tests/glut/mouseshow.output @@ -0,0 +1 @@ +Got Past MouseShow! diff --git a/tests/compile_tests/glut/qb64pe.ico b/tests/compile_tests/glut/qb64pe.ico new file mode 100644 index 0000000000000000000000000000000000000000..541e29c4795d6a88fb14cf4758d0fa1f20a2316b GIT binary patch literal 68348 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�#gR=0uS1rb5*hhjU)yP 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 literal 0 HcmV?d00001 diff --git a/tests/compile_tests/glut/screenexists.bas b/tests/compile_tests/glut/screenexists.bas new file mode 100644 index 000000000..1f6a7ff74 --- /dev/null +++ b/tests/compile_tests/glut/screenexists.bas @@ -0,0 +1,5 @@ +$CONSOLE +_Dest _Console + +Print _ScreenExists +System diff --git a/tests/compile_tests/glut/screenexists.output b/tests/compile_tests/glut/screenexists.output new file mode 100644 index 000000000..33c4d9955 --- /dev/null +++ b/tests/compile_tests/glut/screenexists.output @@ -0,0 +1 @@ + 1 diff --git a/tests/compile_tests/glut/screenhide.bas b/tests/compile_tests/glut/screenhide.bas new file mode 100644 index 000000000..e7b63e601 --- /dev/null +++ b/tests/compile_tests/glut/screenhide.bas @@ -0,0 +1,6 @@ +$CONSOLE +_Dest _Console + +_ScreenHide +Print "Got Past ScreenHide!" +System diff --git a/tests/compile_tests/glut/screenhide.output b/tests/compile_tests/glut/screenhide.output new file mode 100644 index 000000000..900901875 --- /dev/null +++ b/tests/compile_tests/glut/screenhide.output @@ -0,0 +1 @@ +Got Past ScreenHide! diff --git a/tests/compile_tests/glut/screenhide_commands.bas b/tests/compile_tests/glut/screenhide_commands.bas new file mode 100644 index 000000000..6bb92b3c9 --- /dev/null +++ b/tests/compile_tests/glut/screenhide_commands.bas @@ -0,0 +1,58 @@ +$SCREENHIDE +$CONSOLE +_Dest _Console +ON ERROR GOTO errorhand + +$IF WIN THEN +Print _DesktopHeight > 0 +$ELSE +Print _DesktopHeight = 0 +$END IF + +$IF WIN THEN +Print _DesktopWidth > 0 +$ELSE +Print _DesktopWidth = 0 +$END IF + +_Icon +Print "Got past icon!" + +_MouseHide +Print "Got past MouseHide!" + +_MouseShow +Print "Got past MouseHide!" + +Print _ScreenExists + +_ScreenHide +Print "Got past ScreenHide" + +Print _ScreenIcon <> 0 + +$IF LINUX THEN +' Since these functions don't work on linux they also don't trigger errors +' We're just printing the error manually so the test passes on Linux +Print "Error:"; 5 +Print "Error:"; 5 +$ELSE +Print _ScreenX >= 0 +Print _ScreenY >= 0 +$END IF + +_Title "foobar" +Print "Title: "; _Title$ + +Print _WindowHandle <> 0 +Print _WindowHasFocus <= 0 ' This can be a bit random + +_ScreenShow +Print "Got past ScreenShow!" +System + +System + +errorhand: +PRINT "Error:"; ERR +RESUME NEXT diff --git a/tests/compile_tests/glut/screenhide_commands.output b/tests/compile_tests/glut/screenhide_commands.output new file mode 100644 index 000000000..900b089e8 --- /dev/null +++ b/tests/compile_tests/glut/screenhide_commands.output @@ -0,0 +1,14 @@ +-1 +-1 +Got past icon! +Got past MouseHide! +Got past MouseHide! + 0 +Got past ScreenHide + 0 +Error: 5 +Error: 5 +Title: foobar + 0 +-1 +Got past ScreenShow! diff --git a/tests/compile_tests/glut/screenhide_sub.bas b/tests/compile_tests/glut/screenhide_sub.bas new file mode 100644 index 000000000..7a37532fd --- /dev/null +++ b/tests/compile_tests/glut/screenhide_sub.bas @@ -0,0 +1,30 @@ +$CONSOLE +_Dest _Console + +_ScreenHide +Print _DesktopHeight > 0 +Print _DesktopWidth > 0 +_Icon +Print "Got Past Icon!" +_MouseHide +Print "Got Past MouseHide!" +_MouseShow +Print "Got Past MouseShow!" +Print _ScreenExists +Print _ScreenIcon <> 0 +Print _ScreenX >= 0 +Print _ScreenY >= 0 +_Title "foobar" +Print "Title: "; _Title$ + +$IF WIN THEN +Print _WindowHandle <> 0 +$ELSE +Print _WindowHandle = 0 +$END IF + +Print _WindowHasFocus <= 0 ' This can be a bit random + +_ScreenShow +Print "Got past ScreenShow!" +System diff --git a/tests/compile_tests/glut/screenhide_sub.output b/tests/compile_tests/glut/screenhide_sub.output new file mode 100644 index 000000000..617884d16 --- /dev/null +++ b/tests/compile_tests/glut/screenhide_sub.output @@ -0,0 +1,13 @@ +-1 +-1 +Got Past Icon! +Got Past MouseHide! +Got Past MouseShow! + 1 + 0 +-1 +-1 +Title: foobar +-1 +-1 +Got past ScreenShow! diff --git a/tests/compile_tests/glut/screenicon.bas b/tests/compile_tests/glut/screenicon.bas new file mode 100644 index 000000000..91af25c26 --- /dev/null +++ b/tests/compile_tests/glut/screenicon.bas @@ -0,0 +1,5 @@ +$CONSOLE +_Dest _Console + +Print _ScreenIcon <> 0 +System diff --git a/tests/compile_tests/glut/screenicon.output b/tests/compile_tests/glut/screenicon.output new file mode 100644 index 000000000..cff180f6c --- /dev/null +++ b/tests/compile_tests/glut/screenicon.output @@ -0,0 +1 @@ + 0 diff --git a/tests/compile_tests/glut/screenshow.bas b/tests/compile_tests/glut/screenshow.bas new file mode 100644 index 000000000..8f38b48cc --- /dev/null +++ b/tests/compile_tests/glut/screenshow.bas @@ -0,0 +1,6 @@ +$CONSOLE +_Dest _Console + +_ScreenShow +Print "Got past ScreenShow!" +System diff --git a/tests/compile_tests/glut/screenshow.output b/tests/compile_tests/glut/screenshow.output new file mode 100644 index 000000000..a06faccc5 --- /dev/null +++ b/tests/compile_tests/glut/screenshow.output @@ -0,0 +1 @@ +Got past ScreenShow! diff --git a/tests/compile_tests/glut/screenx.bas b/tests/compile_tests/glut/screenx.bas new file mode 100644 index 000000000..86f2eba6b --- /dev/null +++ b/tests/compile_tests/glut/screenx.bas @@ -0,0 +1,5 @@ +$CONSOLE +_Dest _Console + +Print _ScreenX >= 0 +System diff --git a/tests/compile_tests/glut/screenx.output b/tests/compile_tests/glut/screenx.output new file mode 100644 index 000000000..30610d132 --- /dev/null +++ b/tests/compile_tests/glut/screenx.output @@ -0,0 +1 @@ +-1 diff --git a/tests/compile_tests/glut/screeny.bas b/tests/compile_tests/glut/screeny.bas new file mode 100644 index 000000000..0b4cc8186 --- /dev/null +++ b/tests/compile_tests/glut/screeny.bas @@ -0,0 +1,5 @@ +$CONSOLE +_Dest _Console + +Print _ScreenY >= 0 +System diff --git a/tests/compile_tests/glut/screeny.output b/tests/compile_tests/glut/screeny.output new file mode 100644 index 000000000..30610d132 --- /dev/null +++ b/tests/compile_tests/glut/screeny.output @@ -0,0 +1 @@ +-1 diff --git a/tests/compile_tests/glut/title.bas b/tests/compile_tests/glut/title.bas new file mode 100644 index 000000000..b17ef2ea9 --- /dev/null +++ b/tests/compile_tests/glut/title.bas @@ -0,0 +1,6 @@ +$CONSOLE +_Dest _Console + +_Title "foobar" +Print "Got past Title!" +System diff --git a/tests/compile_tests/glut/title.output b/tests/compile_tests/glut/title.output new file mode 100644 index 000000000..58c08ff4d --- /dev/null +++ b/tests/compile_tests/glut/title.output @@ -0,0 +1 @@ +Got past Title! diff --git a/tests/compile_tests/glut/title_func.bas b/tests/compile_tests/glut/title_func.bas new file mode 100644 index 000000000..867ab3bca --- /dev/null +++ b/tests/compile_tests/glut/title_func.bas @@ -0,0 +1,5 @@ +$CONSOLE +_Dest _Console + +Print "Title: "; _Title$ +System diff --git a/tests/compile_tests/glut/title_func.output b/tests/compile_tests/glut/title_func.output new file mode 100644 index 000000000..04eb51c01 --- /dev/null +++ b/tests/compile_tests/glut/title_func.output @@ -0,0 +1 @@ +Title: diff --git a/tests/compile_tests/glut/windowhandle.bas b/tests/compile_tests/glut/windowhandle.bas new file mode 100644 index 000000000..d94f9dfca --- /dev/null +++ b/tests/compile_tests/glut/windowhandle.bas @@ -0,0 +1,11 @@ +$CONSOLE +_Dest _Console + +' _WindowHandle only returns an actual handle on Windows +$IF WIN THEN +Print _WindowHandle <> 0 +$ELSE +Print _WindowHandle = 0 +$END IF + +System diff --git a/tests/compile_tests/glut/windowhandle.output b/tests/compile_tests/glut/windowhandle.output new file mode 100644 index 000000000..30610d132 --- /dev/null +++ b/tests/compile_tests/glut/windowhandle.output @@ -0,0 +1 @@ +-1 diff --git a/tests/compile_tests/glut/windowhasfocus.bas b/tests/compile_tests/glut/windowhasfocus.bas new file mode 100644 index 000000000..1c3f2de03 --- /dev/null +++ b/tests/compile_tests/glut/windowhasfocus.bas @@ -0,0 +1,5 @@ +$CONSOLE +_Dest _Console + +Print _WindowHasFocus <= 0 ' This can be a bit random +System diff --git a/tests/compile_tests/glut/windowhasfocus.output b/tests/compile_tests/glut/windowhasfocus.output new file mode 100644 index 000000000..30610d132 --- /dev/null +++ b/tests/compile_tests/glut/windowhasfocus.output @@ -0,0 +1 @@ +-1 From 3e03cef6520f3e059864510409490f25df70eda5 Mon Sep 17 00:00:00 2001 From: Matthew Kilgore Date: Wed, 23 Nov 2022 02:19:02 -0500 Subject: [PATCH 2/7] Install Xvfb on Linux build agent Xvfb is being used to give us an X server implementation on the Linux build agents. A running X server is necessary for graphics to function (which we have so far avoided testing). --- tests/compile_tests.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/compile_tests.sh b/tests/compile_tests.sh index 6746289dd..ed05cf59e 100755 --- a/tests/compile_tests.sh +++ b/tests/compile_tests.sh @@ -32,6 +32,12 @@ show_incorrect_result() # This is either win, lnx, or osx OS=$CI_OS +# On Linux, we make use of xvfb-run to provide each test with a framebuffer +# based X server, which allows graphics to work. +if [ "$OS" == "lnx" ]; then + LNX_PREFIX=xvfb-run +fi + # Each .bas file represents a separate test. while IFS= read -r test do @@ -107,7 +113,7 @@ do pushd . > /dev/null cd "./tests/compile_tests/$category" - testResult=$("../../../$EXE" "../../../$RESULTS_DIR" "$category-$testName" 2>&1) + testResult=$($LNX_PREFIX "../../../$EXE" "../../../$RESULTS_DIR" "$category-$testName" 2>&1) ERR=$? popd > /dev/null From d678be717c628bf679daf55cef5bd4dc2d7b00d8 Mon Sep 17 00:00:00 2001 From: Matthew Kilgore Date: Thu, 24 Nov 2022 04:50:48 -0500 Subject: [PATCH 3/7] Move GLUT initialization logic into separate .cpp file --- Makefile | 5 + internal/c/libqb.cpp | 534 +----------------- internal/c/libqb/build.mk | 7 + internal/c/libqb/include/glut-thread.h | 31 + internal/c/libqb/include/keyhandler.h | 299 ++++++++++ .../c/libqb/src/console-only-main-thread.cpp | 33 ++ internal/c/libqb/src/glut-main-thread.cpp | 173 ++++++ internal/c/libqb/src/mac-key-monitor.h | 10 + internal/c/libqb/src/mac-key-monitor.mm | 85 +++ internal/c/parts/core/build.mk | 2 + 10 files changed, 655 insertions(+), 524 deletions(-) create mode 100644 internal/c/libqb/include/glut-thread.h create mode 100644 internal/c/libqb/include/keyhandler.h create mode 100644 internal/c/libqb/src/console-only-main-thread.cpp create mode 100644 internal/c/libqb/src/glut-main-thread.cpp create mode 100644 internal/c/libqb/src/mac-key-monitor.h create mode 100644 internal/c/libqb/src/mac-key-monitor.mm diff --git a/Makefile b/Makefile index 9e6f7f7cc..c9b02292b 100644 --- a/Makefile +++ b/Makefile @@ -421,6 +421,11 @@ EXE_OBJS := $(QBLIB) $(EXE_OBJS) %.o: %.cpp $(CXX) $(CXXFLAGS) $< -c -o $@ +ifeq ($(OS),osx) +%.o: %.mm + $(CXX) $(CXXFLAGS) $< -c -o $@ +endif + $(PATH_INTERNAL_TEMP)/data.o: $(PATH_INTERNAL_TEMP)/data.bin $(OBJCOPY) -Ibinary $(OBJCOPY_FLAGS) $< $@ diff --git a/internal/c/libqb.cpp b/internal/c/libqb.cpp index 09a62a662..3c4ed2b22 100644 --- a/internal/c/libqb.cpp +++ b/internal/c/libqb.cpp @@ -29,6 +29,8 @@ #include "image.h" #include "gui.h" #include "http.h" +#include "keyhandler.h" +#include "glut-thread.h" int32 disableEvents = 0; @@ -243,26 +245,10 @@ extern "C" void QB64_Window_Handle(void *handle) { //... } -static bool is_glut_up(); -static void start_glut_thread(); - -#define NEEDS_GLUT(error_result) do { \ - if (!is_glut_up()) { \ - error(5); \ - return error_result; \ - } \ - } while (0) - -#define OPTIONAL_GLUT(result) do { \ - if (!is_glut_up()) \ - return result; \ - } while (0) - // forward references void set_view(int32 new_mode); void set_render_source(int32 new_handle); void set_render_dest(int32 new_handle); -void reinit_glut_callbacks(); int32 framebufferobjects_supported = 0; @@ -1243,294 +1229,6 @@ int64 GetTicks() { return orwl_gettime(); } int64 GetTicks() { return ((((int64)clock()) * ((int64)1000)) / ((int64)CLOCKS_PER_SEC)); } #endif -#define QBK 200000 -#define VK 100000 -#define UC 1073741824 -/* QBK codes: - 200000-200010: Numpad keys with Num-Lock off - NO_NUMLOCK_KP0=INSERT - NO_NUMLOCK_KP1=END - NO_NUMLOCK_KP2=DOWN - NO_NUMLOCK_KP3=PGDOWN - NO_NUMLOCK_KP4... - NO_NUMLOCK_KP5 - NO_NUMLOCK_KP6 - NO_NUMLOCK_KP7 - NO_NUMLOCK_KP8 - NO_NUMLOCK_KP9 - NO_NUMLOCK_KP_PERIOD=DEL - 200011: SCROLL_LOCK_ON - 200012: INSERT_MODE_ON -*/ -#define QBK_SCROLL_LOCK_MODE 11 -#define QBK_INSERT_MODE 12 -#define QBK_CHR0 13 -typedef enum { - QBVK_UNKNOWN = 0, - QBVK_FIRST = 0, - QBVK_BACKSPACE = 8, - QBVK_TAB = 9, - QBVK_CLEAR = 12, - QBVK_RETURN = 13, - QBVK_PAUSE = 19, - QBVK_ESCAPE = 27, - QBVK_SPACE = 32, - QBVK_EXCLAIM = 33, - QBVK_QUOTEDBL = 34, - QBVK_HASH = 35, - QBVK_DOLLAR = 36, - QBVK_AMPERSAND = 38, - QBVK_QUOTE = 39, - QBVK_LEFTPAREN = 40, - QBVK_RIGHTPAREN = 41, - QBVK_ASTERISK = 42, - QBVK_PLUS = 43, - QBVK_COMMA = 44, - QBVK_MINUS = 45, - QBVK_PERIOD = 46, - QBVK_SLASH = 47, - QBVK_0 = 48, - QBVK_1 = 49, - QBVK_2 = 50, - QBVK_3 = 51, - QBVK_4 = 52, - QBVK_5 = 53, - QBVK_6 = 54, - QBVK_7 = 55, - QBVK_8 = 56, - QBVK_9 = 57, - QBVK_COLON = 58, - QBVK_SEMICOLON = 59, - QBVK_LESS = 60, - QBVK_EQUALS = 61, - QBVK_GREATER = 62, - QBVK_QUESTION = 63, - QBVK_AT = 64, - // Skip uppercase letters - QBVK_LEFTBRACKET = 91, - QBVK_BACKSLASH = 92, - QBVK_RIGHTBRACKET = 93, - QBVK_CARET = 94, - QBVK_UNDERSCORE = 95, - QBVK_BACKQUOTE = 96, - QBVK_a = 97, - QBVK_b = 98, - QBVK_c = 99, - QBVK_d = 100, - QBVK_e = 101, - QBVK_f = 102, - QBVK_g = 103, - QBVK_h = 104, - QBVK_i = 105, - QBVK_j = 106, - QBVK_k = 107, - QBVK_l = 108, - QBVK_m = 109, - QBVK_n = 110, - QBVK_o = 111, - QBVK_p = 112, - QBVK_q = 113, - QBVK_r = 114, - QBVK_s = 115, - QBVK_t = 116, - QBVK_u = 117, - QBVK_v = 118, - QBVK_w = 119, - QBVK_x = 120, - QBVK_y = 121, - QBVK_z = 122, - QBVK_DELETE = 127, - // End of ASCII mapped QBVKs - // International QBVKs - QBVK_WORLD_0 = 160, /* 0xA0 */ - QBVK_WORLD_1 = 161, - QBVK_WORLD_2 = 162, - QBVK_WORLD_3 = 163, - QBVK_WORLD_4 = 164, - QBVK_WORLD_5 = 165, - QBVK_WORLD_6 = 166, - QBVK_WORLD_7 = 167, - QBVK_WORLD_8 = 168, - QBVK_WORLD_9 = 169, - QBVK_WORLD_10 = 170, - QBVK_WORLD_11 = 171, - QBVK_WORLD_12 = 172, - QBVK_WORLD_13 = 173, - QBVK_WORLD_14 = 174, - QBVK_WORLD_15 = 175, - QBVK_WORLD_16 = 176, - QBVK_WORLD_17 = 177, - QBVK_WORLD_18 = 178, - QBVK_WORLD_19 = 179, - QBVK_WORLD_20 = 180, - QBVK_WORLD_21 = 181, - QBVK_WORLD_22 = 182, - QBVK_WORLD_23 = 183, - QBVK_WORLD_24 = 184, - QBVK_WORLD_25 = 185, - QBVK_WORLD_26 = 186, - QBVK_WORLD_27 = 187, - QBVK_WORLD_28 = 188, - QBVK_WORLD_29 = 189, - QBVK_WORLD_30 = 190, - QBVK_WORLD_31 = 191, - QBVK_WORLD_32 = 192, - QBVK_WORLD_33 = 193, - QBVK_WORLD_34 = 194, - QBVK_WORLD_35 = 195, - QBVK_WORLD_36 = 196, - QBVK_WORLD_37 = 197, - QBVK_WORLD_38 = 198, - QBVK_WORLD_39 = 199, - QBVK_WORLD_40 = 200, - QBVK_WORLD_41 = 201, - QBVK_WORLD_42 = 202, - QBVK_WORLD_43 = 203, - QBVK_WORLD_44 = 204, - QBVK_WORLD_45 = 205, - QBVK_WORLD_46 = 206, - QBVK_WORLD_47 = 207, - QBVK_WORLD_48 = 208, - QBVK_WORLD_49 = 209, - QBVK_WORLD_50 = 210, - QBVK_WORLD_51 = 211, - QBVK_WORLD_52 = 212, - QBVK_WORLD_53 = 213, - QBVK_WORLD_54 = 214, - QBVK_WORLD_55 = 215, - QBVK_WORLD_56 = 216, - QBVK_WORLD_57 = 217, - QBVK_WORLD_58 = 218, - QBVK_WORLD_59 = 219, - QBVK_WORLD_60 = 220, - QBVK_WORLD_61 = 221, - QBVK_WORLD_62 = 222, - QBVK_WORLD_63 = 223, - QBVK_WORLD_64 = 224, - QBVK_WORLD_65 = 225, - QBVK_WORLD_66 = 226, - QBVK_WORLD_67 = 227, - QBVK_WORLD_68 = 228, - QBVK_WORLD_69 = 229, - QBVK_WORLD_70 = 230, - QBVK_WORLD_71 = 231, - QBVK_WORLD_72 = 232, - QBVK_WORLD_73 = 233, - QBVK_WORLD_74 = 234, - QBVK_WORLD_75 = 235, - QBVK_WORLD_76 = 236, - QBVK_WORLD_77 = 237, - QBVK_WORLD_78 = 238, - QBVK_WORLD_79 = 239, - QBVK_WORLD_80 = 240, - QBVK_WORLD_81 = 241, - QBVK_WORLD_82 = 242, - QBVK_WORLD_83 = 243, - QBVK_WORLD_84 = 244, - QBVK_WORLD_85 = 245, - QBVK_WORLD_86 = 246, - QBVK_WORLD_87 = 247, - QBVK_WORLD_88 = 248, - QBVK_WORLD_89 = 249, - QBVK_WORLD_90 = 250, - QBVK_WORLD_91 = 251, - QBVK_WORLD_92 = 252, - QBVK_WORLD_93 = 253, - QBVK_WORLD_94 = 254, - QBVK_WORLD_95 = 255, /* 0xFF */ - // Numeric keypad - QBVK_KP0 = 256, - QBVK_KP1 = 257, - QBVK_KP2 = 258, - QBVK_KP3 = 259, - QBVK_KP4 = 260, - QBVK_KP5 = 261, - QBVK_KP6 = 262, - QBVK_KP7 = 263, - QBVK_KP8 = 264, - QBVK_KP9 = 265, - QBVK_KP_PERIOD = 266, - QBVK_KP_DIVIDE = 267, - QBVK_KP_MULTIPLY = 268, - QBVK_KP_MINUS = 269, - QBVK_KP_PLUS = 270, - QBVK_KP_ENTER = 271, - QBVK_KP_EQUALS = 272, - // Arrows + Home/End pad - QBVK_UP = 273, - QBVK_DOWN = 274, - QBVK_RIGHT = 275, - QBVK_LEFT = 276, - QBVK_INSERT = 277, - QBVK_HOME = 278, - QBVK_END = 279, - QBVK_PAGEUP = 280, - QBVK_PAGEDOWN = 281, - // Function keys - QBVK_F1 = 282, - QBVK_F2 = 283, - QBVK_F3 = 284, - QBVK_F4 = 285, - QBVK_F5 = 286, - QBVK_F6 = 287, - QBVK_F7 = 288, - QBVK_F8 = 289, - QBVK_F9 = 290, - QBVK_F10 = 291, - QBVK_F11 = 292, - QBVK_F12 = 293, - QBVK_F13 = 294, - QBVK_F14 = 295, - QBVK_F15 = 296, - // Key state modifier keys - QBVK_NUMLOCK = 300, - QBVK_CAPSLOCK = 301, - QBVK_SCROLLOCK = 302, - // If more modifiers are added, the window defocus code in qb64_os_event_linux must be altered - QBVK_RSHIFT = 303, - QBVK_LSHIFT = 304, - QBVK_RCTRL = 305, - QBVK_LCTRL = 306, - QBVK_RALT = 307, - QBVK_LALT = 308, - QBVK_RMETA = 309, - QBVK_LMETA = 310, - QBVK_LSUPER = 311, /* Left "Windows" key */ - QBVK_RSUPER = 312, /* Right "Windows" key */ - QBVK_MODE = 313, /* "Alt Gr" key */ - QBVK_COMPOSE = 314, /* Multi-key compose key */ - // Miscellaneous function keys - QBVK_HELP = 315, - QBVK_PRINT = 316, - QBVK_SYSREQ = 317, - QBVK_BREAK = 318, - QBVK_MENU = 319, - QBVK_POWER = 320, /* Power Macintosh power key */ - QBVK_EURO = 321, /* Some european keyboards */ - QBVK_UNDO = 322, /* Atari keyboard has Undo */ - QBVK_LAST -} QBVKs; -// Enumeration of valid key mods (possibly OR'd together) -typedef enum { - KMOD_NONE = 0x0000, - KMOD_LSHIFT = 0x0001, - KMOD_RSHIFT = 0x0002, - KMOD_LCTRL = 0x0040, - KMOD_RCTRL = 0x0080, - KMOD_LALT = 0x0100, - KMOD_RALT = 0x0200, - KMOD_LMETA = 0x0400, - KMOD_RMETA = 0x0800, - KMOD_NUM = 0x1000, - KMOD_CAPS = 0x2000, - KMOD_MODE = 0x4000, - KMOD_RESERVED = 0x8000 -} KMODs; -#define KMOD_CTRL (KMOD_LCTRL | KMOD_RCTRL) -#define KMOD_SHIFT (KMOD_LSHIFT | KMOD_RSHIFT) -#define KMOD_ALT (KMOD_LALT | KMOD_RALT) -#define KMOD_META (KMOD_LMETA | KMOD_RMETA) - /* Restricted Functionality: (Security focused approach, does not include restricting sound etc) Block while compiling: (ONLY things that cannot be caught at runtime) @@ -27547,7 +27245,7 @@ void sub_screenicon() { int32 func_windowexists() { #ifdef QB64_GLUT - return is_glut_up(); + return libqb_is_glut_up(); #else return -1; #endif @@ -34286,7 +33984,7 @@ void sub__screenshow() { #ifdef QB64_GLUT screen_hide = 0; // $SCREENHIDE programs will not have the window running - start_glut_thread(); + libqb_start_glut_thread(); glutShowWindow(); #endif } @@ -34296,7 +33994,9 @@ void sub__screenhide() { return; #ifdef QB64_GLUT - // start_glut_thread(); + // This is probably unnecessary, no conditions allow for screen_hide==0 + // without GLUT running, but it doesn't hurt anything. + libqb_start_glut_thread(); glutHideWindow(); #endif @@ -37367,165 +37067,6 @@ qbs *func__dir(qbs *context_in) { #endif } -static void glutWarning(const char *fmt, va_list lst) { - // Do something -} - -// Performs all of the FreeGLUT initialization except for calling glutMainLoop() -static void initialize_glut(int argc, char **argv) { -#ifdef QB64_GLUT -# ifdef CORE_FREEGLUT - // This keeps FreeGlut from dumping warnings to console - glutInitWarningFunc(glutWarning); - glutInitErrorFunc(glutWarning); -# endif - - glutInit(&argc, argv); - -# ifdef QB64_MACOSX - [NSEvent addLocalMonitorForEventsMatchingMask:NSFlagsChangedMask - handler:^NSEvent *(NSEvent *event) { - // notes on bitfields: - // if ([event modifierFlags] == 131330) keydown_vk(VK+QBVK_LSHIFT);// 100000000100000010 - // if ([event modifierFlags] == 131332) keydown_vk(VK+QBVK_RSHIFT);// 100000000100000100 - // if ([event modifierFlags] == 262401) keydown_vk(VK+QBVK_LCTRL); //1000000000100000001 - // if ([event modifierFlags] == 270592) keydown_vk(VK+QBVK_RCTRL); //1000010000100000000 - // if ([event modifierFlags] == 524576) keydown_vk(VK+QBVK_LALT); //10000000000100100000 - // if ([event modifierFlags] == 524608) keydown_vk(VK+QBVK_RALT); //10000000000101000000 - // caps lock // 10000000100000000 - - int x = [event modifierFlags]; - - if (x & (1 << 0)) { - if (!keyheld(VK + QBVK_LCTRL)) - keydown_vk(VK + QBVK_LCTRL); - } else { - if (keyheld(VK + QBVK_LCTRL)) - keyup_vk(VK + QBVK_LCTRL); - } - if (x & (1 << 13)) { - if (!keyheld(VK + QBVK_RCTRL)) - keydown_vk(VK + QBVK_RCTRL); - } else { - if (keyheld(VK + QBVK_RCTRL)) - keyup_vk(VK + QBVK_RCTRL); - } - - if (x & (1 << 1)) { - if (!keyheld(VK + QBVK_LSHIFT)) - keydown_vk(VK + QBVK_LSHIFT); - } else { - if (keyheld(VK + QBVK_LSHIFT)) - keyup_vk(VK + QBVK_LSHIFT); - } - if (x & (1 << 2)) { - if (!keyheld(VK + QBVK_RSHIFT)) - keydown_vk(VK + QBVK_RSHIFT); - } else { - if (keyheld(VK + QBVK_RSHIFT)) - keyup_vk(VK + QBVK_RSHIFT); - } - - if (x & (1 << 5)) { - if (!keyheld(VK + QBVK_LALT)) - keydown_vk(VK + QBVK_LALT); - } else { - if (keyheld(VK + QBVK_LALT)) - keyup_vk(VK + QBVK_LALT); - } - if (x & (1 << 6)) { - if (!keyheld(VK + QBVK_RALT)) - keydown_vk(VK + QBVK_RALT); - } else { - if (keyheld(VK + QBVK_RALT)) - keyup_vk(VK + QBVK_RALT); - } - - if (x & (1 << 16)) { - if (!keyheld(VK + QBVK_CAPSLOCK)) - keydown_vk(VK + QBVK_CAPSLOCK); - } else { - if (keyheld(VK + QBVK_CAPSLOCK)) - keyup_vk(VK + QBVK_CAPSLOCK); - } - - return event; - }]; -# endif - -# ifdef QB64_WINDOWS - glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH | GLUT_MULTISAMPLE); -# else - glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH); -# endif - - glutInitWindowSize(640, 400); // cannot be changed unless display_x(etc) are modified - - if (!glutGet(GLUT_DISPLAY_MODE_POSSIBLE)) // must be called on Linux or GLUT crashes - { - exit(1); - } - - if (!window_title) { - glutCreateWindow("Untitled"); - } else { - glutCreateWindow((char *)window_title); - } - - GLenum err = glewInit(); - if (GLEW_OK != err) { - gui_alert((char *)glewGetErrorString(err)); - } - if (glewIsSupported("GL_EXT_framebuffer_object")) - framebufferobjects_supported = 1; - - glutDisplayFunc(GLUT_DISPLAY_REQUEST); - -# ifdef QB64_WINDOWS - glutTimerFunc(8, GLUT_TIMER_EVENT, 0); -# else - glutIdleFunc(GLUT_IDLEFUNC); -# endif - - glutKeyboardFunc(GLUT_KEYBOARD_FUNC); - glutKeyboardUpFunc(GLUT_KEYBOARDUP_FUNC); - glutSpecialFunc(GLUT_SPECIAL_FUNC); - glutSpecialUpFunc(GLUT_SPECIALUP_FUNC); - glutMouseFunc(GLUT_MOUSE_FUNC); - glutMotionFunc(GLUT_MOTION_FUNC); - glutPassiveMotionFunc(GLUT_PASSIVEMOTION_FUNC); - glutReshapeFunc(GLUT_RESHAPE_FUNC); - -# ifdef CORE_FREEGLUT - glutMouseWheelFunc(GLUT_MOUSEWHEEL_FUNC); -# endif -#endif // QB64_GLUT -} - -static bool glut_is_started; -static struct completion glut_thread_starter; -static struct completion *glut_thread_initialized; - -static void start_glut_thread() { - if (glut_is_started) - return; - - struct completion init; - completion_init(&init); - - glut_thread_initialized = &init; - - completion_finish(&glut_thread_starter); - - completion_wait(&init); - completion_clear(&init); -} - -// Checks whether the GLUT thread is running -static bool is_glut_up() { - return glut_is_started; -} - extern void set_dynamic_info(); int main(int argc, char *argv[]) { @@ -37967,14 +37508,7 @@ int main(int argc, char *argv[]) { libqb_http_init(); -#ifdef QB64_GUI - if (!screen_hide) { - initialize_glut(argc, argv); // Initialize GLUT if the screen isn't hidden - glut_is_started = true; - } else { - completion_init(&glut_thread_starter); - } -#endif + libqb_glut_presetup(argc, argv); struct libqb_thread *qbmain = libqb_thread_new(); libqb_thread_start(qbmain, QBMAIN, NULL); @@ -37984,32 +37518,9 @@ int main(int argc, char *argv[]) { lock_display_required = 1; -#ifdef QB64_GUI + libqb_start_main_thread(argc, argv); - struct libqb_thread *main_loop = libqb_thread_new(); - libqb_thread_start(main_loop, MAIN_LOOP, NULL); - - // This happens for $SCREENHIDE programs. This thread waits on the - // `glut_thread_starter` completion, which will get completed if a - // _ScreenShow is used. - if (!glut_is_started) { - completion_wait(&glut_thread_starter); - - initialize_glut(argc, argv); - glut_is_started = true; - - if (glut_thread_initialized) - completion_finish(glut_thread_initialized); - } - - glutMainLoop(); - -#else - // normally MAIN_LOOP() is launched in a separate thread to reserve the primary thread for GLUT - // that is not required, so run MAIN_LOOP() in our primary thread - MAIN_LOOP(NULL); - return 0; -#endif + return 0; // Should never get here } //###################### Main Loop #################### @@ -40238,31 +39749,6 @@ extern "C" int qb64_custom_event(int event, int v1, int v2, int v3, int v4, int return -1; // Unknown command (use for debugging purposes only) } // qb64_custom_event -void reinit_glut_callbacks() { - -#ifdef QB64_GLUT - - glutDisplayFunc(GLUT_DISPLAY_REQUEST); -# ifdef QB64_WINDOWS - glutTimerFunc(8, GLUT_TIMER_EVENT, 0); -# else - glutIdleFunc(GLUT_IDLEFUNC); -# endif - glutKeyboardFunc(GLUT_KEYBOARD_FUNC); - glutKeyboardUpFunc(GLUT_KEYBOARDUP_FUNC); - glutSpecialFunc(GLUT_SPECIAL_FUNC); - glutSpecialUpFunc(GLUT_SPECIALUP_FUNC); - glutMouseFunc(GLUT_MOUSE_FUNC); - glutMotionFunc(GLUT_MOTION_FUNC); - glutPassiveMotionFunc(GLUT_PASSIVEMOTION_FUNC); - glutReshapeFunc(GLUT_RESHAPE_FUNC); -# ifdef CORE_FREEGLUT - glutMouseWheelFunc(GLUT_MOUSEWHEEL_FUNC); -# endif - -#endif -} - int32 func__capslock() { #ifdef QB64_WINDOWS return -GetKeyState(VK_CAPITAL); diff --git a/internal/c/libqb/build.mk b/internal/c/libqb/build.mk index 3350de34c..bde5b3834 100644 --- a/internal/c/libqb/build.mk +++ b/internal/c/libqb/build.mk @@ -8,4 +8,11 @@ libqb-objs-y$(DEP_HTTP) += $(PATH_LIBQB)/src/http-stub.o libqb-objs-y += $(PATH_LIBQB)/src/threading-$(PLATFORM).o +libqb-objs-y$(DEP_CONSOLE_ONLY) += $(PATH_LIBQB)/src/glut-main-thread.o +libqb-objs-$(DEP_CONSOLE_ONLY) += $(PATH_LIBQB)/src/console-only-main-thread.o + +ifeq ($(OS),osx) +libqb-objs-y$(DEP_CONSOLE_ONLY) += $(PATH_LIBQB)/src/mac-key-monitor.o +endif + CLEAN_LIST += $(libqb-objs-y) $(libqb-objs-yy) $(libqb-objs-) diff --git a/internal/c/libqb/include/glut-thread.h b/internal/c/libqb/include/glut-thread.h new file mode 100644 index 000000000..d87397e9e --- /dev/null +++ b/internal/c/libqb/include/glut-thread.h @@ -0,0 +1,31 @@ +#ifndef INCLUDE_LIBQB_GLUT_THREAD_H +#define INCLUDE_LIBQB_GLUT_THREAD_H + +// Called to potentially setup GLUT before starting the program. +void libqb_glut_presetup(int argc, char **argv); + +// Starts the "main thread", including handling all the GLUT setup. +void libqb_start_main_thread(int argc, char **argv); + +// Used to support _ScreenShow, which can start the GLUT thread after the +// program is started +void libqb_start_glut_thread(); + +// Indicates whether GLUT is currently running (and thus whether we're able to +// do any GLUT-related stuff +bool libqb_is_glut_up(); + +// Convinence macros, exists a function depending on the state of GLUT +#define NEEDS_GLUT(error_result) do { \ + if (!libqb_is_glut_up()) { \ + error(5); \ + return error_result; \ + } \ + } while (0) + +#define OPTIONAL_GLUT(result) do { \ + if (!libqb_is_glut_up()) \ + return result; \ + } while (0) + +#endif diff --git a/internal/c/libqb/include/keyhandler.h b/internal/c/libqb/include/keyhandler.h new file mode 100644 index 000000000..4909c5ae8 --- /dev/null +++ b/internal/c/libqb/include/keyhandler.h @@ -0,0 +1,299 @@ +#ifndef INCLUDE_LIBQB_KEYHANDLER_h +#define INCLUDE_LIBQB_KEYHANDLER_h + +#include + +int32_t keyheld(uint32_t x); + +void keydown_vk(uint32_t key); +void keyup_vk(uint32_t key); + +#define QBK 200000 +#define VK 100000 +#define UC 1073741824 +/* QBK codes: + 200000-200010: Numpad keys with Num-Lock off + NO_NUMLOCK_KP0=INSERT + NO_NUMLOCK_KP1=END + NO_NUMLOCK_KP2=DOWN + NO_NUMLOCK_KP3=PGDOWN + NO_NUMLOCK_KP4... + NO_NUMLOCK_KP5 + NO_NUMLOCK_KP6 + NO_NUMLOCK_KP7 + NO_NUMLOCK_KP8 + NO_NUMLOCK_KP9 + NO_NUMLOCK_KP_PERIOD=DEL + 200011: SCROLL_LOCK_ON + 200012: INSERT_MODE_ON +*/ +#define QBK_SCROLL_LOCK_MODE 11 +#define QBK_INSERT_MODE 12 +#define QBK_CHR0 13 +typedef enum { + QBVK_UNKNOWN = 0, + QBVK_FIRST = 0, + QBVK_BACKSPACE = 8, + QBVK_TAB = 9, + QBVK_CLEAR = 12, + QBVK_RETURN = 13, + QBVK_PAUSE = 19, + QBVK_ESCAPE = 27, + QBVK_SPACE = 32, + QBVK_EXCLAIM = 33, + QBVK_QUOTEDBL = 34, + QBVK_HASH = 35, + QBVK_DOLLAR = 36, + QBVK_AMPERSAND = 38, + QBVK_QUOTE = 39, + QBVK_LEFTPAREN = 40, + QBVK_RIGHTPAREN = 41, + QBVK_ASTERISK = 42, + QBVK_PLUS = 43, + QBVK_COMMA = 44, + QBVK_MINUS = 45, + QBVK_PERIOD = 46, + QBVK_SLASH = 47, + QBVK_0 = 48, + QBVK_1 = 49, + QBVK_2 = 50, + QBVK_3 = 51, + QBVK_4 = 52, + QBVK_5 = 53, + QBVK_6 = 54, + QBVK_7 = 55, + QBVK_8 = 56, + QBVK_9 = 57, + QBVK_COLON = 58, + QBVK_SEMICOLON = 59, + QBVK_LESS = 60, + QBVK_EQUALS = 61, + QBVK_GREATER = 62, + QBVK_QUESTION = 63, + QBVK_AT = 64, + // Skip uppercase letters + QBVK_LEFTBRACKET = 91, + QBVK_BACKSLASH = 92, + QBVK_RIGHTBRACKET = 93, + QBVK_CARET = 94, + QBVK_UNDERSCORE = 95, + QBVK_BACKQUOTE = 96, + QBVK_a = 97, + QBVK_b = 98, + QBVK_c = 99, + QBVK_d = 100, + QBVK_e = 101, + QBVK_f = 102, + QBVK_g = 103, + QBVK_h = 104, + QBVK_i = 105, + QBVK_j = 106, + QBVK_k = 107, + QBVK_l = 108, + QBVK_m = 109, + QBVK_n = 110, + QBVK_o = 111, + QBVK_p = 112, + QBVK_q = 113, + QBVK_r = 114, + QBVK_s = 115, + QBVK_t = 116, + QBVK_u = 117, + QBVK_v = 118, + QBVK_w = 119, + QBVK_x = 120, + QBVK_y = 121, + QBVK_z = 122, + QBVK_DELETE = 127, + // End of ASCII mapped QBVKs + // International QBVKs + QBVK_WORLD_0 = 160, /* 0xA0 */ + QBVK_WORLD_1 = 161, + QBVK_WORLD_2 = 162, + QBVK_WORLD_3 = 163, + QBVK_WORLD_4 = 164, + QBVK_WORLD_5 = 165, + QBVK_WORLD_6 = 166, + QBVK_WORLD_7 = 167, + QBVK_WORLD_8 = 168, + QBVK_WORLD_9 = 169, + QBVK_WORLD_10 = 170, + QBVK_WORLD_11 = 171, + QBVK_WORLD_12 = 172, + QBVK_WORLD_13 = 173, + QBVK_WORLD_14 = 174, + QBVK_WORLD_15 = 175, + QBVK_WORLD_16 = 176, + QBVK_WORLD_17 = 177, + QBVK_WORLD_18 = 178, + QBVK_WORLD_19 = 179, + QBVK_WORLD_20 = 180, + QBVK_WORLD_21 = 181, + QBVK_WORLD_22 = 182, + QBVK_WORLD_23 = 183, + QBVK_WORLD_24 = 184, + QBVK_WORLD_25 = 185, + QBVK_WORLD_26 = 186, + QBVK_WORLD_27 = 187, + QBVK_WORLD_28 = 188, + QBVK_WORLD_29 = 189, + QBVK_WORLD_30 = 190, + QBVK_WORLD_31 = 191, + QBVK_WORLD_32 = 192, + QBVK_WORLD_33 = 193, + QBVK_WORLD_34 = 194, + QBVK_WORLD_35 = 195, + QBVK_WORLD_36 = 196, + QBVK_WORLD_37 = 197, + QBVK_WORLD_38 = 198, + QBVK_WORLD_39 = 199, + QBVK_WORLD_40 = 200, + QBVK_WORLD_41 = 201, + QBVK_WORLD_42 = 202, + QBVK_WORLD_43 = 203, + QBVK_WORLD_44 = 204, + QBVK_WORLD_45 = 205, + QBVK_WORLD_46 = 206, + QBVK_WORLD_47 = 207, + QBVK_WORLD_48 = 208, + QBVK_WORLD_49 = 209, + QBVK_WORLD_50 = 210, + QBVK_WORLD_51 = 211, + QBVK_WORLD_52 = 212, + QBVK_WORLD_53 = 213, + QBVK_WORLD_54 = 214, + QBVK_WORLD_55 = 215, + QBVK_WORLD_56 = 216, + QBVK_WORLD_57 = 217, + QBVK_WORLD_58 = 218, + QBVK_WORLD_59 = 219, + QBVK_WORLD_60 = 220, + QBVK_WORLD_61 = 221, + QBVK_WORLD_62 = 222, + QBVK_WORLD_63 = 223, + QBVK_WORLD_64 = 224, + QBVK_WORLD_65 = 225, + QBVK_WORLD_66 = 226, + QBVK_WORLD_67 = 227, + QBVK_WORLD_68 = 228, + QBVK_WORLD_69 = 229, + QBVK_WORLD_70 = 230, + QBVK_WORLD_71 = 231, + QBVK_WORLD_72 = 232, + QBVK_WORLD_73 = 233, + QBVK_WORLD_74 = 234, + QBVK_WORLD_75 = 235, + QBVK_WORLD_76 = 236, + QBVK_WORLD_77 = 237, + QBVK_WORLD_78 = 238, + QBVK_WORLD_79 = 239, + QBVK_WORLD_80 = 240, + QBVK_WORLD_81 = 241, + QBVK_WORLD_82 = 242, + QBVK_WORLD_83 = 243, + QBVK_WORLD_84 = 244, + QBVK_WORLD_85 = 245, + QBVK_WORLD_86 = 246, + QBVK_WORLD_87 = 247, + QBVK_WORLD_88 = 248, + QBVK_WORLD_89 = 249, + QBVK_WORLD_90 = 250, + QBVK_WORLD_91 = 251, + QBVK_WORLD_92 = 252, + QBVK_WORLD_93 = 253, + QBVK_WORLD_94 = 254, + QBVK_WORLD_95 = 255, /* 0xFF */ + // Numeric keypad + QBVK_KP0 = 256, + QBVK_KP1 = 257, + QBVK_KP2 = 258, + QBVK_KP3 = 259, + QBVK_KP4 = 260, + QBVK_KP5 = 261, + QBVK_KP6 = 262, + QBVK_KP7 = 263, + QBVK_KP8 = 264, + QBVK_KP9 = 265, + QBVK_KP_PERIOD = 266, + QBVK_KP_DIVIDE = 267, + QBVK_KP_MULTIPLY = 268, + QBVK_KP_MINUS = 269, + QBVK_KP_PLUS = 270, + QBVK_KP_ENTER = 271, + QBVK_KP_EQUALS = 272, + // Arrows + Home/End pad + QBVK_UP = 273, + QBVK_DOWN = 274, + QBVK_RIGHT = 275, + QBVK_LEFT = 276, + QBVK_INSERT = 277, + QBVK_HOME = 278, + QBVK_END = 279, + QBVK_PAGEUP = 280, + QBVK_PAGEDOWN = 281, + // Function keys + QBVK_F1 = 282, + QBVK_F2 = 283, + QBVK_F3 = 284, + QBVK_F4 = 285, + QBVK_F5 = 286, + QBVK_F6 = 287, + QBVK_F7 = 288, + QBVK_F8 = 289, + QBVK_F9 = 290, + QBVK_F10 = 291, + QBVK_F11 = 292, + QBVK_F12 = 293, + QBVK_F13 = 294, + QBVK_F14 = 295, + QBVK_F15 = 296, + // Key state modifier keys + QBVK_NUMLOCK = 300, + QBVK_CAPSLOCK = 301, + QBVK_SCROLLOCK = 302, + // If more modifiers are added, the window defocus code in qb64_os_event_linux must be altered + QBVK_RSHIFT = 303, + QBVK_LSHIFT = 304, + QBVK_RCTRL = 305, + QBVK_LCTRL = 306, + QBVK_RALT = 307, + QBVK_LALT = 308, + QBVK_RMETA = 309, + QBVK_LMETA = 310, + QBVK_LSUPER = 311, /* Left "Windows" key */ + QBVK_RSUPER = 312, /* Right "Windows" key */ + QBVK_MODE = 313, /* "Alt Gr" key */ + QBVK_COMPOSE = 314, /* Multi-key compose key */ + // Miscellaneous function keys + QBVK_HELP = 315, + QBVK_PRINT = 316, + QBVK_SYSREQ = 317, + QBVK_BREAK = 318, + QBVK_MENU = 319, + QBVK_POWER = 320, /* Power Macintosh power key */ + QBVK_EURO = 321, /* Some european keyboards */ + QBVK_UNDO = 322, /* Atari keyboard has Undo */ + QBVK_LAST +} QBVKs; +// Enumeration of valid key mods (possibly OR'd together) +typedef enum { + KMOD_NONE = 0x0000, + KMOD_LSHIFT = 0x0001, + KMOD_RSHIFT = 0x0002, + KMOD_LCTRL = 0x0040, + KMOD_RCTRL = 0x0080, + KMOD_LALT = 0x0100, + KMOD_RALT = 0x0200, + KMOD_LMETA = 0x0400, + KMOD_RMETA = 0x0800, + KMOD_NUM = 0x1000, + KMOD_CAPS = 0x2000, + KMOD_MODE = 0x4000, + KMOD_RESERVED = 0x8000 +} KMODs; +#define KMOD_CTRL (KMOD_LCTRL | KMOD_RCTRL) +#define KMOD_SHIFT (KMOD_LSHIFT | KMOD_RSHIFT) +#define KMOD_ALT (KMOD_LALT | KMOD_RALT) +#define KMOD_META (KMOD_LMETA | KMOD_RMETA) + +#endif diff --git a/internal/c/libqb/src/console-only-main-thread.cpp b/internal/c/libqb/src/console-only-main-thread.cpp new file mode 100644 index 000000000..cb3a401f1 --- /dev/null +++ b/internal/c/libqb/src/console-only-main-thread.cpp @@ -0,0 +1,33 @@ + +#include "libqb-common.h" + +#include +#include +#include +#include + +#include "glut-thread.h" + +// This file is for Console-Only programs. They never invoke GLUT so the setup +// here is much simpler. + +// FIXME: PUt this definition somewhere else +void MAIN_LOOP(void *); + + +void libqb_glut_presetup(int argc, char **argv) { + +} + +void libqb_start_main_thread(int argc, char **argv) { + // Because GLUT is not used, we can just run MAIN_LOOP without creating a + // new thread for it. + MAIN_LOOP(NULL); +} + +void libqb_start_glut_thread() { +} + +bool libqb_is_glut_up() { + return false; +} diff --git a/internal/c/libqb/src/glut-main-thread.cpp b/internal/c/libqb/src/glut-main-thread.cpp new file mode 100644 index 000000000..819a5a233 --- /dev/null +++ b/internal/c/libqb/src/glut-main-thread.cpp @@ -0,0 +1,173 @@ + +#include "libqb-common.h" + +#include +#include +#include +#include +#include +#include +#include +#include "GL/glew.h" + +// note: MacOSX uses Apple's GLUT not FreeGLUT +#ifdef QB64_MACOSX +# include +#else +# define CORE_FREEGLUT +# include "freeglut.h" +#endif + +#include "mutex.h" +#include "thread.h" +#include "completion.h" +#include "gui.h" +#include "mac-key-monitor.h" +#include "glut-thread.h" + +// FIXME: These extern variable and function definitions should probably go +// somewhere more global so that they can be referenced by libqb.cpp +extern uint8_t *window_title; +extern int32_t framebufferobjects_supported; +extern int32_t screen_hide; + +void MAIN_LOOP(void *); +void GLUT_KEYBOARD_FUNC(unsigned char key, int x, int y); +void GLUT_DISPLAY_REQUEST(); +void GLUT_KEYBOARDUP_FUNC(unsigned char key, int x, int y); +void GLUT_SPECIAL_FUNC(int key, int x, int y); +void GLUT_SPECIALUP_FUNC(int key, int x, int y); +void GLUT_MOUSE_FUNC(int glut_button, int state, int x, int y); +void GLUT_MOTION_FUNC(int x, int y); +void GLUT_PASSIVEMOTION_FUNC(int x, int y); +void GLUT_RESHAPE_FUNC(int width, int height); + +#ifdef QB64_WINDOWS +void GLUT_TIMER_EVENT(int ignore); +#else +void GLUT_IDLEFUNC(); +#endif + +#ifdef CORE_FREEGLUT +void GLUT_MOUSEWHEEL_FUNC(int wheel, int direction, int x, int y); +#endif + +static void glutWarning(const char *fmt, va_list lst) { + // This keeps FreeGlut from dumping warnings to console +} + +// Performs all of the FreeGLUT initialization except for calling glutMainLoop() +static void initialize_glut(int argc, char **argv) { +# ifdef CORE_FREEGLUT + glutInitWarningFunc(glutWarning); + glutInitErrorFunc(glutWarning); +# endif + + glutInit(&argc, argv); + + mac_register_key_handler(); + +# ifdef QB64_WINDOWS + glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH | GLUT_MULTISAMPLE); +# else + glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH); +# endif + + glutInitWindowSize(640, 400); // cannot be changed unless display_x(etc) are modified + + if (!glutGet(GLUT_DISPLAY_MODE_POSSIBLE)) // must be called on Linux or GLUT crashes + { + exit(1); + } + + if (!window_title) { + glutCreateWindow("Untitled"); + } else { + glutCreateWindow((char *)window_title); + } + + GLenum err = glewInit(); + if (GLEW_OK != err) { + gui_alert((char *)glewGetErrorString(err)); + } + + if (glewIsSupported("GL_EXT_framebuffer_object")) + framebufferobjects_supported = 1; + + glutDisplayFunc(GLUT_DISPLAY_REQUEST); + +# ifdef QB64_WINDOWS + glutTimerFunc(8, GLUT_TIMER_EVENT, 0); +# else + glutIdleFunc(GLUT_IDLEFUNC); +# endif + + glutKeyboardFunc(GLUT_KEYBOARD_FUNC); + glutKeyboardUpFunc(GLUT_KEYBOARDUP_FUNC); + glutSpecialFunc(GLUT_SPECIAL_FUNC); + glutSpecialUpFunc(GLUT_SPECIALUP_FUNC); + glutMouseFunc(GLUT_MOUSE_FUNC); + glutMotionFunc(GLUT_MOTION_FUNC); + glutPassiveMotionFunc(GLUT_PASSIVEMOTION_FUNC); + glutReshapeFunc(GLUT_RESHAPE_FUNC); + +# ifdef CORE_FREEGLUT + glutMouseWheelFunc(GLUT_MOUSEWHEEL_FUNC); +# endif +} + +static bool glut_is_started; +static struct completion glut_thread_starter; +static struct completion *glut_thread_initialized; + +void libqb_start_glut_thread() { + if (glut_is_started) + return; + + struct completion init; + completion_init(&init); + + glut_thread_initialized = &init; + + completion_finish(&glut_thread_starter); + + completion_wait(&init); + completion_clear(&init); +} + +// Checks whether the GLUT thread is running +bool libqb_is_glut_up() { + return glut_is_started; +} + +void libqb_glut_presetup(int argc, char **argv) { + if (!screen_hide) { + initialize_glut(argc, argv); // Initialize GLUT if the screen isn't hidden + glut_is_started = true; + } else { + completion_init(&glut_thread_starter); + } +} + +void libqb_start_main_thread(int argc, char **argv) { + + // Start the 'MAIN_LOOP' in a separate thread, as GLUT has to run on the + // initial thread. + struct libqb_thread *main_loop = libqb_thread_new(); + libqb_thread_start(main_loop, MAIN_LOOP, NULL); + + // This happens for $SCREENHIDE programs. This thread waits on the + // `glut_thread_starter` completion, which will get completed if a + // _ScreenShow is used. + if (!glut_is_started) { + completion_wait(&glut_thread_starter); + + initialize_glut(argc, argv); + glut_is_started = true; + + if (glut_thread_initialized) + completion_finish(glut_thread_initialized); + } + + glutMainLoop(); +} diff --git a/internal/c/libqb/src/mac-key-monitor.h b/internal/c/libqb/src/mac-key-monitor.h new file mode 100644 index 000000000..b6f20581b --- /dev/null +++ b/internal/c/libqb/src/mac-key-monitor.h @@ -0,0 +1,10 @@ +#ifndef INCLUDE_INTERNAL_MAC_KEY_MONITOR_H +#define INCLUDE_INTERNAL_MAC_KEY_MONITOR_H + +#ifdef QB64_MACOSX +void mac_register_key_handler(); +#else +static inline void mac_register_key_handler() { }; +#endif + +#endif diff --git a/internal/c/libqb/src/mac-key-monitor.mm b/internal/c/libqb/src/mac-key-monitor.mm new file mode 100644 index 000000000..0e6987ebb --- /dev/null +++ b/internal/c/libqb/src/mac-key-monitor.mm @@ -0,0 +1,85 @@ + +#include "libqb-common.h" + +#include +#include +#include +#include + +#include "Cocoa/Cocoa.h" +#include + +#include "keyhandler.h" +#include "mac-key-monitor.h" + +void mac_register_key_handler() +{ + [NSEvent addLocalMonitorForEventsMatchingMask:NSFlagsChangedMask + handler:^NSEvent *(NSEvent *event) { + // notes on bitfields: + // if ([event modifierFlags] == 131330) keydown_vk(VK+QBVK_LSHIFT);// 100000000100000010 + // if ([event modifierFlags] == 131332) keydown_vk(VK+QBVK_RSHIFT);// 100000000100000100 + // if ([event modifierFlags] == 262401) keydown_vk(VK+QBVK_LCTRL); //1000000000100000001 + // if ([event modifierFlags] == 270592) keydown_vk(VK+QBVK_RCTRL); //1000010000100000000 + // if ([event modifierFlags] == 524576) keydown_vk(VK+QBVK_LALT); //10000000000100100000 + // if ([event modifierFlags] == 524608) keydown_vk(VK+QBVK_RALT); //10000000000101000000 + // caps lock // 10000000100000000 + + int x = [event modifierFlags]; + + if (x & (1 << 0)) { + if (!keyheld(VK + QBVK_LCTRL)) + keydown_vk(VK + QBVK_LCTRL); + } else { + if (keyheld(VK + QBVK_LCTRL)) + keyup_vk(VK + QBVK_LCTRL); + } + if (x & (1 << 13)) { + if (!keyheld(VK + QBVK_RCTRL)) + keydown_vk(VK + QBVK_RCTRL); + } else { + if (keyheld(VK + QBVK_RCTRL)) + keyup_vk(VK + QBVK_RCTRL); + } + + if (x & (1 << 1)) { + if (!keyheld(VK + QBVK_LSHIFT)) + keydown_vk(VK + QBVK_LSHIFT); + } else { + if (keyheld(VK + QBVK_LSHIFT)) + keyup_vk(VK + QBVK_LSHIFT); + } + if (x & (1 << 2)) { + if (!keyheld(VK + QBVK_RSHIFT)) + keydown_vk(VK + QBVK_RSHIFT); + } else { + if (keyheld(VK + QBVK_RSHIFT)) + keyup_vk(VK + QBVK_RSHIFT); + } + + if (x & (1 << 5)) { + if (!keyheld(VK + QBVK_LALT)) + keydown_vk(VK + QBVK_LALT); + } else { + if (keyheld(VK + QBVK_LALT)) + keyup_vk(VK + QBVK_LALT); + } + if (x & (1 << 6)) { + if (!keyheld(VK + QBVK_RALT)) + keydown_vk(VK + QBVK_RALT); + } else { + if (keyheld(VK + QBVK_RALT)) + keyup_vk(VK + QBVK_RALT); + } + + if (x & (1 << 16)) { + if (!keyheld(VK + QBVK_CAPSLOCK)) + keydown_vk(VK + QBVK_CAPSLOCK); + } else { + if (keyheld(VK + QBVK_CAPSLOCK)) + keyup_vk(VK + QBVK_CAPSLOCK); + } + + return event; + }]; +} diff --git a/internal/c/parts/core/build.mk b/internal/c/parts/core/build.mk index 6a7cc2b05..6a8e9b765 100644 --- a/internal/c/parts/core/build.mk +++ b/internal/c/parts/core/build.mk @@ -12,5 +12,7 @@ $(FREEGLUT_LIB): $(FREEGLUT_OBJS) QB_CORE_LIB := $(FREEGLUT_LIB) +CXXFLAGS += -I$(PATH_INTERNAL_C)/parts/core/src/ -I$(PATH_INTERNAL_C)/parts/core/glew/include/ + CLEAN_LIST += $(FREEGLUT_LIB) $(FREEGLUT_OBJS) From 72193e34e57a4a74bf92356fea4bcead567e1f51 Mon Sep 17 00:00:00 2001 From: Matthew Kilgore Date: Fri, 25 Nov 2022 00:52:40 -0500 Subject: [PATCH 4/7] Add GLUT command queue, processed on GLUT thread This fixes all the code so that all the calls to glut functions happen on the same thread that is running GLUT. We achieve this by creating a queue of GLUT commands to execute. Commands can be added to the queue anywhere in the code, and then the queue is processed on the GLUT thread via it's idle func or timer func. The command is run and if necessary the result is provided in the message queue object. Each object contains a completion which can be waited on to block until the GLUT thread has processed the command. Fixes: #66 --- internal/c/libqb.cpp | 63 +++------ internal/c/libqb/build.mk | 3 + internal/c/libqb/include/glut-thread.h | 14 ++ .../c/libqb/src/console-only-main-thread.cpp | 30 +++++ internal/c/libqb/src/glut-message.cpp | 49 +++++++ internal/c/libqb/src/glut-message.h | 125 ++++++++++++++++++ internal/c/libqb/src/glut-msg-queue.cpp | 75 +++++++++++ 7 files changed, 318 insertions(+), 41 deletions(-) create mode 100644 internal/c/libqb/src/glut-message.cpp create mode 100644 internal/c/libqb/src/glut-message.h create mode 100644 internal/c/libqb/src/glut-msg-queue.cpp diff --git a/internal/c/libqb.cpp b/internal/c/libqb.cpp index 3c4ed2b22..e20f40a8f 100644 --- a/internal/c/libqb.cpp +++ b/internal/c/libqb.cpp @@ -269,7 +269,6 @@ int32 environment_2d__letterbox = 0; // 1=vertical black stripes required, 2 int32 window_focused = 0; // Not used on Windows uint8 *window_title = NULL; -int32 temp_window_title_set = 0; double max_fps = 60; // 60 is the default int32 auto_fps = 0; // set to 1 to make QB64 auto-adjust fps based on load @@ -501,14 +500,6 @@ int32 dont_call_sub_gl = 0; void GLUT_DISPLAY_REQUEST(); -void timerCB(int millisec) // not currently being used -{ -#ifdef QB64_GLUT - glutPostRedisplay(); - glutTimerFunc(millisec, timerCB, millisec); -#endif -} - struct display_frame_struct { int32 state; int64 order; @@ -23482,7 +23473,7 @@ void sub__mousehide() { #ifdef QB64_GUI # ifdef QB64_GLUT OPTIONAL_GLUT(); - glutSetCursor(GLUT_CURSOR_NONE); + libqb_glut_set_cursor(GLUT_CURSOR_NONE); # endif #endif } @@ -23554,7 +23545,7 @@ void sub__mouseshow(qbs *style, int32 passed) { } cursor_valid: - glutSetCursor(mouse_cursor_style); + libqb_glut_set_cursor(mouse_cursor_style); #endif } @@ -23636,7 +23627,7 @@ void sub__mousemove(float x, float y) { x2 += environment_2d__screen_x1; y2 += environment_2d__screen_y1; - glutWarpPointer(x2, y2); + libqb_glut_warp_pointer(x2, y2); return; error: @@ -27216,7 +27207,7 @@ int32 func_screenwidth() { #else # ifdef QB64_GLUT OPTIONAL_GLUT(0); - return glutGet(GLUT_SCREEN_WIDTH); + return libqb_glut_get(GLUT_SCREEN_WIDTH); # else return 0; # endif @@ -27229,7 +27220,7 @@ int32 func_screenheight() { #else # ifdef QB64_GLUT OPTIONAL_GLUT(0); - return glutGet(GLUT_SCREEN_HEIGHT); + return libqb_glut_get(GLUT_SCREEN_HEIGHT); # else return 0; # endif @@ -27239,7 +27230,7 @@ int32 func_screenheight() { void sub_screenicon() { #ifdef QB64_GLUT NEEDS_GLUT(); - glutIconifyWindow(); + libqb_glut_iconify_window(); #endif } @@ -32649,10 +32640,10 @@ qbs *func__os() { int32 func__screenx() { #if defined(QB64_GUI) && defined(QB64_WINDOWS) && defined(QB64_GLUT) NEEDS_GLUT(0); - return glutGet(GLUT_WINDOW_X) - glutGet(GLUT_WINDOW_BORDER_WIDTH); + return libqb_glut_get(GLUT_WINDOW_X) - libqb_glut_get(GLUT_WINDOW_BORDER_WIDTH); #elif defined(QB64_GUI) && defined(QB64_MACOSX) && defined(QB64_GLUT) NEEDS_GLUT(0); - return glutGet(GLUT_WINDOW_X); + return libqb_glut_get(GLUT_WINDOW_X); #endif return 0; // if not windows then return 0 } @@ -32660,10 +32651,10 @@ int32 func__screenx() { int32 func__screeny() { #if defined(QB64_GUI) && defined(QB64_WINDOWS) && defined(QB64_GLUT) NEEDS_GLUT(0); - return glutGet(GLUT_WINDOW_Y) - glutGet(GLUT_WINDOW_BORDER_WIDTH) - glutGet(GLUT_WINDOW_HEADER_HEIGHT); + return libqb_glut_get(GLUT_WINDOW_Y) - libqb_glut_get(GLUT_WINDOW_BORDER_WIDTH) - libqb_glut_get(GLUT_WINDOW_HEADER_HEIGHT); #elif defined(QB64_GUI) && defined(QB64_MACOSX) && defined(QB64_GLUT) NEEDS_GLUT(0); - return glutGet(GLUT_WINDOW_Y); + return libqb_glut_get(GLUT_WINDOW_Y); #endif return 0; // if not windows then return 0 } @@ -32682,18 +32673,18 @@ void sub__screenmove(int32 x, int32 y, int32 passed) { NEEDS_GLUT(); if (passed == 2) { - glutPositionWindow(x, y); + libqb_glut_position_window(x, y); } else { int32 SW = -1, SH, WW, WH; while (SW == -1) { - SW = glutGet(GLUT_SCREEN_WIDTH); + SW = libqb_glut_get(GLUT_SCREEN_WIDTH); } - SH = glutGet(GLUT_SCREEN_HEIGHT); - WW = glutGet(GLUT_WINDOW_WIDTH); - WH = glutGet(GLUT_WINDOW_HEIGHT); + SH = libqb_glut_get(GLUT_SCREEN_HEIGHT); + WW = libqb_glut_get(GLUT_WINDOW_WIDTH); + WH = libqb_glut_get(GLUT_WINDOW_HEIGHT); x = (SW - WW) / 2; y = (SH - WH) / 2; - glutPositionWindow(x, y); + libqb_glut_position_window(x, y); } #endif @@ -33985,7 +33976,7 @@ void sub__screenshow() { screen_hide = 0; // $SCREENHIDE programs will not have the window running libqb_start_glut_thread(); - glutShowWindow(); + libqb_glut_show_window(); #endif } @@ -33997,7 +33988,7 @@ void sub__screenhide() { // This is probably unnecessary, no conditions allow for screen_hide==0 // without GLUT running, but it doesn't hurt anything. libqb_start_glut_thread(); - glutHideWindow(); + libqb_glut_hide_window(); #endif screen_hide = 1; @@ -34657,6 +34648,8 @@ void GLUT_SPECIALUP_FUNC(int key, int x, int y) { GLUT_key_special(key, 0); } #ifdef QB64_WINDOWS void GLUT_TIMER_EVENT(int ignore) { + libqb_process_glut_queue(); + # ifdef QB64_GLUT glutPostRedisplay(); int32 msdelay = 1000.0 / max_fps; @@ -34669,6 +34662,7 @@ void GLUT_TIMER_EVENT(int ignore) { } #else void GLUT_IDLEFUNC() { + libqb_process_glut_queue(); # ifdef QB64_MACOSX # ifdef DEPENDENCY_DEVICEINPUT @@ -35910,13 +35904,6 @@ void GLUT_DISPLAY_REQUEST() { } in_GLUT_DISPLAY_REQUEST = 1; -# ifdef QB64_MACOSX - if (temp_window_title_set == 1) { - glutSetWindowTitle((char *)window_title); - temp_window_title_set = 0; - } -# endif - // general use variables static int32 i, i2, i3; static int32 x, y, x2, y2; @@ -36726,15 +36713,9 @@ void sub__title(qbs *title) { if (old_buf) free(old_buf); -#ifdef QB64_GLUT -# ifdef QB64_MACOSX - temp_window_title_set = 1; -# else OPTIONAL_GLUT(); - glutSetWindowTitle((char *)window_title); -# endif -#endif + libqb_glut_set_window_title((char *)window_title); } // title void sub__echo(qbs *message) { diff --git a/internal/c/libqb/build.mk b/internal/c/libqb/build.mk index bde5b3834..9ffba313b 100644 --- a/internal/c/libqb/build.mk +++ b/internal/c/libqb/build.mk @@ -9,6 +9,9 @@ libqb-objs-y$(DEP_HTTP) += $(PATH_LIBQB)/src/http-stub.o libqb-objs-y += $(PATH_LIBQB)/src/threading-$(PLATFORM).o libqb-objs-y$(DEP_CONSOLE_ONLY) += $(PATH_LIBQB)/src/glut-main-thread.o +libqb-objs-y$(DEP_CONSOLE_ONLY) += $(PATH_LIBQB)/src/glut-message.o +libqb-objs-y$(DEP_CONSOLE_ONLY) += $(PATH_LIBQB)/src/glut-msg-queue.o + libqb-objs-$(DEP_CONSOLE_ONLY) += $(PATH_LIBQB)/src/console-only-main-thread.o ifeq ($(OS),osx) diff --git a/internal/c/libqb/include/glut-thread.h b/internal/c/libqb/include/glut-thread.h index d87397e9e..9d08905a7 100644 --- a/internal/c/libqb/include/glut-thread.h +++ b/internal/c/libqb/include/glut-thread.h @@ -15,6 +15,20 @@ void libqb_start_glut_thread(); // do any GLUT-related stuff bool libqb_is_glut_up(); +// Called at consistent intervals from a GLUT callback +void libqb_process_glut_queue(); + +// These functions perform the same actions as their coresponding glut* functions. +// They tell the GLUT thread to perform the command, returning the result if applicable +void libqb_glut_set_cursor(int style); +void libqb_glut_warp_pointer(int x, int y); +int libqb_glut_get(int id); +void libqb_glut_iconify_window(); +void libqb_glut_position_window(int x, int y); +void libqb_glut_show_window(); +void libqb_glut_hide_window(); +void libqb_glut_set_window_title(const char *title); + // Convinence macros, exists a function depending on the state of GLUT #define NEEDS_GLUT(error_result) do { \ if (!libqb_is_glut_up()) { \ diff --git a/internal/c/libqb/src/console-only-main-thread.cpp b/internal/c/libqb/src/console-only-main-thread.cpp index cb3a401f1..e25d445f6 100644 --- a/internal/c/libqb/src/console-only-main-thread.cpp +++ b/internal/c/libqb/src/console-only-main-thread.cpp @@ -31,3 +31,33 @@ void libqb_start_glut_thread() { bool libqb_is_glut_up() { return false; } + +void libqb_process_glut_queue() { +} + +void libqb_glut_set_cursor(int style) { + +} + +void libqb_glut_warp_pointer(int x, int y) +{ +} + +int libqb_glut_get(int id) { + return 0; +} + +void libqb_glut_iconify_window() { +} + +void libqb_glut_position_window(int x, int y) { +} + +void libqb_glut_show_window() { +} + +void libqb_glut_hide_window() { +} + +void libqb_glut_set_window_title(const char *title) { +} diff --git a/internal/c/libqb/src/glut-message.cpp b/internal/c/libqb/src/glut-message.cpp new file mode 100644 index 000000000..128068f2a --- /dev/null +++ b/internal/c/libqb/src/glut-message.cpp @@ -0,0 +1,49 @@ + +#include "libqb-common.h" + +#include +#include +#include +#include + +// note: MacOSX uses Apple's GLUT not FreeGLUT +#ifdef QB64_MACOSX +# include +#else +# define CORE_FREEGLUT +# include "freeglut.h" +#endif + +#include "glut-message.h" + +void glut_message_set_cursor::execute() { + glutSetCursor(style); +} + +void glut_message_warp_pointer::execute() { + glutWarpPointer(x, y); +} + +void glut_message_get::execute() { + response_value = glutGet(id); +} + +void glut_message_iconify_window::execute() { + glutIconifyWindow(); +} + +void glut_message_position_window::execute() { + glutPositionWindow(x, y); +} + +void glut_message_show_window::execute() { + glutShowWindow(); +} + +void glut_message_hide_window::execute() { + glutHideWindow(); +} + +void glut_message_set_window_title::execute() { + glutSetWindowTitle(newTitle); +} diff --git a/internal/c/libqb/src/glut-message.h b/internal/c/libqb/src/glut-message.h new file mode 100644 index 000000000..1f58d1a3a --- /dev/null +++ b/internal/c/libqb/src/glut-message.h @@ -0,0 +1,125 @@ +#ifndef INCLUDE_LIBQB_GLUT_MESSAGE_H +#define INCLUDE_LIBQB_GLUT_MESSAGE_H + +#include +#include +#include +#include "completion.h" + +class glut_message { + private: + completion *finished = NULL; + + void initCompletion() { + finished = new completion(); + completion_init(finished); + } + + protected: + glut_message(bool withCompletion) { + if (withCompletion) + initCompletion(); + } + + public: + // Calling this indicates to the creator of the message that it has been + // completed, and any response data is availiable to be read. + // + // If `finsihed` is NULL that means nobody is waiting for the response. In + // that situation we're free to simply delete the object. + void finish() { + if (finished) + completion_finish(finished); + else + delete this; + } + + void wait_for_response() { + completion_wait(finished); + } + + virtual ~glut_message() { + if (finished) { + completion_wait(finished); // Should be a NOP, but better to check anyway + completion_clear(finished); + + delete finished; + } + } + + virtual void execute() = 0; +}; + +class glut_message_set_cursor : public glut_message { + public: + int style; + void execute(); + + glut_message_set_cursor(int _style) : glut_message(false), style(_style) { } +}; + +class glut_message_warp_pointer : public glut_message { + public: + int x, y; + void execute(); + + glut_message_warp_pointer(int _x, int _y) : glut_message(false), x(_x), y(_y) { } +}; + +class glut_message_get : public glut_message { + public: + int id; + int response_value; + void execute(); + + glut_message_get(int _id) : glut_message(true), id(_id), response_value(0) { } +}; + +class glut_message_iconify_window : public glut_message { + public: + void execute(); + + glut_message_iconify_window() : glut_message(false) { } +}; + +class glut_message_position_window : public glut_message { + public: + int x, y; + void execute(); + + glut_message_position_window(int _x, int _y) : glut_message(false), x(_x), y(_y) { } +}; + +class glut_message_show_window : public glut_message { + public: + void execute(); + + glut_message_show_window() : glut_message(false) { } +}; + +class glut_message_hide_window : public glut_message { + public: + void execute(); + + glut_message_hide_window() : glut_message(false) { } +}; + +class glut_message_set_window_title : public glut_message { + public: + char *newTitle; + void execute(); + + glut_message_set_window_title(const char *title) : glut_message(false) { + newTitle = strdup(title); + } + + virtual ~glut_message_set_window_title() { + free(newTitle); + } +}; + +// Queues a glut_message to be processed. Returns false if the message was not +// queued. +bool libqb_queue_glut_message(glut_message *msg); + +#endif diff --git a/internal/c/libqb/src/glut-msg-queue.cpp b/internal/c/libqb/src/glut-msg-queue.cpp new file mode 100644 index 000000000..9fcb9cf87 --- /dev/null +++ b/internal/c/libqb/src/glut-msg-queue.cpp @@ -0,0 +1,75 @@ + +#include "libqb-common.h" + +#include +#include + +#include "mutex.h" +#include "glut-message.h" +#include "glut-thread.h" + +static libqb_mutex *glut_msg_queue_lock = libqb_mutex_new(); +static std::queue glut_msg_queue; + +bool libqb_queue_glut_message(glut_message *msg) { + if (!libqb_is_glut_up()) { + msg->finish(); + return false; + } + + libqb_mutex_guard guard(glut_msg_queue_lock); + + glut_msg_queue.push(msg); + + return true; +} + +void libqb_process_glut_queue() { + libqb_mutex_guard guard(glut_msg_queue_lock); + + while (!glut_msg_queue.empty()) { + glut_message *msg = glut_msg_queue.front(); + glut_msg_queue.pop(); + + msg->execute(); + + msg->finish(); + } +} + +void libqb_glut_set_cursor(int style) { + libqb_queue_glut_message(new glut_message_set_cursor(style)); +} + +void libqb_glut_warp_pointer(int x, int y) { + libqb_queue_glut_message(new glut_message_warp_pointer(x, y)); +} + +int libqb_glut_get(int id) { + glut_message_get msg(id); + + libqb_queue_glut_message(&msg); + msg.wait_for_response(); + + return msg.response_value; +} + +void libqb_glut_iconify_window() { + libqb_queue_glut_message(new glut_message_iconify_window()); +} + +void libqb_glut_position_window(int x, int y) { + libqb_queue_glut_message(new glut_message_position_window(x, y)); +} + +void libqb_glut_show_window() { + libqb_queue_glut_message(new glut_message_show_window()); +} + +void libqb_glut_hide_window() { + libqb_queue_glut_message(new glut_message_hide_window()); +} + +void libqb_glut_set_window_title(const char *title) { + libqb_queue_glut_message(new glut_message_set_window_title(title)); +} From 6c288ecb6ff2a79657d90ee62421eca00bceaf03 Mon Sep 17 00:00:00 2001 From: Matthew Kilgore Date: Fri, 25 Nov 2022 01:00:20 -0500 Subject: [PATCH 5/7] Don't compile libqb.cpp as Objective-C on Mac OS With the recent changes to libqb.cpp to pull out some of the GLUT logic, the only actual Objective-C in libqb.cpp was pulled out. That being the case, it's no longer necessary to have libqb.mm for compiling libqb.cpp, so we're removing it to simplify the compliation logic a bit. --- Makefile | 5 ----- internal/c/libqb.cpp | 6 ------ internal/c/libqb.mm | 2 -- 3 files changed, 13 deletions(-) delete mode 100644 internal/c/libqb.mm diff --git a/Makefile b/Makefile index c9b02292b..f47b3b665 100644 --- a/Makefile +++ b/Makefile @@ -401,13 +401,8 @@ endif QBLIB := $(PATH_INTERNAL_C)/$(QBLIB_NAME).o -ifneq ($(OS),osx) $(QBLIB): $(PATH_INTERNAL_C)/libqb.cpp $(CXX) $(CXXFLAGS) $< -c -o $@ -else -$(QBLIB): $(PATH_INTERNAL_C)/libqb.mm - $(CXX) $(CXXFLAGS) $< -c -o $@ -endif ifeq ($(OS),win) CLEAN_LIST += $(ICON_OBJ) diff --git a/internal/c/libqb.cpp b/internal/c/libqb.cpp index e20f40a8f..0effb84f2 100644 --- a/internal/c/libqb.cpp +++ b/internal/c/libqb.cpp @@ -13,13 +13,7 @@ #ifdef QB64_MACOSX # include -# include "Cocoa/Cocoa.h" -# include -# include -# include - # include //required for _NSGetExecutablePath - #endif #include "mutex.h" diff --git a/internal/c/libqb.mm b/internal/c/libqb.mm deleted file mode 100644 index 59eae3f8c..000000000 --- a/internal/c/libqb.mm +++ /dev/null @@ -1,2 +0,0 @@ -#include "libqb.cpp" - From f7fabda198d23e7e44931aad7f4b2c0913eb02c9 Mon Sep 17 00:00:00 2001 From: Matthew Kilgore Date: Mon, 28 Nov 2022 02:13:19 -0500 Subject: [PATCH 6/7] Fix random seg faults on exit Fairly straightfowrad, programs were randomly seg faulting on exit. This was happening due to GLUT registering a cleanup function via atexit(), which then gets called when exit() is called. The issue happens when exit() is called on a thread other than the GLUT thread, which leads to the exit() call then attempting to cleanup GLUT while the other thread is still using it, which randomly leads to seg faults. Fixing this is slightly annoying. We cannot stop the GLUT thread, as the basic GLUT API (which is used on Mac OS) simply does not offer a way to exit the glutMainLoop() call. Thus the simplest solution is to simply make sure we call exit() on the GLUT thread, which we can fairly easily due via the message queue. That being the case, a new libqb_exit() API was added, which simply regsiters the GLUT exit message and then waits for the program to end. The atexit() handler then runs on the GLUT thread and everything works out fine. In the future we probably should redo the exit logic a bit so that all the threads are actually stopped/joined to ensure the exit process is consistent, however this is good enough for now. Also, there's plenty of error states which call exit() which I did not address. --- internal/c/libqb.cpp | 2 +- internal/c/libqb/include/glut-thread.h | 5 +++++ internal/c/libqb/src/console-only-main-thread.cpp | 9 +++++++++ internal/c/libqb/src/glut-main-thread.cpp | 15 +++++++++++++++ internal/c/libqb/src/glut-message.cpp | 5 +++++ internal/c/libqb/src/glut-message.h | 8 ++++++++ internal/c/libqb/src/glut-msg-queue.cpp | 10 ++++++++++ 7 files changed, 53 insertions(+), 1 deletion(-) diff --git a/internal/c/libqb.cpp b/internal/c/libqb.cpp index 0effb84f2..64d027868 100644 --- a/internal/c/libqb.cpp +++ b/internal/c/libqb.cpp @@ -37686,7 +37686,7 @@ end_program: snd_un_init(); - exit(exit_code); + libqb_exit(exit_code); } // used to preserve the previous frame's content for comparison/reuse purposes diff --git a/internal/c/libqb/include/glut-thread.h b/internal/c/libqb/include/glut-thread.h index 9d08905a7..0578e9efd 100644 --- a/internal/c/libqb/include/glut-thread.h +++ b/internal/c/libqb/include/glut-thread.h @@ -18,6 +18,10 @@ bool libqb_is_glut_up(); // Called at consistent intervals from a GLUT callback void libqb_process_glut_queue(); +// Called to properly exit the program. Necessary because GLUT requires a +// special care to not seg-fault when exiting the program. +void libqb_exit(int); + // These functions perform the same actions as their coresponding glut* functions. // They tell the GLUT thread to perform the command, returning the result if applicable void libqb_glut_set_cursor(int style); @@ -28,6 +32,7 @@ void libqb_glut_position_window(int x, int y); void libqb_glut_show_window(); void libqb_glut_hide_window(); void libqb_glut_set_window_title(const char *title); +void libqb_glut_exit_program(int exitcode); // Convinence macros, exists a function depending on the state of GLUT #define NEEDS_GLUT(error_result) do { \ diff --git a/internal/c/libqb/src/console-only-main-thread.cpp b/internal/c/libqb/src/console-only-main-thread.cpp index e25d445f6..fa737f556 100644 --- a/internal/c/libqb/src/console-only-main-thread.cpp +++ b/internal/c/libqb/src/console-only-main-thread.cpp @@ -61,3 +61,12 @@ void libqb_glut_hide_window() { void libqb_glut_set_window_title(const char *title) { } + +void libqb_glut_exit_program(int exitcode) { + libqb_exit(exitcode); +} + +// Since there's no GLUT thread to deal with we can just exit() like normal +void libqb_exit(int code) { + exit(code); +} diff --git a/internal/c/libqb/src/glut-main-thread.cpp b/internal/c/libqb/src/glut-main-thread.cpp index 819a5a233..bd776cc93 100644 --- a/internal/c/libqb/src/glut-main-thread.cpp +++ b/internal/c/libqb/src/glut-main-thread.cpp @@ -171,3 +171,18 @@ void libqb_start_main_thread(int argc, char **argv) { glutMainLoop(); } + +// Due to GLUT making use of cleanup via atexit, we have to call exit() from +// the same thread handling the GLUT logic so that the atexit handler also runs +// from that thread (not doing that can result in a segfault due to using GLUT +// from two threads at the same time). +// +// This is acomplished by simply queuing a GLUT message that calls exit() for us. +void libqb_exit(int exitcode) +{ + // If GLUT isn't running then we're free to do the exit() call from here + if (!libqb_is_glut_up()) + exit(exitcode); + + libqb_glut_exit_program(exitcode); +} diff --git a/internal/c/libqb/src/glut-message.cpp b/internal/c/libqb/src/glut-message.cpp index 128068f2a..1bfc115c2 100644 --- a/internal/c/libqb/src/glut-message.cpp +++ b/internal/c/libqb/src/glut-message.cpp @@ -47,3 +47,8 @@ void glut_message_hide_window::execute() { void glut_message_set_window_title::execute() { glutSetWindowTitle(newTitle); } + +void glut_message_exit_program::execute() { + exit(exitCode); +} + diff --git a/internal/c/libqb/src/glut-message.h b/internal/c/libqb/src/glut-message.h index 1f58d1a3a..c3c556740 100644 --- a/internal/c/libqb/src/glut-message.h +++ b/internal/c/libqb/src/glut-message.h @@ -118,6 +118,14 @@ class glut_message_set_window_title : public glut_message { } }; +class glut_message_exit_program : public glut_message { + public: + int exitCode; + void execute(); + + glut_message_exit_program(int _exitCode) : glut_message(true), exitCode(_exitCode) { } +}; + // Queues a glut_message to be processed. Returns false if the message was not // queued. bool libqb_queue_glut_message(glut_message *msg); diff --git a/internal/c/libqb/src/glut-msg-queue.cpp b/internal/c/libqb/src/glut-msg-queue.cpp index 9fcb9cf87..a22641e9e 100644 --- a/internal/c/libqb/src/glut-msg-queue.cpp +++ b/internal/c/libqb/src/glut-msg-queue.cpp @@ -73,3 +73,13 @@ void libqb_glut_hide_window() { void libqb_glut_set_window_title(const char *title) { libqb_queue_glut_message(new glut_message_set_window_title(title)); } + +void libqb_glut_exit_program(int exitcode) { + glut_message_exit_program msg(exitcode); + + libqb_queue_glut_message(&msg); + msg.wait_for_response(); + + // Should never return + exit(exitcode); +} From 7ac2eefcb8edac0316844a2472c830f59b494355 Mon Sep 17 00:00:00 2001 From: Matthew Kilgore Date: Wed, 30 Nov 2022 17:23:47 -0500 Subject: [PATCH 7/7] Fix slowdown of _ScreenX and _ScreenY The commands _ScreenX and _ScreenY got significantly slower due to the need to wait for the GLUT thread to wake up and execute the glutGet() command for them. We've already seen a few programs (including the IDE) where this behavior completely grinds the program to a halt, so we definitely can't keep it. The simple solution here is to not call glutGet() on every _ScreenX/Y command. Instead every time the idle/timer function runs we get the current values for the relevant glutGet() variables and store them. libqb_glut_get() then checks if the value being read is one of the ones we read in the idle/timer functionand if so just returns the last read value. By doing it this way the commands no longer has to wait on the GLUT thread for the result. --- internal/c/libqb/src/glut-msg-queue.cpp | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/internal/c/libqb/src/glut-msg-queue.cpp b/internal/c/libqb/src/glut-msg-queue.cpp index a22641e9e..8e5fe4291 100644 --- a/internal/c/libqb/src/glut-msg-queue.cpp +++ b/internal/c/libqb/src/glut-msg-queue.cpp @@ -4,6 +4,14 @@ #include #include +// note: MacOSX uses Apple's GLUT not FreeGLUT +#ifdef QB64_MACOSX +# include +#else +# define CORE_FREEGLUT +# include "freeglut.h" +#endif + #include "mutex.h" #include "glut-message.h" #include "glut-thread.h" @@ -11,6 +19,15 @@ static libqb_mutex *glut_msg_queue_lock = libqb_mutex_new(); static std::queue glut_msg_queue; +// These values from GLUT are read on every process of the msg queue. Calls to +// libqb_glut_get() can then read from these values directly rather than wait +// for the GLUT thread to process the command. +static int glut_window_x, glut_window_y; + +#ifdef CORE_FREEGLUT +static int glut_window_border_width, glut_window_header_height; +#endif + bool libqb_queue_glut_message(glut_message *msg) { if (!libqb_is_glut_up()) { msg->finish(); @@ -27,6 +44,14 @@ bool libqb_queue_glut_message(glut_message *msg) { void libqb_process_glut_queue() { libqb_mutex_guard guard(glut_msg_queue_lock); + glut_window_x = glutGet(GLUT_WINDOW_X); + glut_window_y = glutGet(GLUT_WINDOW_Y); + +#ifdef CORE_FREEGLUT + glut_window_border_width = glutGet(GLUT_WINDOW_BORDER_WIDTH); + glut_window_header_height = glutGet(GLUT_WINDOW_HEADER_HEIGHT); +#endif + while (!glut_msg_queue.empty()) { glut_message *msg = glut_msg_queue.front(); glut_msg_queue.pop(); @@ -45,7 +70,34 @@ void libqb_glut_warp_pointer(int x, int y) { libqb_queue_glut_message(new glut_message_warp_pointer(x, y)); } +static bool is_static_glut_value(int id) { + return id == GLUT_WINDOW_Y + || id == GLUT_WINDOW_X +#ifdef CORE_FREEGLUT + || id == GLUT_WINDOW_BORDER_WIDTH + || id == GLUT_WINDOW_HEADER_HEIGHT +#endif + ; +} + +static int __get_static_glut_value(int id) { + switch (id) { + case GLUT_WINDOW_Y: return glut_window_y; + case GLUT_WINDOW_X: return glut_window_x; +#ifdef CORE_FREEGLUT + case GLUT_WINDOW_BORDER_WIDTH: return glut_window_border_width; + case GLUT_WINDOW_HEADER_HEIGHT: return glut_window_header_height; +#endif + default: return -1; + } +} + int libqb_glut_get(int id) { + if (is_static_glut_value(id)) { + libqb_mutex_guard guard(glut_msg_queue_lock); + return __get_static_glut_value(id); + } + glut_message_get msg(id); libqb_queue_glut_message(&msg);