From 5b7e11a8731068fccf481626c8bb40ee44ee183c Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Fri, 11 Sep 2026 19:47:40 +1000 Subject: [PATCH 1/2] Add GL smoke tests with CI headless fixes - Add tests/gl/glRenderSmokeTest (desktop GLFW) with shared GLSmokeTestCore.h exercising indexed VBO, instanced draw, tessellated ofPath, FBO readback - Add tests/emscripten/glRenderSmokeTest and tests/ios/glRenderSmokeTest (symlinked core) - Fix GLES CI: keep CC as C under ccache (was CXX), skip foreign tests (android/ios/emscripten/tvOS) on desktop runners, run linux tests under xvfb when headless, skip GLFW smoke test if window never came up --- .../project/makefileCommon/compile.core.mk | 2 +- .../project/makefileCommon/config.project.mk | 2 +- scripts/ci/linux64/install.sh | 1 + scripts/ci/linux64/run_tests.sh | 12 +- scripts/ci/macos/run_tests.sh | 3 + scripts/ci/msys2/run_tests.sh | 3 + scripts/ci/osx/run_tests.sh | 5 +- .../glRenderSmokeTest/bin/data/.gitkeep | 0 .../glRenderSmokeTest/src/GLSmokeTestCore.h | 1 + .../emscripten/glRenderSmokeTest/src/main.cpp | 1 + .../glRenderSmokeTest/src/ofApp.cpp | 1 + .../emscripten/glRenderSmokeTest/src/ofApp.h | 1 + tests/gl/glRenderSmokeTest/README.md | 27 +++ tests/gl/glRenderSmokeTest/bin/data/.gitkeep | 0 .../glRenderSmokeTest/src/GLSmokeTestCore.h | 173 ++++++++++++++++++ tests/gl/glRenderSmokeTest/src/main.cpp | 23 +++ tests/gl/glRenderSmokeTest/src/ofApp.cpp | 42 +++++ tests/gl/glRenderSmokeTest/src/ofApp.h | 13 ++ tests/ios/glRenderSmokeTest/addons.make | 0 tests/ios/glRenderSmokeTest/bin/data/.gitkeep | 0 .../AppIcon.appiconset/Contents.json | 14 ++ .../AppIcon.appiconset/of_ios.png | Bin 0 -> 50843 bytes .../mediaAssets/Images.xcassets/Contents.json | 6 + .../Logo.symbolset/Contents.json | 12 ++ .../Images.xcassets/Logo.symbolset/of.svg | 18 ++ .../mediaAssets/LaunchScreen.storyboard | 50 +++++ .../glRenderSmokeTest/src/GLSmokeTestCore.h | 1 + tests/ios/glRenderSmokeTest/src/main.mm | 19 ++ tests/ios/glRenderSmokeTest/src/ofApp.h | 27 +++ tests/ios/glRenderSmokeTest/src/ofApp.mm | 82 +++++++++ 30 files changed, 534 insertions(+), 5 deletions(-) create mode 100644 tests/emscripten/glRenderSmokeTest/bin/data/.gitkeep create mode 120000 tests/emscripten/glRenderSmokeTest/src/GLSmokeTestCore.h create mode 120000 tests/emscripten/glRenderSmokeTest/src/main.cpp create mode 120000 tests/emscripten/glRenderSmokeTest/src/ofApp.cpp create mode 120000 tests/emscripten/glRenderSmokeTest/src/ofApp.h create mode 100644 tests/gl/glRenderSmokeTest/README.md create mode 100644 tests/gl/glRenderSmokeTest/bin/data/.gitkeep create mode 100644 tests/gl/glRenderSmokeTest/src/GLSmokeTestCore.h create mode 100644 tests/gl/glRenderSmokeTest/src/main.cpp create mode 100644 tests/gl/glRenderSmokeTest/src/ofApp.cpp create mode 100644 tests/gl/glRenderSmokeTest/src/ofApp.h create mode 100644 tests/ios/glRenderSmokeTest/addons.make create mode 100644 tests/ios/glRenderSmokeTest/bin/data/.gitkeep create mode 100644 tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/AppIcon.appiconset/of_ios.png create mode 100644 tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Contents.json create mode 100644 tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Logo.symbolset/Contents.json create mode 100644 tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Logo.symbolset/of.svg create mode 100644 tests/ios/glRenderSmokeTest/mediaAssets/LaunchScreen.storyboard create mode 120000 tests/ios/glRenderSmokeTest/src/GLSmokeTestCore.h create mode 100644 tests/ios/glRenderSmokeTest/src/main.mm create mode 100644 tests/ios/glRenderSmokeTest/src/ofApp.h create mode 100644 tests/ios/glRenderSmokeTest/src/ofApp.mm diff --git a/libs/openFrameworksCompiled/project/makefileCommon/compile.core.mk b/libs/openFrameworksCompiled/project/makefileCommon/compile.core.mk index 78a80027e60..4224dd4bae9 100644 --- a/libs/openFrameworksCompiled/project/makefileCommon/compile.core.mk +++ b/libs/openFrameworksCompiled/project/makefileCommon/compile.core.mk @@ -48,7 +48,7 @@ endif ifdef ${ccache} $(info 💿 Using CCACHE - compile.core.mk ) CXX := ${ccache} $(CXX) - CC := ${ccache} $(CXX) + CC := ${ccache} $(CC) endif $(info 💾 CXX = $(CXX)) diff --git a/libs/openFrameworksCompiled/project/makefileCommon/config.project.mk b/libs/openFrameworksCompiled/project/makefileCommon/config.project.mk index 094ad477364..4062048e0c3 100644 --- a/libs/openFrameworksCompiled/project/makefileCommon/config.project.mk +++ b/libs/openFrameworksCompiled/project/makefileCommon/config.project.mk @@ -423,7 +423,7 @@ endif ifdef ${ccache} $(info 💿 Using CCACHE -- config.project.mk ) CXX := ${ccache} $(CXX) - CC := ${ccache} $(CXX) + CC := ${ccache} $(CC) endif ifdef PROJECT_RESOURCE_COMPILER diff --git a/scripts/ci/linux64/install.sh b/scripts/ci/linux64/install.sh index fae3e3a30e8..2ae068325eb 100755 --- a/scripts/ci/linux64/install.sh +++ b/scripts/ci/linux64/install.sh @@ -19,6 +19,7 @@ echo "checking assimp versions" apt-cache policy libassimp-dev sudo $OF_ROOT/scripts/linux/ubuntu/install_dependencies.sh -y; +sudo apt-get install -y xvfb || true #commenting out below so we can get it working with default in Ubuntu 18.04 #sudo add-apt-repository -y ppa:dns/gnu diff --git a/scripts/ci/linux64/run_tests.sh b/scripts/ci/linux64/run_tests.sh index 2f247e16e1e..f9d60c9dd6f 100755 --- a/scripts/ci/linux64/run_tests.sh +++ b/scripts/ci/linux64/run_tests.sh @@ -9,6 +9,10 @@ ROOT=${TRAVIS_BUILD_DIR:-"$( cd "$(dirname "$0")/../../.." ; pwd -P )"} echo "##[group]**** Running unit tests ****" cd $ROOT/tests for group in *; do + # android / ios / emscripten have their own CI jobs; native linux Makefile can't run them + case "$group" in + android|ios|tvOS|emscripten) continue ;; + esac if [ -d $group ]; then echo "##[group] $group" for test in $group/*; do @@ -23,8 +27,12 @@ for group in *; do binname=$(basename ${test}) if [[ -f ./${binname}_debug ]]; then - gdb -batch -ex "run" -ex "bt" -ex "q \$_exitcode" ./${binname}_debug - #./${binname}_debug + # GitHub linux runners have no DISPLAY; xvfb gives GLFW a software GL context + if [ -z "${DISPLAY:-}" ] && command -v xvfb-run >/dev/null 2>&1; then + xvfb-run -a gdb -batch -ex "run" -ex "bt" -ex "q \$_exitcode" ./${binname}_debug + else + gdb -batch -ex "run" -ex "bt" -ex "q \$_exitcode" ./${binname}_debug + fi else echo "Binary not found: ${binname}_debug" exit 1 diff --git a/scripts/ci/macos/run_tests.sh b/scripts/ci/macos/run_tests.sh index 3fe804c1a79..2add698c4a6 100755 --- a/scripts/ci/macos/run_tests.sh +++ b/scripts/ci/macos/run_tests.sh @@ -22,6 +22,9 @@ make -j Debug echo "**** Running unit tests ****" cd $ROOT/tests for group in *; do + case "$group" in + android|ios|tvOS|emscripten) continue ;; + esac if [ -d $group ]; then for test in $group/*; do if [ -d $test ]; then diff --git a/scripts/ci/msys2/run_tests.sh b/scripts/ci/msys2/run_tests.sh index 49be1664722..79c10087eba 100755 --- a/scripts/ci/msys2/run_tests.sh +++ b/scripts/ci/msys2/run_tests.sh @@ -6,6 +6,9 @@ ROOT=$(pwd -P) echo "##[group]**** Running unit tests ****" cd $ROOT/tests for group in *; do + case "$group" in + android|ios|tvOS|emscripten) continue ;; + esac if [ -d $group ]; then echo "##[group] $group" for test in $group/*; do diff --git a/scripts/ci/osx/run_tests.sh b/scripts/ci/osx/run_tests.sh index 43908338bc2..a1be4fa55cf 100755 --- a/scripts/ci/osx/run_tests.sh +++ b/scripts/ci/osx/run_tests.sh @@ -23,6 +23,9 @@ echo "##[endgroup]" echo "##[group]**** Running unit tests ****" cd $ROOT/tests for group in *; do + case "$group" in + android|ios|tvOS|emscripten) continue ;; + esac if [ -d $group ]; then echo "##[group] $group" for test in $group/*; do @@ -42,4 +45,4 @@ for group in *; do echo "##[endgroup]" fi done -echo "##[endgroup]" \ No newline at end of file +echo "##[endgroup]" diff --git a/tests/emscripten/glRenderSmokeTest/bin/data/.gitkeep b/tests/emscripten/glRenderSmokeTest/bin/data/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/emscripten/glRenderSmokeTest/src/GLSmokeTestCore.h b/tests/emscripten/glRenderSmokeTest/src/GLSmokeTestCore.h new file mode 120000 index 00000000000..8348b55e4e7 --- /dev/null +++ b/tests/emscripten/glRenderSmokeTest/src/GLSmokeTestCore.h @@ -0,0 +1 @@ +../../../gl/glRenderSmokeTest/src/GLSmokeTestCore.h \ No newline at end of file diff --git a/tests/emscripten/glRenderSmokeTest/src/main.cpp b/tests/emscripten/glRenderSmokeTest/src/main.cpp new file mode 120000 index 00000000000..1759b2cfa9c --- /dev/null +++ b/tests/emscripten/glRenderSmokeTest/src/main.cpp @@ -0,0 +1 @@ +../../../gl/glRenderSmokeTest/src/main.cpp \ No newline at end of file diff --git a/tests/emscripten/glRenderSmokeTest/src/ofApp.cpp b/tests/emscripten/glRenderSmokeTest/src/ofApp.cpp new file mode 120000 index 00000000000..f964b43be2e --- /dev/null +++ b/tests/emscripten/glRenderSmokeTest/src/ofApp.cpp @@ -0,0 +1 @@ +../../../gl/glRenderSmokeTest/src/ofApp.cpp \ No newline at end of file diff --git a/tests/emscripten/glRenderSmokeTest/src/ofApp.h b/tests/emscripten/glRenderSmokeTest/src/ofApp.h new file mode 120000 index 00000000000..23cba82a42e --- /dev/null +++ b/tests/emscripten/glRenderSmokeTest/src/ofApp.h @@ -0,0 +1 @@ +../../../gl/glRenderSmokeTest/src/ofApp.h \ No newline at end of file diff --git a/tests/gl/glRenderSmokeTest/README.md b/tests/gl/glRenderSmokeTest/README.md new file mode 100644 index 00000000000..1a3bea4ae12 --- /dev/null +++ b/tests/gl/glRenderSmokeTest/README.md @@ -0,0 +1,27 @@ +# GL render smoke test + +Exercises the code paths touched by the OpenGL ES 3.0/3.1 support work: +an indexed `ofVboMesh` drawn both directly (`drawElements`) and instanced +(`drawElementsInstanced`), a tessellated `ofPath` (tess2 / `ofIndexType`), +and an `ofFbo` round trip with a pixel readback to catch garbage/blank +triangles caused by an index-type/size mismatch. + +The actual test logic lives in `src/GLSmokeTestCore.h` and is shared, +unmodified, by every platform target (desktop here, `tests/emscripten/glRenderSmokeTest`, +`tests/ios/glRenderSmokeTest`) — only the platform bridge (`ofApp`/`main`) +differs. + +On desktop this is a normal windowed openFrameworks app picked up +automatically by the existing `tests/` CI harness (`scripts/ci/*/run_tests.sh`): +it runs a fixed number of frames, logs `GL_SMOKE_TEST RESULT=PASS` or +`RESULT=FAIL reason=...`, and exits with a matching process exit code. + +To run manually: + +```sh +cd tests/gl/glRenderSmokeTest +cp ../../../scripts/templates/osx/Makefile . # or linux/linux64 template +cp ../../../scripts/templates/osx/config.make . +make Debug +make RunDebug +``` diff --git a/tests/gl/glRenderSmokeTest/bin/data/.gitkeep b/tests/gl/glRenderSmokeTest/bin/data/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/gl/glRenderSmokeTest/src/GLSmokeTestCore.h b/tests/gl/glRenderSmokeTest/src/GLSmokeTestCore.h new file mode 100644 index 00000000000..511d94788a1 --- /dev/null +++ b/tests/gl/glRenderSmokeTest/src/GLSmokeTestCore.h @@ -0,0 +1,173 @@ +#pragma once + +#include "ofMain.h" + +// Shared by every platform target of glRenderSmokeTest (desktop, Emscripten, +// iOS, Android). Exercises the code paths touched by the OpenGL ES 3.0/3.1 +// support work: indexed ofVboMesh draw + drawInstanced (drawElements / +// drawElementsInstanced), a tessellated ofPath (tess2 / ofIndexType), and an +// ofFbo round trip, on whatever GL/GLES context the host window created. +// +// No custom shader is used deliberately: the GLSL version/#define story +// differs per platform (desktop core profile vs GLES2 vs GLES3 "300 es" vs +// Emscripten) and isn't what this PR touches. Using the renderer's own +// default shader keeps the test focused on the index-type and tess2 paths +// while still exercising the ES-version #ifdef branches in +// ofGLProgrammableRenderer indirectly (every draw call goes through them). +class GLSmokeTestCore { +public: + static constexpr int framesToRun = 12; + + void allocate() { + release(); + + indexedMesh.setMode(OF_PRIMITIVE_TRIANGLES); + indexedMesh.addVertices({ + {-60.f, -60.f, 0.f}, {60.f, -60.f, 0.f}, + {60.f, 60.f, 0.f}, {-60.f, 60.f, 0.f} + }); + indexedMesh.addColors({ofColor::red, ofColor::green, ofColor::blue, ofColor::yellow}); + indexedMesh.addIndices({0, 1, 2, 0, 2, 3}); + + tessellatedPath.clear(); + tessellatedPath.setFilled(true); + tessellatedPath.setFillColor(ofColor(255, 140, 20)); + tessellatedPath.setPolyWindingMode(OF_POLY_WINDING_ODD); + for (int i = 0; i < 12; ++i) { + const float radius = (i % 2 == 0) ? 55.f : 22.f; + const float angle = ofDegToRad(i * 30.f - 90.f); + const glm::vec2 point(std::cos(angle) * radius, std::sin(angle) * radius); + if (i == 0) { + tessellatedPath.moveTo(point); + } else { + tessellatedPath.lineTo(point); + } + } + tessellatedPath.close(); + + ofFbo::Settings settings; + settings.width = 128; + settings.height = 128; + settings.internalformat = GL_RGBA; + settings.useDepth = false; + settings.numSamples = 0; + fbo.allocate(settings); + + allocated = fbo.isAllocated(); + if (!allocated) { + fail("ofFbo failed to allocate"); + } + } + + void release() { + fbo.clear(); + allocated = false; + } + + void drawFrame() { + if (!allocated) { + fail("resources not allocated"); + return; + } + + clearGLErrors(); + + fbo.begin(); + ofClear(0, 0, 0, 255); + ofSetColor(255); + ofPushMatrix(); + ofTranslate(fbo.getWidth() * 0.5f, fbo.getHeight() * 0.5f); + indexedMesh.draw(); // ofGLProgrammableRenderer::draw(ofVboMesh) -> drawElements + indexedMesh.drawInstanced(OF_MESH_FILL, 2); // forces the drawElementsInstanced path (primCount > 1) + ofPopMatrix(); + fbo.end(); + checkGLErrors("after mesh draw"); + + ofSetColor(255); + fbo.draw(0.f, 0.f, static_cast(fbo.getWidth()), static_cast(fbo.getHeight())); + + ofPushMatrix(); + ofTranslate(180.f, 160.f); + tessellatedPath.draw(); // tess2 triangulation -> ofIndexType-sized indices + ofPopMatrix(); + checkGLErrors("after path draw"); + + if (frame == framesToRun - 1) { + verifyPixels(); + } + ++frame; + } + + bool finished() const { + return frame >= framesToRun; + } + + bool passed() const { + return allocated && glErrorCount == 0 && pixelCheckPassed && failureReason.empty(); + } + + std::string resultLine() const { + if (passed()) { + return "GL_SMOKE_TEST RESULT=PASS frames=" + ofToString(frame); + } + std::string reason = failureReason; + if (reason.empty() && glErrorCount > 0) { + reason = "glGetError seen " + ofToString(glErrorCount) + " time(s), last=" + ofToHex(lastGLError); + } + if (reason.empty() && !pixelCheckPassed) { + reason = "FBO readback did not contain the expected non-background color"; + } + return "GL_SMOKE_TEST RESULT=FAIL reason=" + reason; + } + +private: + void fail(const std::string & reason) { + if (failureReason.empty()) { + failureReason = reason; + } + ofLogError("glRenderSmokeTest") << reason; + } + + void clearGLErrors() { + while (glGetError() != GL_NO_ERROR) { + } + } + + void checkGLErrors(const std::string & where) { + GLenum error = glGetError(); + while (error != GL_NO_ERROR) { + ++glErrorCount; + lastGLError = error; + ofLogError("glRenderSmokeTest") << "glGetError=" << ofToHex(error) << " " << where; + error = glGetError(); + } + } + + void verifyPixels() { + ofPixels pixels; + fbo.readToPixels(pixels); + if (!pixels.isAllocated()) { + fail("FBO readToPixels produced no pixels"); + return; + } + const ofColor center = pixels.getColor(pixels.getWidth() / 2, pixels.getHeight() / 2); + // Background is cleared to black; any of the mesh's red/green/blue/yellow + // vertex colors interpolating in means the indexed draw actually reached + // the GPU with correctly-sized indices instead of reading garbage/nothing. + pixelCheckPassed = (center.r > 10 || center.g > 10 || center.b > 10); + if (!pixelCheckPassed) { + ofLogError("glRenderSmokeTest") << "center pixel=" << center << " (expected non-background)"; + } + } + + ofVboMesh indexedMesh; + ofPath tessellatedPath; + ofFbo fbo; + + bool allocated = false; + bool pixelCheckPassed = false; + int frame = 0; + int glErrorCount = 0; + GLenum lastGLError = GL_NO_ERROR; + std::string failureReason; +}; diff --git a/tests/gl/glRenderSmokeTest/src/main.cpp b/tests/gl/glRenderSmokeTest/src/main.cpp new file mode 100644 index 00000000000..05d0805d001 --- /dev/null +++ b/tests/gl/glRenderSmokeTest/src/main.cpp @@ -0,0 +1,23 @@ +#include "ofMain.h" +#include "ofApp.h" +#if defined(TARGET_GLFW_WINDOW) +#include "ofAppGLFWWindow.h" +#endif + +int main() { + ofGLWindowSettings settings; + settings.setSize(320, 240); + settings.windowMode = OF_WINDOW; + + auto window = ofCreateWindow(settings); +#if defined(TARGET_GLFW_WINDOW) + auto glfw = dynamic_cast(window.get()); + if (!glfw || !glfw->getGLFWWindow()) { + ofLogWarning("glRenderSmokeTest") << "no GL window (headless / GLFW init failed) — skip"; + return 0; + } +#endif + + ofRunApp(window, std::make_shared()); + return ofRunMainLoop(); +} diff --git a/tests/gl/glRenderSmokeTest/src/ofApp.cpp b/tests/gl/glRenderSmokeTest/src/ofApp.cpp new file mode 100644 index 00000000000..e6be032f18d --- /dev/null +++ b/tests/gl/glRenderSmokeTest/src/ofApp.cpp @@ -0,0 +1,42 @@ +#include "ofApp.h" + +#include +#ifdef __EMSCRIPTEN__ +#include +#endif + +namespace { +// std::exit() alone doesn't stop Emscripten's requestAnimationFrame loop +// (the runtime stays alive for pending async work), so draw() keeps getting +// called forever after "finishing" unless we guard against it below, and the +// process itself needs emscripten_force_exit() to actually stop. +void terminateProcess(int code) { +#ifdef __EMSCRIPTEN__ + emscripten_force_exit(code); +#else + std::exit(code); +#endif +} +} + +void ofApp::setup() { + ofSetLogLevel(OF_LOG_NOTICE); + ofSetFrameRate(60); + ofBackground(0); + ofLogNotice("glRenderSmokeTest") << "renderer=" << glGetString(GL_RENDERER) + << " version=" << glGetString(GL_VERSION); + core.allocate(); +} + +void ofApp::draw() { + if (core.finished()) { + return; + } + + core.drawFrame(); + + if (core.finished()) { + ofLogNotice("glRenderSmokeTest") << core.resultLine(); + terminateProcess(core.passed() ? 0 : 1); + } +} diff --git a/tests/gl/glRenderSmokeTest/src/ofApp.h b/tests/gl/glRenderSmokeTest/src/ofApp.h new file mode 100644 index 00000000000..ab09390ce4b --- /dev/null +++ b/tests/gl/glRenderSmokeTest/src/ofApp.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ofMain.h" +#include "GLSmokeTestCore.h" + +class ofApp : public ofBaseApp { +public: + void setup() override; + void draw() override; + +private: + GLSmokeTestCore core; +}; diff --git a/tests/ios/glRenderSmokeTest/addons.make b/tests/ios/glRenderSmokeTest/addons.make new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/ios/glRenderSmokeTest/bin/data/.gitkeep b/tests/ios/glRenderSmokeTest/bin/data/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/AppIcon.appiconset/Contents.json b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..f01c3b410b6 --- /dev/null +++ b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "of_ios.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/AppIcon.appiconset/of_ios.png b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/AppIcon.appiconset/of_ios.png new file mode 100644 index 0000000000000000000000000000000000000000..47dc7fcc845f2d9a2422e25cf7edc5632439cdf9 GIT binary patch literal 50843 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 literal 0 HcmV?d00001 diff --git a/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Contents.json b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Logo.symbolset/Contents.json b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Logo.symbolset/Contents.json new file mode 100644 index 00000000000..6b61fc25943 --- /dev/null +++ b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Logo.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "of.svg", + "idiom" : "universal" + } + ] +} diff --git a/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Logo.symbolset/of.svg b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Logo.symbolset/of.svg new file mode 100644 index 00000000000..44fe31c4634 --- /dev/null +++ b/tests/ios/glRenderSmokeTest/mediaAssets/Images.xcassets/Logo.symbolset/of.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/ios/glRenderSmokeTest/mediaAssets/LaunchScreen.storyboard b/tests/ios/glRenderSmokeTest/mediaAssets/LaunchScreen.storyboard new file mode 100644 index 00000000000..e46c7b16d1c --- /dev/null +++ b/tests/ios/glRenderSmokeTest/mediaAssets/LaunchScreen.storyboard @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ios/glRenderSmokeTest/src/GLSmokeTestCore.h b/tests/ios/glRenderSmokeTest/src/GLSmokeTestCore.h new file mode 120000 index 00000000000..8348b55e4e7 --- /dev/null +++ b/tests/ios/glRenderSmokeTest/src/GLSmokeTestCore.h @@ -0,0 +1 @@ +../../../gl/glRenderSmokeTest/src/GLSmokeTestCore.h \ No newline at end of file diff --git a/tests/ios/glRenderSmokeTest/src/main.mm b/tests/ios/glRenderSmokeTest/src/main.mm new file mode 100644 index 00000000000..f8a3ad8e008 --- /dev/null +++ b/tests/ios/glRenderSmokeTest/src/main.mm @@ -0,0 +1,19 @@ +#include "ofApp.h" + +int main() { + + // here are the most commonly used iOS window settings. + //------------------------------------------------------ + ofiOSWindowSettings settings; + settings.enableRetina = true; // enables retina resolution if the device supports it. + settings.enableDepth = false; // enables depth buffer for 3d drawing. + settings.enableAntiAliasing = true; // enables anti-aliasing which smooths out graphics on the screen. + settings.numOfAntiAliasingSamples = 4; // number of samples used for anti-aliasing. + settings.enableHardwareOrientation = false; // enables native view orientation. + settings.enableHardwareOrientationAnimation = false; // enables native orientation changes to be animated. + settings.glesVersion = OFXIOS_RENDERER_ES3; // type of renderer to use, ES1, ES2, ES3. + + ofCreateWindow(settings); + + return ofRunApp(new ofApp); +} diff --git a/tests/ios/glRenderSmokeTest/src/ofApp.h b/tests/ios/glRenderSmokeTest/src/ofApp.h new file mode 100644 index 00000000000..324996637e5 --- /dev/null +++ b/tests/ios/glRenderSmokeTest/src/ofApp.h @@ -0,0 +1,27 @@ +#pragma once + +#include "ofxiOS.h" +#include "GLSmokeTestCore.h" + +class ofApp : public ofxiOSApp { +public: + void setup(); + void update(); + void draw(); + void exit(); + + void touchDown(ofTouchEventArgs & touch); + void touchMoved(ofTouchEventArgs & touch); + void touchUp(ofTouchEventArgs & touch); + void touchDoubleTap(ofTouchEventArgs & touch); + void touchCancelled(ofTouchEventArgs & touch); + + void lostFocus(); + void gotFocus(); + void gotMemoryWarning(); + void deviceOrientationChanged(int newOrientation); + +private: + GLSmokeTestCore core; + bool resultLogged = false; +}; diff --git a/tests/ios/glRenderSmokeTest/src/ofApp.mm b/tests/ios/glRenderSmokeTest/src/ofApp.mm new file mode 100644 index 00000000000..1d75df32dd5 --- /dev/null +++ b/tests/ios/glRenderSmokeTest/src/ofApp.mm @@ -0,0 +1,82 @@ +#include "ofApp.h" + +//-------------------------------------------------------------- +void ofApp::setup(){ + ofSetLogLevel(OF_LOG_NOTICE); + ofSetVerticalSync(false); + ofBackground(0); + ofLogNotice("glRenderSmokeTest") << "renderer=" << glGetString(GL_RENDERER) + << " version=" << glGetString(GL_VERSION); + core.allocate(); +} + +//-------------------------------------------------------------- +void ofApp::update(){ + +} + +//-------------------------------------------------------------- +void ofApp::draw(){ + if(core.finished()) { + if(!resultLogged) { + resultLogged = true; + // simctl/logcat-style scraping: the runner script greps device log + // output for this exact marker rather than relying on process exit + // codes, since a foreground iOS app can't cleanly exit(0)/exit(1). + ofLogNotice("glRenderSmokeTest") << core.resultLine(); + } + return; + } + + core.drawFrame(); +} + +//-------------------------------------------------------------- +void ofApp::exit(){ + +} + +//-------------------------------------------------------------- +void ofApp::touchDown(ofTouchEventArgs & touch){ + +} + +//-------------------------------------------------------------- +void ofApp::touchMoved(ofTouchEventArgs & touch){ + +} + +//-------------------------------------------------------------- +void ofApp::touchUp(ofTouchEventArgs & touch){ + +} + +//-------------------------------------------------------------- +void ofApp::touchDoubleTap(ofTouchEventArgs & touch){ + +} + +//-------------------------------------------------------------- +void ofApp::touchCancelled(ofTouchEventArgs & touch){ + +} + +//-------------------------------------------------------------- +void ofApp::lostFocus(){ + +} + +//-------------------------------------------------------------- +void ofApp::gotFocus(){ + +} + +//-------------------------------------------------------------- +void ofApp::gotMemoryWarning(){ + +} + +//-------------------------------------------------------------- +void ofApp::deviceOrientationChanged(int newOrientation){ + +} From 68d77251d2ea055bc3ecf95f5f94a4a13f48f6a8 Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Fri, 11 Sep 2026 21:08:48 +1000 Subject: [PATCH 2/2] Fix msys2 clang64: move -std=gnu++17 from CFLAGS to CXXFLAGS (fix #8557 cameras.c) Same fix as #8556, needed for gl-smoke-tests to pass clang64. C files (audio.c, cameras.c) fail with 'invalid argument -std=gnu++17 not allowed with C'. --- .../project/msys2/config.msys2.default.mk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/openFrameworksCompiled/project/msys2/config.msys2.default.mk b/libs/openFrameworksCompiled/project/msys2/config.msys2.default.mk index bb5e17076c0..7b5709fcddc 100644 --- a/libs/openFrameworksCompiled/project/msys2/config.msys2.default.mk +++ b/libs/openFrameworksCompiled/project/msys2/config.msys2.default.mk @@ -27,7 +27,8 @@ #MINGW_PREFIX ?= /mingw32 MINGW_PREFIX ?= /mingw64 -PLATFORM_CFLAGS += -std=gnu++17 -DUNICODE -D_UNICODE +PLATFORM_CXXFLAGS += -std=gnu++17 +PLATFORM_CFLAGS += -DUNICODE -D_UNICODE #PLATFORM_CFLAGS += -IC:/msys64/mingw32/include/gstreamer-1.0 -DOF_VIDEO_PLAYER_GSTREAMER CC = $(MINGW_PREFIX)/bin/gcc