diff --git a/Resources/Shaders/oolite-blur.fragment b/Resources/Shaders/oolite-blur.fragment index 1f12ba4530..add43cbe20 100644 --- a/Resources/Shaders/oolite-blur.fragment +++ b/Resources/Shaders/oolite-blur.fragment @@ -1,3 +1,4 @@ +#ifndef OO_LEGACY_GL out vec4 FragColor; in vec2 TexCoords; @@ -53,3 +54,77 @@ void main() FragColor = vec4(result, 1.0); } } +#else + +// Legacy GLSL 1.20 port of the 7x7 std dev 3.0 Gaussian blur. The original +// used a uniform array initialized with an array constructor, textureSize() +// and isnan() — none of which exist in GLSL 1.20. The weights are now const +// scalars, the taps are unrolled, the texel size arrives as a uniform set +// from ObjC, and the NaN check uses x != x. +const float weight0 = 0.13649812722755; +const float weight1 = 0.12918787500405662; +const float weight2 = 0.10952340502389683; +const float weight3 = 0.08317258170844949; +const float weight4 = 0.05657737457255749; +const float weight5 = 0.03447396964662017; +const float weight6 = 0.018815730430644367; + +const float radius = 1.2; + +uniform sampler2D imageIn; +uniform bool horizontal; +uniform vec2 texelSize; // 1.0 / framebuffer size (replaces textureSize) + +varying vec2 TexCoords; + +#define BLUR_TAP(off, w) result += texture2D(imageIn, TexCoords + (off)).rgb * (w); + +void main() +{ + vec2 tex_offset = texelSize; + vec3 result = texture2D(imageIn, TexCoords).rgb * weight0; // current fragment's contribution + if(horizontal) + { + BLUR_TAP(vec2( tex_offset.x * 1.0 * radius, 0.0), weight1) + BLUR_TAP(vec2(-tex_offset.x * 1.0 * radius, 0.0), weight1) + BLUR_TAP(vec2( tex_offset.x * 2.0 * radius, 0.0), weight2) + BLUR_TAP(vec2(-tex_offset.x * 2.0 * radius, 0.0), weight2) + BLUR_TAP(vec2( tex_offset.x * 3.0 * radius, 0.0), weight3) + BLUR_TAP(vec2(-tex_offset.x * 3.0 * radius, 0.0), weight3) + BLUR_TAP(vec2( tex_offset.x * 4.0 * radius, 0.0), weight4) + BLUR_TAP(vec2(-tex_offset.x * 4.0 * radius, 0.0), weight4) + BLUR_TAP(vec2( tex_offset.x * 5.0 * radius, 0.0), weight5) + BLUR_TAP(vec2(-tex_offset.x * 5.0 * radius, 0.0), weight5) + BLUR_TAP(vec2( tex_offset.x * 6.0 * radius, 0.0), weight6) + BLUR_TAP(vec2(-tex_offset.x * 6.0 * radius, 0.0), weight6) + } + else + { + BLUR_TAP(vec2(0.0, tex_offset.y * 1.0 * radius), weight1) + BLUR_TAP(vec2(0.0, -tex_offset.y * 1.0 * radius), weight1) + BLUR_TAP(vec2(0.0, tex_offset.y * 2.0 * radius), weight2) + BLUR_TAP(vec2(0.0, -tex_offset.y * 2.0 * radius), weight2) + BLUR_TAP(vec2(0.0, tex_offset.y * 3.0 * radius), weight3) + BLUR_TAP(vec2(0.0, -tex_offset.y * 3.0 * radius), weight3) + BLUR_TAP(vec2(0.0, tex_offset.y * 4.0 * radius), weight4) + BLUR_TAP(vec2(0.0, -tex_offset.y * 4.0 * radius), weight4) + BLUR_TAP(vec2(0.0, tex_offset.y * 5.0 * radius), weight5) + BLUR_TAP(vec2(0.0, -tex_offset.y * 5.0 * radius), weight5) + BLUR_TAP(vec2(0.0, tex_offset.y * 6.0 * radius), weight6) + BLUR_TAP(vec2(0.0, -tex_offset.y * 6.0 * radius), weight6) + } + + // at least one known OXP, DH nav buoy, has an NaN issue resulting in black squares appearing on a + // specific location on its main model during the blurring passes, so make sure to always check + // against NaNs + if(result.x != result.x || result.y != result.y || result.z != result.z) + { + // mark offending pixel in red + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + else + { + gl_FragColor = vec4(result, 1.0); + } +} +#endif diff --git a/Resources/Shaders/oolite-blur.vertex b/Resources/Shaders/oolite-blur.vertex index 657c2dbd5d..793392371a 100644 --- a/Resources/Shaders/oolite-blur.vertex +++ b/Resources/Shaders/oolite-blur.vertex @@ -1,3 +1,4 @@ +#ifndef OO_LEGACY_GL layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoords; @@ -7,4 +8,16 @@ void main() { TexCoords = aTexCoords; gl_Position = vec4(aPos, 1.0); -} \ No newline at end of file +} +#else +attribute vec3 aPos; +attribute vec2 aTexCoords; + +varying vec2 TexCoords; + +void main() +{ + TexCoords = aTexCoords; + gl_Position = vec4(aPos.xy, 0.0, 1.0); +} +#endif diff --git a/Resources/Shaders/oolite-default-planet.fragment b/Resources/Shaders/oolite-default-planet.fragment index 86c3e52a6f..3ea0f3d8cf 100644 --- a/Resources/Shaders/oolite-default-planet.fragment +++ b/Resources/Shaders/oolite-default-planet.fragment @@ -249,6 +249,7 @@ vec3 CalcSpecularLight(in vec3 lightVector, in vec3 eyeVector, in float exponent // https://www.jcgt.org/published/0010/02/02/ // original code returns roughness squared but we need the actual value for our // specular calculations, so we return its square root instead +#ifndef OO_LEGACY_GL float isotropicNDFFiltering (vec3 normal, float roughness2) { const float sigma2 = 0.15915494; @@ -260,6 +261,13 @@ float isotropicNDFFiltering (vec3 normal, float roughness2) float filteredRoughness2 = clamp((roughness2 + clampedKernelRoughness2), 0.0, 1.0); return sqrt(filteredRoughness2); } +#else +// Legacy GLSL (no derivatives): skip specular anti-aliasing, pass roughness through. +float isotropicNDFFiltering (vec3 normal, float roughness2) +{ + return clamp(sqrt(roughness2), 0.0, 1.0); +} +#endif // More physically accurate specular lighting models diff --git a/Resources/Shaders/oolite-default-shader.fragment b/Resources/Shaders/oolite-default-shader.fragment index d642b9d66e..47ffe2e578 100644 --- a/Resources/Shaders/oolite-default-shader.fragment +++ b/Resources/Shaders/oolite-default-shader.fragment @@ -244,6 +244,7 @@ vec4 CalcSpecularLight(in vec3 lightVector, in vec3 eyeVector, in float exponent // https://www.jcgt.org/published/0010/02/02/ // original code returns roughness squared but we need the actual value for our // specular calculations, so we return its square root instead +#ifndef OO_LEGACY_GL float isotropicNDFFiltering (vec3 normal, float roughness2) { const float sigma2 = 0.15915494; @@ -255,6 +256,13 @@ float isotropicNDFFiltering (vec3 normal, float roughness2) float filteredRoughness2 = clamp((roughness2 + clampedKernelRoughness2), 0.0, 1.0); return sqrt(filteredRoughness2); } +#else +// Legacy GLSL (no derivatives): skip specular anti-aliasing, pass roughness through. +float isotropicNDFFiltering (vec3 normal, float roughness2) +{ + return clamp(sqrt(roughness2), 0.0, 1.0); +} +#endif // More physically accurate specular lighting models diff --git a/Resources/Shaders/oolite-final.fragment b/Resources/Shaders/oolite-final.fragment index ec7fbf1797..8599cc57f0 100644 --- a/Resources/Shaders/oolite-final.fragment +++ b/Resources/Shaders/oolite-final.fragment @@ -1,3 +1,4 @@ +#ifndef OO_LEGACY_GL out vec4 FragColor; in vec2 TexCoords; @@ -856,3 +857,868 @@ void main() //FragColor = vec4(bloomColor, 1.0); FragColor = vec4(result, 1.0); } +#else +// GLSL 1.20 port of the OpenGL 3.3 original (see upstream history): the +// user-facing `out` declaration became gl_FragColor, `in` became `varying`, +// texture() became texture2D(), the switch statements became if/else chains +// and the global initialized from a uniform was removed. + +varying vec2 TexCoords; + +uniform sampler2D scene; +uniform sampler2D bloomBlur; +uniform bool bloom; +uniform int uPostFX; +uniform float uTime; +uniform vec2 uResolution; +uniform float uSaturation; +uniform int uSDRToneMapper; + +#ifndef OO_DITHER +#define OO_DITHER 1 +#endif + + +// ========================================================================================== +// grayscale +// ========================================================================================== +vec3 grayscale(vec3 col) +{ + vec3 luma = vec3(0.2126, 0.7152, 0.0722); + col = vec3(dot(col, luma)); + + return col; +} +// ------------------------------------------------------------------------------------------ + + +// SDR Tone Mappers +//----------------- +// +// AgX code from https://github.com/donmccurdy/three.js/blob/feat/agx-punchy/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js +// and https://iolite-engine.com/blog_posts/minimal_agx_implementation +// MIT License +// +// Agx tone mapper +// Matrices for rec 2020 <> rec 709 color space conversion +// matrix provided in row-major order so it has been transposed +// https://www.itu.int/pub/R-REP-BT.2407-2017 + +#define AGX_LOOK_BASE 0 +#define AGX_LOOK_PUNCHY 1 +#define AGX_LOOK_GOLDEN 2 + +const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3( + vec3( 1.6605, - 0.1246, - 0.0182 ), + vec3( - 0.5876, 1.1329, - 0.1006 ), + vec3( - 0.0728, - 0.0083, 1.1187 ) +); + +const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3( + vec3( 0.6274, 0.0691, 0.0164 ), + vec3( 0.3293, 0.9195, 0.0880 ), + vec3( 0.0433, 0.0113, 0.8956 ) +); + +// Mean error^2: 1.85907662e-06 +vec3 agxDefaultContrastApprox7(vec3 x) +{ + vec3 x2 = x * x; + vec3 x4 = x2 * x2; + vec3 x6 = x4 * x2; + return - 17.86 * x6 * x + + 78.01 * x6 + - 126.7 * x4 * x + + 92.06 * x4 + - 28.72 * x2 * x + + 4.361 * x2 + - 0.1718 * x + + 0.002857; +} + +// Mean error^2: 3.6705141e-06 +vec3 agxDefaultContrastApprox(vec3 x) +{ + vec3 x2 = x * x; + vec3 x4 = x2 * x2; + + return + 15.5 * x4 * x2 + - 40.14 * x4 * x + + 31.96 * x4 + - 6.868 * x2 * x + + 0.4298 * x2 + + 0.1191 * x + - 0.00232; +} + +vec3 agxLook(vec3 color, int look) +{ + if (look == AGX_LOOK_BASE) + { + return color; + } + + vec3 offset = vec3(0.0); + vec3 slope = vec3(1.0); + vec3 power = vec3(1.0); + float sat = 1.0; + + if (look == AGX_LOOK_PUNCHY) + { + power = vec3(1.35, 1.35, 1.35); + sat = 1.4; + } + else // AGX_LOOK_GOLDEN + { + slope = vec3(1.0, 0.9, 0.5); + power = vec3(0.8); + sat = 0.8; + } + + // ASC CDL + color = pow(color * slope + offset, power); + + const vec3 lw = vec3(0.2126, 0.7152, 0.0722); + float luma = dot(color, lw); + + return luma + sat * (color - luma); +} + +// AgX Tone Mapping implementation based on Filament, which in turn is based +// on Blender's implementation using rec 2020 primaries +// https://github.com/google/filament/pull/7236 +// Inputs and outputs are encoded as Linear-sRGB. + +vec3 AgX(vec3 color, int look) +{ + float toneMappingExposure = 1.0; + + // AgX constants + const mat3 AgXInsetMatrix = mat3( + vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ), + vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ), + vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) + ); + + // explicit AgXOutsetMatrix generated from Filaments AgXOutsetMatrixInv + const mat3 AgXOutsetMatrix = mat3( + vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ), + vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ), + vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 ) + ); + + // LOG2_MIN = -10.0 + // LOG2_MAX = +6.5 + // MIDDLE_GRAY = 0.18 + const float AgxMinEv = - 12.47393; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY) + const float AgxMaxEv = 4.026069; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY) + + color *= toneMappingExposure; + + color = LINEAR_SRGB_TO_LINEAR_REC2020 * color; + + color = AgXInsetMatrix * color; + + // Log2 encoding + color = max(color, 1e-10); // avoid 0 or negative numbers for log2 + color = log2(color); + color = (color - AgxMinEv) / (AgxMaxEv - AgxMinEv); + + color = clamp(color, 0.0, 1.0); + + // Apply sigmoid + color = agxDefaultContrastApprox(color); + + // Apply AgX look + color = agxLook(color, look); + + color = AgXOutsetMatrix * color; + + // Linearize + color = pow(max( vec3( 0.0 ), color), vec3(2.2)); + + color = LINEAR_REC2020_TO_LINEAR_SRGB * color; + + // Gamut mapping. Simple clamp for now. + color = clamp(color, 0.0, 1.0); + + return color; +} +//----------------- + + +vec3 ACESFilm(vec3 color) +{ + // taken from https://www.shadertoy.com/view/XsGfWV This is based on the actual + // ACES sources and is effectively the glsl translation of Stephen Hill's fit + // (https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl) + mat3 m1 = mat3( + 0.59719, 0.07600, 0.02840, + 0.35458, 0.90834, 0.13383, + 0.04823, 0.01566, 0.83777 + ); + mat3 m2 = mat3( + 1.60475, -0.10208, -0.00327, + -0.53108, 1.10813, -0.07276, + -0.07367, -0.00605, 1.07602 + ); + // prevents some ACES artifacts, especially bright blues shifting towards purple + // see https://community.acescentral.com/t/colour-artefacts-or-breakup-using-aces/520/48 + mat3 highlightsFixLMT = mat3( + 0.9404372683, -0.0183068787, 0.0778696104, + 0.0083786969, 0.8286599939, 0.1629613092, + 0.0005471261, -0.0008833746, 1.0003362486 + ); + vec3 v = m1 * color * highlightsFixLMT; + vec3 a = v * (v + 0.0245786) - 0.000090537; + vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081; + return clamp(m2 * (a / b), 0.0, 1.0); +} +//----------------- + + +vec3 hejlBurgessDawson(vec3 x) +{ + // includes gamma correction in the formula + x = max(vec3(0.0), x -vec3(0.004)); + return (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06); +} +//----------------- + + +vec3 uc2(vec3 x) +{ + // no float-literal `f` suffixes: illegal before GLSL 1.30 + const float A = 0.15; + const float B = 0.50; + const float C = 0.10; + const float D = 0.20; + const float E = 0.02; + const float F = 0.30; + const float W = 11.2; + const float exposureBias = 2.0; + x *= exposureBias; + x = ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; + float white = ((W * (A * W + C * B) + D * E) / (W * (A * W + B) + D * F)) - E / F; + return x / max(vec3(0.00001), white); +} +//----------------- + + +// Uchimura 2017, "HDR theory and practice" +// https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp +vec3 uchimura(vec3 x, float P, float a, float m, float l, float c, float b) +{ + float l0 = ((P - m) * l) / a; + float L0 = m - m / a; + float L1 = m + (1.0 - m) / a; + float S0 = m + l0; + float S1 = m + a * l0; + float C2 = (a * P) / (P - S1); + float CP = -C2 / P; + + vec3 w0 = vec3(1.0 - smoothstep(0.0, m, x)); + vec3 w2 = vec3(step(m + l0, x)); + vec3 w1 = vec3(1.0 - w0 - w2); + + vec3 T = vec3(m * pow(x / m, vec3(c)) + b); + vec3 S = vec3(P - (P - S1) * exp(CP * (x - S0))); + vec3 L = vec3(m + a * (x - m)); + + return T * w0 + L * w1 + S * w2; +} + + +vec3 uchimura(vec3 x) +{ + const float P = 1.0; // max display brightness + const float a = 1.0; // contrast + const float m = 0.22; // linear section start + const float l = 0.4; // linear section length + const float c = 1.33; // black + const float b = 0.0; // pedestal + + return uchimura(x, P, a, m, l, c, b); +} +//----------------- + + +vec3 reinhard(vec3 x) +{ + return x / (x + vec3(1.0)); +} +//----------------- + + +mat3 saturationMatrix( float saturation ) +{ + vec3 luminance = vec3(0.2126, 0.7152, 0.0722); + float oneMinusSat = 1.0 - saturation; + + vec3 red = vec3( luminance.x * oneMinusSat ); + red.r += saturation; + + vec3 green = vec3( luminance.y * oneMinusSat ); + green.g += saturation; + + vec3 blue = vec3( luminance.z * oneMinusSat ); + blue.b += saturation; + + return mat3( + red, + green, + blue); +} + + +//========================================================================================== +// desaturateHighlights - we desaturate colors as they become brighter +//========================================================================================== +vec3 desaturateHighlights(vec3 col) +{ + // Kd is rate of desaturation + const float Kd = 0.055; + vec3 luma = grayscale(col); + float desaturationFraction = 1.0 - 1.0 / (Kd * max(col.r, max(col.g, col.b)) + 1.0); + return mix(col, luma, desaturationFraction); +} + + +// ========================================================================================== +// cloak vision - original code by Nikos Papadopoulos, https://www.shadertoy.com/view/Xsl3zf +// ========================================================================================== +float hash(float n) +{ + return fract(sin(n)*43758.5453123); +} + +vec3 cloakVision(vec3 c) +{ + vec2 p = TexCoords; + vec2 u = p * 2. - 1.; + vec2 n = u; + float time = uTime; + + + // flicker, grain, vignette, fade in + c += sin(hash(uTime)) * 0.01; + c += hash((hash(n.x) + n.y) * uTime) * 0.25; + c *= smoothstep(length(n * n * n * vec2(0.075, 0.4)), 1.0, 0.4); + c *= smoothstep(0.001, 3.5, uTime) * 1.5; + + c = grayscale(c) * vec3(0.2, 0.4, 1.5 - hash(uTime) * 0.3); + + return c; +} +// ------------------------------------------------------------------------------------------ + +// ========================================================================================== +// color blindness correction - see https://terra-howard.com/thesis/ +// ========================================================================================== +const mat3 RGBtoOpponentMat = mat3(0.2814, -0.0971, -0.0930, 0.6938, 0.1458,-0.2529, 0.0638, -0.0250, 0.4665); +const mat3 OpponentToRGBMat = mat3(1.1677, 0.9014, 0.7214, -6.4315, 2.5970, 0.1257, -0.5044, 0.0159, 2.0517); + +//const int NONE = 0; +const int PROTANOPIA = 1; +const int DEUTERANOPIA = 2; +const int TRITANOPIA = 3; + +// GLSL 1.20: globals must have constant-expression initializers, so read the +// uPostFX uniform directly at the use sites instead of aliasing it here. + +void blindnessFilter( out vec3 myoutput, in vec3 myinput ) +{ + if (uPostFX == PROTANOPIA) { + vec3 opponentColor = RGBtoOpponentMat * myinput; + opponentColor.x -= opponentColor.y * 1.5; // reds (y <= 0) become lighter, greens (y >= 0) become darker + vec3 rgbColor = OpponentToRGBMat * opponentColor; + myoutput = rgbColor; + } else if (uPostFX == DEUTERANOPIA) { + vec3 opponentColor = RGBtoOpponentMat * myinput; + opponentColor.x -= opponentColor.y * 1.5; // reds (y <= 0) become lighter, greens (y >= 0) become darker + vec3 rgbColor = OpponentToRGBMat * opponentColor; + myoutput = rgbColor; + } else if (uPostFX == TRITANOPIA) { + vec3 opponentColor = RGBtoOpponentMat * myinput; + opponentColor.x -= ((3.0 * opponentColor.z) - opponentColor.y) * 0.25; + vec3 rgbColor = OpponentToRGBMat * opponentColor; + myoutput = rgbColor; + } else { + myoutput = myinput; + } +} + +void blindnessVision( out vec3 myoutput, in vec3 myinput ) +{ + vec4 blindVisionR; + vec4 blindVisionG; + vec4 blindVisionB; + if (uPostFX == PROTANOPIA) { + blindVisionR = vec4( 0.20, 0.99, -0.19, 0.0); + blindVisionG = vec4( 0.16, 0.79, 0.04, 0.0); + blindVisionB = vec4( 0.01, -0.01, 1.00, 0.0); + } else if (uPostFX == DEUTERANOPIA) { + blindVisionR = vec4( 0.43, 0.72, -0.15, 0.0 ); + blindVisionG = vec4( 0.34, 0.57, 0.09, 0.0 ); + blindVisionB = vec4(-0.02, 0.03, 1.00, 0.0 ); + } else if (uPostFX == TRITANOPIA) { + blindVisionR = vec4( 0.97, 0.11, -0.08, 0.0 ); + blindVisionG = vec4( 0.02, 0.82, 0.16, 0.0 ); + blindVisionB = vec4(-0.06, 0.88, 0.18, 0.0 ); + } else { + blindVisionR = vec4(1.0, 0.0, 0.0, 0.0 ); + blindVisionG = vec4(0.0, 1.0, 0.0, 0.0 ); + blindVisionB = vec4(0.0, 0.0, 1.0, 0.0 ); + } + myoutput = vec3(dot(myinput, blindVisionR.rgb), dot(myinput, blindVisionG.rgb), dot(myinput, blindVisionB.rgb)); +} + +vec3 colorBlindness(vec3 inColor) +{ + vec3 texColor = inColor; + vec3 tmp; + vec3 colorBlindColor = inColor; + blindnessFilter(tmp, texColor); + blindnessVision(colorBlindColor, tmp); + //blindnessVision(colorBlindColor, texColor); + + return colorBlindColor; +} +// ------------------------------------------------------------------------------------------ + + +// ========================================================================================== +// old movie - based on https://www.shadertoy.com/view/Xdl3D8 +// ========================================================================================== +#define BLACK_AND_WHITE +#define LINES_AND_FLICKER +#define BLOTCHES +#define GRAIN +//#define ROLLING_FILM + +#define FREQUENCY 15.0 +#define sepia mat3(0.393, 0.769, 0.189, 0.349, 0.686, 0.168, 0.272, 0.534, 0.131) + +float rand(vec2 co) +{ + return fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453); +} + +float rand(float c){ + return rand(vec2(c,1.0)); +} + +float randomLine(float seed, vec2 uv) +{ + float b = 0.01 * rand(seed); + float a = rand(seed+1.0); + float c = rand(seed+2.0) - 0.5; + float mu = rand(seed+3.0); + + float l = 1.0; + + if ( mu > 0.2) + l = pow( abs(a * uv.x + b * uv.y + c ), 1.0/8.0 ); + else + l = 2.0 - pow( abs(a * uv.x + b * uv.y + c), 1.0/8.0 ); + + return mix(0.5, 1.0, l); +} + +// Generate some blotches. +float randomBlotch(float seed, vec2 uv) +{ + float x = rand(seed); + float y = rand(seed+1.0); + float s = 0.01 * rand(seed+2.0); + + vec2 p = vec2(x,y) - uv; + p.x *= uResolution.x / uResolution.y; + float a = atan(p.y,p.x); + float v = 1.0; + float ss = s*s * (sin(6.2831*a*x)*0.1 + 1.0); + + if ( dot(p,p) < ss ) v = 0.2; + else + v = pow(dot(p,p) - ss, 1.0/16.0); + + return mix(0.3 + 0.2 * (1.0 - (s / 0.02)), 1.0, v); +} + +vec3 oldMovie(sampler2D inImage, vec2 fragCoord) +{ + vec3 fragColor = vec3(0.0); + vec2 uv = fragCoord; + // Set frequency of global effect to 15 variations per second + float t = float(int(uTime * FREQUENCY)); + + // Get some image movement + vec2 suv = uv + 0.002 * vec2( rand(t), rand(t + 23.0)); + + // Get the image + vec3 image = texture2D( inImage, vec2(suv.x, suv.y) ).xyz; + + #ifdef BLACK_AND_WHITE + // Convert it to B/W + //float luma = dot( vec3(0.2126, 0.7152, 0.0722), image ); + //vec3 oldImage = luma * vec3(0.7, 0.7, 0.7); + vec3 oldImage = image * sepia; + #else + vec3 oldImage = image; + #endif + + #ifdef ROLLING_FILM + // rolling film effect + vec2 nos_pos = fragCoord - 0.5 + vec2(min(fract(uTime) - 0.02, 0.0), 0.0); + vec2 abs_n_p = vec2(abs(nos_pos)); + if(abs_n_p.x > 0.40) + { + if(abs_n_p.x > 0.42 && abs_n_p.x < 0.48) + oldImage = vec3(0.7) * step(mod(10.0 * (nos_pos.y + fract(uTime)), 1.0), 0.8); + else + oldImage = vec3(0.0); + } + #endif + + // Create a time-varying vignetting effect + float vI = 16.0 * (uv.x * (1.0-uv.x) * uv.y * (1.0-uv.y)); + vI *= mix( 0.7, 1.0, rand(t + 0.5)); + + // Add additive flicker + vI += 1.0 + 0.4 * rand(t+8.); + + // Add a fixed vignetting (independent of the flicker) + vI *= pow(16.0 * uv.x * (1.0-uv.x) * uv.y * (1.0-uv.y), 0.4); + + // Add some random lines and some multiplicative flicker + #ifdef LINES_AND_FLICKER + int l = int(8.0 * rand(t+7.0)); + + if ( 0 < l ) vI *= randomLine( t+6.0+17.* float(0), uv); + if ( 1 < l ) vI *= randomLine( t+6.0+17.* float(1), uv); + if ( 2 < l ) vI *= randomLine( t+6.0+17.* float(2), uv); + if ( 3 < l ) vI *= randomLine( t+6.0+17.* float(3), uv); + if ( 4 < l ) vI *= randomLine( t+6.0+17.* float(4), uv); + if ( 5 < l ) vI *= randomLine( t+6.0+17.* float(5), uv); + if ( 6 < l ) vI *= randomLine( t+6.0+17.* float(6), uv); + if ( 7 < l ) vI *= randomLine( t+6.0+17.* float(7), uv); + + #endif + + // Add some random blotches. + #ifdef BLOTCHES + int s = int( max(8.0 * rand(t+18.0) -2.0, 0.0 )); + + if ( 0 < s ) vI *= randomBlotch( t+6.0+19.* float(0), uv); + if ( 1 < s ) vI *= randomBlotch( t+6.0+19.* float(1), uv); + if ( 2 < s ) vI *= randomBlotch( t+6.0+19.* float(2), uv); + if ( 3 < s ) vI *= randomBlotch( t+6.0+19.* float(3), uv); + if ( 4 < s ) vI *= randomBlotch( t+6.0+19.* float(4), uv); + if ( 5 < s ) vI *= randomBlotch( t+6.0+19.* float(5), uv); + + #endif + + // Show the image modulated by the defects + fragColor = oldImage * vI; + + // Add some grain + #ifdef GRAIN + fragColor *= (1.0+(rand(uv+t*.01)-.2)*.15); + #endif + + return fragColor; +} +// ------------------------------------------------------------------------------------------ + +// ========================================================================================== +// gravitational lensing - based on https://www.shadertoy.com/view/llyyRm +// ========================================================================================== +#define S_RADIUS 0.1 +#define HALO_BRIGHTNESS .25 + +#define PI 3.14159265358979 + +vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +vec3 gravLens( sampler2D inImage, vec2 fragCoord ) +{ + vec3 fragColor = vec3(0.0); + // Normalized pixel coordinates (from 0 to 1) + vec2 uv = fragCoord; + float aspectRatio = uResolution.x/uResolution.y; + + vec2 blackHolePos = vec2( + sin(uTime) + aspectRatio, + (cos(uTime * 1.22) + sin(uTime * .57)) + ) * .2 + .5; + vec2 coord = uv - blackHolePos; + + //really cheap gravitational lensing approximation follows + float len = 1. + (length(coord) - S_RADIUS) / S_RADIUS; + + if (len < S_RADIUS) + { + fragColor = vec3(0.0); + return fragColor; + } + + vec2 k = (coord / (len * len)); + vec2 sCoord = uv - k; + + // compute halo + float haloBrightness = 0.2; + haloBrightness = pow(haloBrightness, 4.); + vec3 hsv = vec3(sCoord.x * .3, .6, haloBrightness); + vec3 haloColor = hsv2rgb(hsv); + haloBrightness = (haloColor.r + haloColor.g + haloColor.b) * HALO_BRIGHTNESS; + haloColor = haloColor * HALO_BRIGHTNESS + vec3(haloBrightness); + + fragColor = (texture2D(inImage, sCoord).rgb + haloColor); + return fragColor; +} +// ------------------------------------------------------------------------------------------ + +// ========================================================================================== +// CRT - based on https://www.shadertoy.com/view/Xtccz4 +// ========================================================================================== +vec2 crt_coords(vec2 uv, float bend) +{ + uv -= 0.5; + uv *= 2.; + uv.x *= 1. + pow(abs(uv.y)/bend, 2.); + uv.y *= 1. + pow(abs(uv.x)/bend, 2.); + + uv /= 2.02; + return uv + .5; +} + +float vignette(vec2 uv, float size, float smoothness, float edgeRounding) +{ + uv -= .5; + uv *= size; + float amount = sqrt(pow(abs(uv.x), edgeRounding) + pow(abs(uv.y), edgeRounding)); + amount = 1. - amount; + return smoothstep(0., smoothness, amount); +} + +float scanline(vec2 uv, float lines, float speed) +{ + return sin(uv.y * lines + uTime * speed); +} + +float random(vec2 uv) +{ + return fract(sin(dot(uv, vec2(15.5151, 42.2561))) * 12341.14122 * sin(uTime * 0.03)); +} + +float noise(vec2 uv) +{ + vec2 i = floor(uv); + vec2 f = fract(uv); + + float a = random(i); + float b = random(i + vec2(1.,0.)); + float c = random(i + vec2(0., 1.)); + float d = random(i + vec2(1.)); + + vec2 u = smoothstep(0., 1., f); + + return mix(a,b, u.x) + (c - a) * u.y * (1. - u.x) + (d - b) * u.x * u.y; + +} + +vec3 CRT(sampler2D inImage, vec2 inCoords) +{ + vec2 uv = inCoords; + vec2 crt_uv = crt_coords(uv, 8.); + + float s1 = scanline(uv, 1500., -10.); + float s2 = scanline(uv, 10., -3.); + + vec3 col = texture2D(inImage, crt_uv).rgb; + col.r = texture2D(inImage, crt_uv + vec2(0., 0.0025)).r; + col.g = texture2D(inImage, crt_uv).g; + col.b = texture2D(inImage, crt_uv + vec2(0., -0.0025)).b; + + col = mix(col, vec3(s1 + s2), 0.05); + col = mix(col, vec3(noise(uv * 500.)), 0.05) * vignette(uv, 1.9, .6, 8.); + + return col; +} +// ------------------------------------------------------------------------------------------ + +// ======================================================================================= +// CRTBadSignal - original at https://www.shadertoy.com/view/ltV3z1 +// ======================================================================================= +vec3 CRTBadSignal(sampler2D inImage, vec2 inCoords) +{ + vec2 uv = inCoords; + float t = uTime; + float fx = 0.001 + 25.0 * abs(sin(t) / 10.0) - mod(1.0 + t, 7.0); + const float s = 50.0; // noise intensity + float x = (floor(uv.x * 300.0)) + (floor(uv.y * 300.0 )) * (t * 0.1); + vec4 dtv = vec4(mod((mod(x, 2.0) + 1.0) * (mod(x, 0.0) + 0.0), 0.01) - 0.005) * s; + vec3 col = clamp(vec3(0.1, 0.1, 0.1) + dtv.xyz / fx, vec3(0.0), vec3(0.1)); + + col.xyz += texture2D(inImage,uv).xyz; + + return col; +} +// ------------------------------------------------------------------------------------------ + +// ======================================================================================= +// FXAA - original at https://www.shadertoy.com/view/MdyyRt +// ======================================================================================= +#define FXAA_STRENGTH 10. +vec3 FXAA(sampler2D _Tex, vec2 uv) +{ + vec3 e = vec3(1./uResolution.xy,0.); + float reducemul = 0.125;// 1. / 8.; + float reducemin = 0.0078125;// 1. / 128.; + + vec4 Or = texture2D(_Tex,uv); + vec4 LD = texture2D(_Tex,uv - e.xy); + vec4 RD = texture2D(_Tex,uv + vec2( e.x,-e.y)); + vec4 LT = texture2D(_Tex,uv + vec2(-e.x, e.y)); + vec4 RT = texture2D(_Tex,uv + e.xy); + float Or_Lum = grayscale(Or.rgb).x; + float LD_Lum = grayscale(LD.rgb).x; + float RD_Lum = grayscale(RD.rgb).x; + float LT_Lum = grayscale(LT.rgb).x; + float RT_Lum = grayscale(RT.rgb).x; + + float min_Lum = min(Or_Lum,min(min(LD_Lum,RD_Lum),min(LT_Lum,RT_Lum))); + float max_Lum = max(Or_Lum,max(max(LD_Lum,RD_Lum),max(LT_Lum,RT_Lum))); + + //x direction,-y direction + vec2 dir = vec2((LT_Lum+RT_Lum)-(LD_Lum+RD_Lum),(LD_Lum+LT_Lum)-(RD_Lum+RT_Lum)); + float dir_reduce = max((LD_Lum+RD_Lum+LT_Lum+RT_Lum)*reducemul*0.25,reducemin); + float dir_min = 1./(min(abs(dir.x),abs(dir.y))+dir_reduce); + dir = min(vec2(FXAA_STRENGTH),max(-vec2(FXAA_STRENGTH),dir*dir_min)) * e.xy; + + vec4 resultA = 0.5*(texture2D(_Tex,uv-0.166667*dir)+texture2D(_Tex,uv+0.166667*dir)); + vec4 resultB = resultA*0.5+0.25*(texture2D(_Tex,uv-0.5*dir)+texture2D(_Tex,uv+0.5*dir)); + float B_Lum = grayscale(resultB.rgb).x; + + if(B_Lum < min_Lum || B_Lum > max_Lum) + { + return resultA.rgb; + } + else + { + return resultB.rgb; + } +} +//------------------------------------------------------------------------------------------ + + +void main() +{ + float exposure = 1.0; + vec3 hdrColor = texture2D(scene, TexCoords).rgb; + vec3 bloomColor = texture2D(bloomBlur, TexCoords).rgb; + + if(bloom) + { + hdrColor += bloomColor; + } + + hdrColor *= exposure; + + // post processing effects here + // GLSL 1.20 has no switch: if/else chain, preserving the original 1/2/3 fall-through + if (uPostFX >= 1 && uPostFX <= 3) + { + hdrColor = colorBlindness(hdrColor); + } + else if (uPostFX == 4) + { + hdrColor = cloakVision(hdrColor); + } + else if (uPostFX == 5) + { + hdrColor = FXAA(scene, TexCoords); + bloomColor = FXAA(bloomBlur, TexCoords); + if(bloom) hdrColor += bloomColor; + hdrColor *= exposure; + } + else if (uPostFX == 6) + { + hdrColor = oldMovie(scene, TexCoords); + bloomColor = oldMovie(bloomBlur, TexCoords); + if(bloom) hdrColor += bloomColor; + hdrColor *= exposure; + } + else if (uPostFX == 7) + { + hdrColor = CRT(scene, TexCoords); + bloomColor = CRT(bloomBlur, TexCoords); + if(bloom) hdrColor += bloomColor; + hdrColor *= exposure; + } + else if (uPostFX == 8) + { + hdrColor = CRTBadSignal(scene, TexCoords); + bloomColor = CRT(bloomBlur, TexCoords); // yes CRT, not a typo + if(bloom) hdrColor += bloomColor; + hdrColor *= exposure; + } + else if (uPostFX == 9) + { + hdrColor = gravLens(scene, TexCoords); + bloomColor = gravLens(bloomBlur, TexCoords); + if(bloom) hdrColor += bloomColor; + hdrColor *= exposure; + } + + // apply saturation + hdrColor = max(saturationMatrix(uSaturation) * hdrColor, vec3(0.0)); + + vec3 result; + bool gammaCorrectionRequired = true; + // GLSL 1.20 has no switch: if/else chain + if (uSDRToneMapper == 0) // ACES + { + result = ACESFilm(hdrColor); + } + else if (uSDRToneMapper == 1) // AgX Punchy + { + result = AgX(hdrColor, AGX_LOOK_PUNCHY); + } + else if (uSDRToneMapper == 2) // Hejl / Burgess - Dawson filmic + { + result = hejlBurgessDawson(desaturateHighlights(hdrColor)); + gammaCorrectionRequired = false; + } + else if (uSDRToneMapper == 3) // Uncharted 2 + { + result = uc2(desaturateHighlights(hdrColor)); + } + else if (uSDRToneMapper == 4) // Uchimura + { + result = uchimura(desaturateHighlights(hdrColor)); + } + else if (uSDRToneMapper == 5) // Reinhard + { + result = reinhard(desaturateHighlights(hdrColor)); + } + else // no tone map - mainly for testing + { + result = hdrColor; + } + + // gamma correction + if (gammaCorrectionRequired) result = pow(result, vec3(1.0 / 2.2)); + +#if OO_DITHER + result += random(TexCoords + uTime) / 255.0; +#endif + + //FragColor = vec4(bloomColor, 1.0); + gl_FragColor = vec4(result, 1.0); +} +#endif diff --git a/Resources/Shaders/oolite-final.vertex b/Resources/Shaders/oolite-final.vertex index 657c2dbd5d..793392371a 100644 --- a/Resources/Shaders/oolite-final.vertex +++ b/Resources/Shaders/oolite-final.vertex @@ -1,3 +1,4 @@ +#ifndef OO_LEGACY_GL layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoords; @@ -7,4 +8,16 @@ void main() { TexCoords = aTexCoords; gl_Position = vec4(aPos, 1.0); -} \ No newline at end of file +} +#else +attribute vec3 aPos; +attribute vec2 aTexCoords; + +varying vec2 TexCoords; + +void main() +{ + TexCoords = aTexCoords; + gl_Position = vec4(aPos.xy, 0.0, 1.0); +} +#endif diff --git a/Resources/Shaders/oolite-texture.fragment b/Resources/Shaders/oolite-texture.fragment index 5f66bdc144..7cfb3a8bcc 100644 --- a/Resources/Shaders/oolite-texture.fragment +++ b/Resources/Shaders/oolite-texture.fragment @@ -1,3 +1,4 @@ +#ifndef OO_LEGACY_GL layout (location = 0) out vec4 out_color; layout (location = 1) out vec4 bright_color; @@ -21,3 +22,26 @@ void main() else bright_color = vec4(0.0, 0.0, 0.0, 1.0); } +#else +varying vec2 v_texture_coordinate; + +uniform sampler2D image; + +void main() +{ + const float bloomThreshold = 1.0; + const float bloomStrength = 0.05; + vec4 sampled = texture2D(image, v_texture_coordinate); + +#ifdef OO_POSTFX_MRT + float brightness = dot(sampled.rgb, vec3(0.2126, 0.7152, 0.0722)); + gl_FragData[0] = sampled; + if (brightness > bloomThreshold) + gl_FragData[1] = vec4(sampled.rgb * bloomStrength, 1.0); + else + gl_FragData[1] = vec4(0.0, 0.0, 0.0, 1.0); +#else + gl_FragColor = sampled; +#endif +} +#endif diff --git a/Resources/Shaders/oolite-texture.vertex b/Resources/Shaders/oolite-texture.vertex index 257f906cd8..bfead2fa27 100644 --- a/Resources/Shaders/oolite-texture.vertex +++ b/Resources/Shaders/oolite-texture.vertex @@ -1,3 +1,4 @@ +#ifndef OO_LEGACY_GL layout(location = 0) in vec2 vert_position; layout(location = 1) in vec2 texture_coordinate; @@ -12,3 +13,15 @@ void main() gl_Position = vec4(vert_position, 0.0, 1.0); vs_out.texture_coordinate = texture_coordinate; } +#else +attribute vec2 vert_position; +attribute vec2 texture_coordinate; + +varying vec2 v_texture_coordinate; + +void main() +{ + gl_Position = vec4(vert_position.xy, 0.0, 1.0); + v_texture_coordinate = texture_coordinate; +} +#endif diff --git a/ShellScripts/Darwin/codesign.sh b/ShellScripts/Darwin/codesign.sh new file mode 100755 index 0000000000..a67d9db345 --- /dev/null +++ b/ShellScripts/Darwin/codesign.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# darwin release gate: Developer ID distribution signing. +# +# Signs every Mach-O inside an .app bundle, then seals the bundle, with a +# Developer ID Application identity for distribution + notarization (see +# notarize.sh for the notarization half). This is a STANDALONE release gate, +# not part of the build: the build's post-build (post_build_fn.sh) ad-hoc +# signs every bundle it produces, and any later byte edit invalidates an +# arm64 signature — so distribution signing always runs last, on a finished +# bundle. +# +# Environment: +# MACOS_SIGNING_IDENTITY Developer ID Application identity, exactly as +# shown by `security find-identity -v -p +# codesigning` (the full name or its 40-char SHA-1). +# Apple Development identities can sign locally but +# can NEVER be notarized; the ad-hoc identity '-' +# is refused here — ad-hoc is the local-run path, +# not a release pass. +# +# Entitlements: deliberately none. Oolite embeds SpiderMonkey 1.8.5 built +# interpreter-only (no JIT), so hardened runtime needs no +# com.apple.security.cs exceptions; granting any would only widen the +# attack surface. +# +# Enumeration: nested Mach-O first, bundle seal last. Every regular file is +# probed with file(1) and only "Mach-O" answers are signed — macOS 26 find +# rejects the any-bit '-perm /111' spelling ("illegal mode string"), and an +# executability filter is the wrong tool anyway (it misses LTO'd/stripped +# oddities and signs data files that merely carry +x). Per-file signatures +# are atomic (one Mach-O, no nested code), so file ORDER is irrelevant; +# only the composite signature — the bundle seal — must come last, and does. +# +# Fail-hard discipline: set -euo pipefail, no `|| true`, no stderr +# suppression. A silently skipped signature is invisible until it is +# somebody else's machine: SIGKILL at launch, or a notarization rejection +# hours later. +# +# Usage: codesign.sh [path/to/Oolite.app] +# (default: build/meson_deployment/oolite.app) + +set -euo pipefail + +die() { + echo "❌ $1" >&2 + exit 1 +} + +identity="${MACOS_SIGNING_IDENTITY:-}" +if [[ -z "$identity" ]]; then + cat >&2 <<'EOF' +❌ MACOS_SIGNING_IDENTITY is not set! + Export the Developer ID Application identity to sign with, e.g.: + export MACOS_SIGNING_IDENTITY="Developer ID Application: ()" + (Requires Apple Developer Program membership. Without it this gate is + BLOCKED: ad-hoc sign locally and record the gate as blocked instead.) +EOF + exit 1 +fi +if [[ "$identity" == "-" ]]; then + echo "❌ Refusing the ad-hoc identity '-' — codesign.sh is the distribution gate." >&2 + echo " Ad-hoc signing is done directly (codesign --force --sign - )," >&2 + echo " never through this script: it must not be mistaken for a release pass." >&2 + exit 1 +fi +case "$identity" in + *"Developer ID Application"*) ;; + *) + echo "⚠️ '$identity' does not look like a Developer ID Application identity." >&2 + echo " It signs, but notarization (notarize.sh) will refuse it." >&2 + ;; +esac + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo_root=$(cd "$script_dir/../.." && pwd) +bundle="${1:-$repo_root/build/meson_deployment/oolite.app}" + +[[ -d "$bundle" ]] || die "Bundle '$bundle' does not exist — build it first (./mk.sh build deployment)." +[[ -f "$bundle/Contents/Info.plist" ]] || die "'$bundle' has no Contents/Info.plist — not an .app bundle." +command -v codesign >/dev/null || die "codesign not found (install Xcode command line tools)." +command -v file >/dev/null || die "file(1) not found." + +# The identity must already be in a keychain codesign can reach; matching +# here turns a typo into an immediate, quoted error instead of a codesign +# failure mid-loop. grep -F: identity names contain regex metacharacters. +if ! security find-identity -v -p codesigning | grep -Fq -- "$identity"; then + echo "❌ Signing identity not found among valid codesigning identities:" >&2 + security find-identity -v -p codesigning >&2 + die "Export MACOS_SIGNING_IDENTITY exactly as listed above." +fi + +# NUL-delimited manifest via a temp file: a pipe would hide find's exit +# status from set -e (the loop would silently end on a traversal error). +macho_list=$(mktemp) +trap 'rm -f "$macho_list"' EXIT +if ! find "$bundle" -type f -print0 > "$macho_list"; then + die "find could not enumerate '$bundle' — bundle is incomplete or unreadable!" +fi + +signed=0 +while IFS= read -r -d '' macho_file; do + if ! file_desc=$(file -b -- "$macho_file"); then + die "file(1) failed on '$macho_file'!" + fi + case "$file_desc" in + *Mach-O*) ;; + *) continue ;; + esac + echo "ℹ️ Signing Mach-O: $macho_file" + if ! codesign --force --options runtime --timestamp --sign "$identity" "$macho_file"; then + die "Failed to sign '$macho_file'!" + fi + signed=$((signed + 1)) +done < "$macho_list" + +if [[ "$signed" -eq 0 ]]; then + die "No Mach-O found under '$bundle' — wrong path or an empty bundle!" +fi +echo "ℹ️ Signed $signed nested Mach-O file(s); sealing the bundle." + +if ! codesign --force --options runtime --timestamp --sign "$identity" "$bundle"; then + die "Failed to seal '$bundle'!" +fi +if ! codesign --verify --deep --strict --verbose=2 "$bundle"; then + die "Signature verification failed for '$bundle'!" +fi + +echo "✅ '$bundle' signed with hardened runtime + timestamped Developer ID — ready for notarize.sh" diff --git a/ShellScripts/Darwin/install_mozilla_js.sh b/ShellScripts/Darwin/install_mozilla_js.sh new file mode 100755 index 0000000000..add4105cec --- /dev/null +++ b/ShellScripts/Darwin/install_mozilla_js.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Install the SpiderMonkey js185 arm64-macOS static library for Oolite. +# Darwin twin of ShellScripts/Linux/install_mozilla_js.sh: same parameter +# contract ("system", "home", or "build"), same target layout. +# Parameter one: "system", "home", or "build" (defaults to home). +# Parameter two: escalate command (defaults to sudo for 'system'). + +run_script() { + local script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + + source "$script_dir/../Linux/dep_location_fn.sh" + local lib_subdir target escalate + dep_location lib_subdir target escalate "mozilla_js" $1 $2 + + local release_url="https://github.com/OoliteProject/mozillajs-macos/releases/download/0.0.1/mozjs-js185-macos-arm64.tar.gz" + + echo "Installing Mozilla JS library (arm64, interpreter-only) to $target" + [[ -n "$escalate" ]] && echo "Using escalation: $escalate" + + local temp_staging=$(mktemp -d) + trap 'rm -rf "$temp_staging"' EXIT + + if ! curl -fL "$release_url" | tar -xz -C "$temp_staging"; then + echo "❌ Mozilla JS library download or extract failed!" >&2 + return 1 + fi + + if ! [[ -d "$temp_staging/include" ]]; then + echo "❌ Mozilla JS library extract empty!" >&2 + return 1 + fi + + if ! $escalate cp -R "$temp_staging/include/"* "$target/include/"; then + echo "❌ Mozilla JS library header install failed!" >&2 + return 1 + fi + mkdir -p "$target/$lib_subdir" + if ! $escalate cp -R "$temp_staging/lib/"* "$target/$lib_subdir/"; then + echo "❌ Mozilla JS library install failed!" >&2 + return 1 + fi + + echo "✅ Installation complete." +} + +run_script "$@" +status=$? + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + exit $status +fi diff --git a/ShellScripts/Darwin/notarize.sh b/ShellScripts/Darwin/notarize.sh new file mode 100755 index 0000000000..b94a1ffd09 --- /dev/null +++ b/ShellScripts/Darwin/notarize.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# darwin release gate: notarization, stapling and the Gatekeeper verdict. +# +# Ship path: ditto-zip the signed bundle, submit to Apple notary service and +# wait, staple the ticket onto the bundle, then prove Gatekeeper accepts it. +# Run AFTER codesign.sh — notarization consumes the bundle's Developer ID +# signature; it cannot create one. +# +# Environment: +# MACOS_NOTARY_PROFILE Keychain profile name for notarytool credentials +# (created once with `xcrun notarytool store-credentials`). +# +# Fail-hard discipline: set -euo pipefail, no `|| true`, no stderr +# suppression. Every step below is a gate; a passed step is printed, a +# failed step aborts with the fix in the message. There is no such thing as +# a "mostly notarized" app. +# +# Usage: notarize.sh [path/to/Oolite.app] +# (default: build/meson_deployment/oolite.app) + +set -euo pipefail + +die() { + echo "❌ $1" >&2 + exit 1 +} + +profile="${MACOS_NOTARY_PROFILE:-}" +if [[ -z "$profile" ]]; then + cat >&2 <<'EOF' +❌ MACOS_NOTARY_PROFILE is not set! + Store App Store Connect API credentials once: + xcrun notarytool store-credentials --key ... --issuer ... --key-id ... + then export MACOS_NOTARY_PROFILE=. + (Requires Apple Developer Program membership. Without it this gate is + BLOCKED: record it as blocked instead of shipping unsigned.) +EOF + exit 1 +fi + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo_root=$(cd "$script_dir/../.." && pwd) +bundle="${1:-$repo_root/build/meson_deployment/oolite.app}" + +[[ -d "$bundle" ]] || die "Bundle '$bundle' does not exist — build and sign it first." +[[ -f "$bundle/Contents/Info.plist" ]] || die "'$bundle' has no Contents/Info.plist — not an .app bundle." +command -v ditto >/dev/null || die "ditto not found." +command -v xcrun >/dev/null || die "xcrun not found (install Xcode command line tools)." +command -v spctl >/dev/null || die "spctl not found." + +# Pre-flight 1: the existing signature must verify before we spend a +# notary round-trip (minutes) on something codesign can reject in seconds. +if ! codesign --verify --deep --strict "$bundle"; then + die "'$bundle' does not pass codesign --verify — run codesign.sh first." +fi + +# Pre-flight 2: the signature must be a Developer ID Application one. +# Ad-hoc and Apple Development signatures cannot be notarized; catching it +# here keeps the failure local and immediate instead of a notary rejection +# after the wait. codesign writes its display output to stderr by design; +# 2>&1 MERGES it for inspection, nothing is suppressed. +sig_info=$(codesign --display --verbose=2 "$bundle" 2>&1) || die "codesign --display failed on '$bundle'." +if ! printf '%s\n' "$sig_info" | grep -q "Authority=Developer ID Application"; then + echo "❌ '$bundle' is not signed with a Developer ID Application identity!" >&2 + printf '%s\n' "$sig_info" >&2 + die "Re-sign with codesign.sh (MACOS_SIGNING_IDENTITY) before notarizing." +fi + +# Notarytool takes a zip, not a bundle; --keepParent keeps Oolite.app as the +# archive root so the notarized artifact unzips exactly as it will ship. +zip_path="${bundle}.zip" +rm -f "$zip_path" +if ! ditto -c -k --keepParent "$bundle" "$zip_path"; then + die "ditto could not archive '$bundle'!" +fi +echo "ℹ️ Archive for submission: $zip_path" + +if ! xcrun notarytool submit "$zip_path" --wait --keychain-profile "$profile"; then + cat >&2 <<'EOF' +❌ Notarization FAILED or did not complete! + Fetch Apple's processing log for the submission id printed above: + xcrun notarytool log --keychain-profile + Fix the reported issue, re-run codesign.sh if the bundle changed, then retry. +EOF + exit 1 +fi + +if ! xcrun stapler staple "$bundle"; then + die "stapler could not attach the notarization ticket to '$bundle'!" +fi +if ! xcrun stapler validate "$bundle"; then + die "stapler rejected the ticket on '$bundle'!" +fi + +# Final verdict: the Gatekeeper policy evaluation a customer's machine runs. +if ! spctl -a -vv "$bundle"; then + die "Gatekeeper (spctl) REJECTED '$bundle'!" +fi + +echo "✅ '$bundle' notarized, stapled and Gatekeeper-accepted — shippable" diff --git a/ShellScripts/Darwin/post_build_fn.sh b/ShellScripts/Darwin/post_build_fn.sh new file mode 100755 index 0000000000..e7f4787b27 --- /dev/null +++ b/ShellScripts/Darwin/post_build_fn.sh @@ -0,0 +1,211 @@ +#!/bin/bash +# darwin post-build: bundle completion. +# +# Sourced by ShellScripts/common/post_build.sh; defines post_build_platform(). +# Installs the templated Info.plist (meson-substituted, see root meson.build), +# the app icon, bundles every non-system dylib into Contents/Frameworks with +# @rpath rewiring, strips Homebrew/MacPorts rpaths, and ad-hoc re-signs every +# Mach-O — any byte edit invalidates arm64 code signatures, and a stale one +# means SIGKILL at launch. + +# Load-command prefixes that must be bundled: Homebrew, MacPorts and +# /usr/local source installs. System paths (/System/*, /usr/lib/*) stay. +_COPY_DEPS_PREFIX='^(/opt/homebrew|/usr/local|/opt/local)/' + +# copy_deps +# +# Recursive dependency bundler. For every /opt/homebrew, /usr/local or +# /opt/local load command of : rewrite it to @rpath/, copy the +# dylib into $COPY_DEPS_FW, retarget its id, give the copy an @loader_path +# rpath so its own (rewritten) @rpath loads resolve inside the bundle, strip +# brew-prefixed rpaths from the copy, then recurse into the copy. +# +# Globals set by post_build_platform() before the first call: +# COPY_DEPS_FW Contents/Frameworks directory (created) +# COPY_DEPS_DONE space-separated list of basenames already visited this run +# (bash-3.2-safe dedup: no associative arrays) +# +# The load-command rewrite happens BEFORE the dedup check: the main binary is +# freshly linked on every build, so its load commands are always absolute brew +# paths again even when every dylib is already copied. Dedup only prevents +# re-copying/recursion. +copy_deps() { + # Every per-call scalar is local: copy_deps recurses into the copies, and + # shared globals (_cd_target first among them) would be clobbered by the + # callee — the caller's remaining -change calls would silently no-op + # against the wrong file (install_name_tool -change exits 0 when the old + # name is absent). COPY_DEPS_FW / COPY_DEPS_DONE stay global on purpose. + local _cd_target="$1" + local _cd_loads _cd_deps _cd_dep _cd_base _cd_rp + if ! _cd_loads=$(otool -L "$_cd_target"); then + echo "❌ copy_deps: 'otool -L $_cd_target' failed!" >&2 + return 1 + fi + # grep exits non-zero on zero matches — the normal terminal case of the + # recursion (all-system deps), not an error. + _cd_deps=$(printf '%s\n' "$_cd_loads" \ + | awk 'NR>1 {print $1}' \ + | grep -E "$_COPY_DEPS_PREFIX" || true) + for _cd_dep in $_cd_deps; do + _cd_base=$(basename "$_cd_dep") + # Rewrite this dependent's load command on every visit (see above). + if ! install_name_tool -change "$_cd_dep" "@rpath/$_cd_base" "$_cd_target"; then + echo "❌ copy_deps: could not rewrite '$_cd_dep' in '$_cd_target'!" >&2 + return 1 + fi + case " $COPY_DEPS_DONE " in + *" $_cd_base "*) continue ;; + esac + COPY_DEPS_DONE="$COPY_DEPS_DONE $_cd_base" + # -L: the keg path may be a symlink into the Cellar; copy the target. + if ! cp -fL "$_cd_dep" "$COPY_DEPS_FW/$_cd_base"; then + echo "❌ copy_deps: could not copy '$_cd_dep'!" >&2 + return 1 + fi + if ! install_name_tool -id "@rpath/$_cd_base" "$COPY_DEPS_FW/$_cd_base"; then + echo "❌ copy_deps: could not set id of '$_cd_base'!" >&2 + return 1 + fi + # The copy resolves its own @rpath loads against ITS rpaths, not the + # executable's: point it at its own directory. Add only if absent so + # re-runs on existing copies stay quiet-and-idempotent. + if ! otool -l "$COPY_DEPS_FW/$_cd_base" \ + | awk '$1 == "cmd" && $2 == "LC_RPATH" { getline; getline; print $2 }' \ + | grep -Fxq "@loader_path"; then + if ! install_name_tool -add_rpath "@loader_path" "$COPY_DEPS_FW/$_cd_base"; then + echo "❌ copy_deps: could not add @loader_path rpath to '$_cd_base'!" >&2 + return 1 + fi + fi + # Brew rpaths must never win over the bundle (they would silently + # resolve loads outside it on machines that have Homebrew). + for _cd_rp in $(otool -l "$COPY_DEPS_FW/$_cd_base" \ + | awk '$1 == "cmd" && $2 == "LC_RPATH" { getline; getline; print $2 }' \ + | grep -E "$_COPY_DEPS_PREFIX" || true); do + if ! install_name_tool -delete_rpath "$_cd_rp" "$COPY_DEPS_FW/$_cd_base"; then + echo "❌ copy_deps: could not delete rpath '$_cd_rp' from '$_cd_base'!" >&2 + return 1 + fi + done + copy_deps "$COPY_DEPS_FW/$_cd_base" || return 1 + done + # Post-condition of this function: no bundling-prefix load command may + # survive on the target (a silent install_name_tool no-op would fail this). + if otool -L "$_cd_target" | awk 'NR>1 {print $1}' | grep -qE "$_COPY_DEPS_PREFIX"; then + echo "❌ copy_deps: '$_cd_target' still loads dylibs outside the bundle after rewriting!" >&2 + return 1 + fi + return 0 +} + +post_build_platform() { + local progpath="$1" + local progdir="$2" + local resourcesdir="$3" + local strip_bin="$4" + local info_plist="$6" # templated plist from meson; gnustep_folder ($5) unused on darwin + + local binname + binname=$(basename "$progpath") + + # Upgrades from the flat layout: a stray Resources/ next to Contents/ + # makes codesign reject the bundle ("unsealed contents"). + rm -rf "$progdir/Resources" + + # Real Info.plist, substituted by meson from installers/macos/Info.plist.in + # (CFBundleShortVersionString @ver_full@; CFBundleVersion @ver_quad@ — the + # dotted quad ResourceManager's OXP version gate compares against the core + # manifest's required_oolite_version). + if [[ -z "$info_plist" ]] || [[ ! -f "$info_plist" ]]; then + echo "❌ Substituted Info.plist '$info_plist' missing — meson wiring broken!" >&2 + return 1 + fi + if ! cp -f "$info_plist" "$progdir/Contents/Info.plist"; then + echo "❌ Failed to install Info.plist into the bundle!" >&2 + return 1 + fi + if ! plutil -lint "$progdir/Contents/Info.plist"; then + echo "❌ Bundle Info.plist failed plutil -lint!" >&2 + return 1 + fi + + if [[ "$strip_bin" == "yes" ]]; then + # Keep debug info in a .dSYM bundle next to the .app, then strip the + # binary (BSD strip has no GNU objcopy debuglink equivalent). + # + # NEXT TO, not inside: codesign refuses to sign a bundle with any + # bundle-root entry beside Contents/ ("unsealed contents present in + # the bundle root"), and notarization rejects the same layouts — the + # first deployment build failed exactly there. Also remove the + # legacy in-bundle dSYM this script produced before the fix (the + # bundle directory survives across builds). + rm -rf "$progdir/$binname.dSYM" + if ! dsymutil "$progpath" -o "$progdir/../$binname.dSYM"; then + echo "❌ Failed to extract debug info from '$progpath'!" >&2 + return 1 + fi + if ! strip -S "$progpath"; then + echo "❌ Failed to strip '$progpath'!" >&2 + return 1 + fi + fi + + # App icon: installers/macos/oolite.icns (generated with iconutil from the + # checked-in Resources/oolite-icon.icns master). Copied here — AFTER the + # shared post_build's quoted-glob 'rm -f "$resourcesdir/*.icns"' (a + # literal-name no-op) has already run — so it survives every build. + local fn_dir + fn_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + if ! cp -f "$fn_dir/../../installers/macos/oolite.icns" "$resourcesdir/oolite.icns"; then + echo "❌ Failed to install the app icon into the bundle!" >&2 + return 1 + fi + + # Dependency bundling: everything non-system into Contents/Frameworks. + local bundle_fw + bundle_fw="$progdir/Contents/Frameworks" + mkdir -p "$bundle_fw" + COPY_DEPS_FW="$bundle_fw" + COPY_DEPS_DONE="" + if ! copy_deps "$progpath"; then + return 1 + fi + # Same for the binary itself: brew rpaths would resolve @rpath loads + # outside the bundle (they precede @executable_path/../Frameworks in the + # load-command list), so the "hidden Homebrew" launch would still leak. + local _rp + for _rp in $(otool -l "$progpath" \ + | awk '$1 == "cmd" && $2 == "LC_RPATH" { getline; getline; print $2 }' \ + | grep -E "$_COPY_DEPS_PREFIX" || true); do + if ! install_name_tool -delete_rpath "$_rp" "$progpath"; then + echo "❌ Could not delete rpath '$_rp' from the binary!" >&2 + return 1 + fi + done + echo "ℹ️ Bundled dylibs:$COPY_DEPS_DONE" + + # Ad-hoc re-sign every Mach-O under Contents (dylibs first, main binary + # last), then seal the bundle. Enumerated explicitly: macOS 26 find + # rejects the any-bit '-perm /111' spelling ("illegal mode string"), and + # these two locations are the only Mach-O code in the bundle. NO + # 2>/dev/null, NO || true: a silently skipped signature means SIGKILL at + # launch (or notarization rejection hours later in Task 11). + local macho_file + for macho_file in "$bundle_fw"/*.dylib "$progpath"; do + [ -e "$macho_file" ] || continue # empty-Frameworks glob, not an error + if ! codesign --force --sign - "$macho_file"; then + echo "❌ Failed to ad-hoc re-sign '$macho_file'!" >&2 + return 1 + fi + done + if ! codesign --force --sign - "$progdir"; then + echo "❌ Failed to ad-hoc seal '$progdir'!" >&2 + return 1 + fi + if ! codesign --verify --deep --strict "$progdir"; then + echo "❌ Bundle signature verification failed for '$progdir'!" >&2 + return 1 + fi + + return 0 +} diff --git a/ShellScripts/Linux/post_build_fn.sh b/ShellScripts/Linux/post_build_fn.sh new file mode 100755 index 0000000000..b59883fde1 --- /dev/null +++ b/ShellScripts/Linux/post_build_fn.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Linux post-build: stripping (with GNU debuglink tooling) and GNUstep +# dependency copying. +# +# Sourced by ShellScripts/common/post_build.sh; defines post_build_platform(). + +post_build_platform() { + local progpath="$1" + local progdir="$2" + local resourcesdir="$3" + local strip_bin="$4" + local gnustep_folder="$5" + + local appname + appname=$(basename "$progpath") + + # Strip binary if requested + if [[ "$strip_bin" == "yes" ]]; then + # Extract symbols to file + local debugpath="$progdir/$appname.debug" + objcopy --only-keep-debug "$progpath" "$debugpath" + # Compress the debug sections in the symbol file + objcopy --compress-debug-sections=zlib-gnu "$debugpath" + # strip the binary + strip -R .comment "$progpath" + # Add the debug link + objcopy --add-gnu-debuglink="$debugpath" "$progpath" + else + # Compress the debug sections in the binary + objcopy --compress-debug-sections=zlib-gnu "$progpath" + fi + + # Copy Linux-specific wrapper script + cp -fu ShellScripts/Linux/run_oolite.sh "$progdir" + local gnustep_conf="$gnustep_folder/etc/GNUstep/GNUstep.conf" + if [ ! -f "$gnustep_conf" ] && [ "$gnustep_folder" = "/usr" ] && [ -f "/etc/GNUstep/GNUstep.conf" ]; then + gnustep_conf="/etc/GNUstep/GNUstep.conf" + fi + install -D "$gnustep_conf" "$resourcesdir/GNUstep.conf.orig" || { echo "❌ GNUstep config" >&2; return 1; } + + # If we're using GNUstep libraries that aren't in a system folder copy them + ldd "$progpath" | \ + grep -E "libgnustep-base|libobjc\.so\." | \ + grep -vE "^[[:space:]]*.*=>[[:space:]]*/(usr/(local/)?|lib(64)?/)" | \ + awk '{print $3}' | \ + xargs -I {} cp -Lrfu {} "$progdir/" + + return 0 +} diff --git a/ShellScripts/Windows/post_build_fn.sh b/ShellScripts/Windows/post_build_fn.sh new file mode 100755 index 0000000000..9de1af089b --- /dev/null +++ b/ShellScripts/Windows/post_build_fn.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Windows post-build: stripping and DLL dependency copying. +# +# Sourced by ShellScripts/common/post_build.sh; defines post_build_platform(). + +post_build_platform() { + local progpath="$1" + local progdir="$2" + local resourcesdir="$3" # unused on Windows + local strip_bin="$4" + # 5th argument (gnustep_folder) is not meaningful on Windows. + + # Strip binary if requested + if [[ "$strip_bin" == "yes" ]]; then + # Windows: Standard GNU strip is safest for PE/COFF + strip "$progpath" + fi + + # Determine and copy DLL dependencies + local unix_prefix + unix_prefix=$(cygpath -u "$MINGW_PREFIX") + ldd "$progpath" | grep "$unix_prefix" | awk '{print $3}' | xargs -I {} cp -rfu {} "$progdir" + + return 0 +} diff --git a/ShellScripts/common/get_build_date_fn.sh b/ShellScripts/common/get_build_date_fn.sh index 4413809d2d..26d5bcc547 100755 --- a/ShellScripts/common/get_build_date_fn.sh +++ b/ShellScripts/common/get_build_date_fn.sh @@ -12,28 +12,59 @@ if [[ "$SUITE_PARENT" != "$ALLOWED_SCRIPT" ]]; then fi unset SUITE_PARENT ALLOWED_SCRIPT +# One-time date-flavour detection, done at source time: GNU date parses +# "@" with -d; BSD date (macOS) reads epochs with -r and parses +# fixed-format strings with -j -f. Linux keeps the GNU paths throughout, so +# its output is unchanged. +if date -u -d "@0" "+%Y" > /dev/null 2>&1; then + OOL_DATE_GNU=1 +else + OOL_DATE_GNU=0 +fi + +# _ool_date_fmt EPOCH FORMAT — format an epoch second as UTC. +_ool_date_fmt() { + if [[ "$OOL_DATE_GNU" == 1 ]]; then + date -u -d "@$1" "$2" + else + date -u -r "$1" "$2" + fi +} + +# _ool_date_to_epoch "YYYY-MM-DD HH:MM" — parse a naive UTC date string. +_ool_date_to_epoch() { + if [[ "$OOL_DATE_GNU" == 1 ]]; then + date -u -d "$1" "+%s" + else + date -u -j -f "%Y-%m-%d %H:%M" "$1" "+%s" + fi +} + get_build_date() { - local -n _cpp_date="$1" - local -n _app_date="$2" - local -n _buildtime="$3" - local -n _builder="$4" + # bash 3.2 has no namerefs: the caller passes variable NAMES and results + # are written back with printf -v. + local cpp_date_var="$1" + local app_date_var="$2" + local buildtime_var="$3" + local builder_var="$4" local buildtime="$5" - if [[ -z "$buildtime" ]]; then - local getversion_timestamp=$(git log -1 --format=%ct) - _buildtime=$(date -u -d "@$getversion_timestamp" "+%Y.%m.%d %H:%M") - else - _buildtime="$buildtime" + local getversion_timestamp + getversion_timestamp=$(git log -1 --format=%ct) + buildtime=$(_ool_date_fmt "$getversion_timestamp" "+%Y.%m.%d %H:%M") fi - local clean_date="${_buildtime//./-}" - _cpp_date=$(date -u -d "$clean_date" +"%b%e %Y") - _app_date=$(date -u -d "$clean_date" +"%Y-%m-%d") + local clean_date="${buildtime//./-}" + local epoch + epoch=$(_ool_date_to_epoch "$clean_date") + printf -v "$cpp_date_var" '%s' "$(_ool_date_fmt "$epoch" +"%b%e %Y")" + printf -v "$app_date_var" '%s' "$(_ool_date_fmt "$epoch" +"%Y-%m-%d")" + printf -v "$buildtime_var" '%s' "$buildtime" if [[ "$GITHUB_REPOSITORY" == "OoliteProject/oolite" ]]; then - _builder="OoliteProject" + printf -v "$builder_var" '%s' "OoliteProject" else - _builder="unknown" + printf -v "$builder_var" '%s' "unknown" fi -} \ No newline at end of file +} diff --git a/ShellScripts/common/get_version.sh b/ShellScripts/common/get_version.sh index f774abcfa5..f7a0ae425a 100755 --- a/ShellScripts/common/get_version.sh +++ b/ShellScripts/common/get_version.sh @@ -4,7 +4,15 @@ # -if [[ -v MINGW_PREFIX ]]; then +# Derive the repository root from this script's own location +# (/ShellScripts/common/) and normalize the working directory to it. +# Launchers like macOS Homebrew meson (run from Python.app) do not guarantee a +# repo cwd nor inherit GIT_DIR/GIT_WORK_TREE; under mk.sh on Linux this cd is +# a no-op (mk.sh already pushes to the repo root). +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$REPO_ROOT" || exit 1 + +if [[ -n "${MINGW_PREFIX+x}" ]]; then # bash-3.2-safe replacement for `[[ -v MINGW_PREFIX ]]` (needs bash >= 4.3) WIN_PID=$(ps -p $$ | awk 'NR>1 {print $4}') PARENT_PROCESS=$(powershell.exe -Command " \$parentId = (gwmi Win32_Process -Filter 'ProcessId = $WIN_PID').ParentProcessId @@ -18,7 +26,10 @@ if [[ -v MINGW_PREFIX ]]; then else PARENT_PROCESS=$(ps -p $PPID -o comm= 2>/dev/null || true) fi -if [[ "$PARENT_PROCESS" != "meson" ]] || [[ -z "$MESON_BUILD_ROOT" ]]; then +# Accept either meson signal: MESON_BUILD_ROOT (set by meson itself) or the +# parent process comm. The comm check alone never matches on macOS, where +# Homebrew meson runs under Python.app, so the parent comm is not "meson". +if [[ -z "$MESON_BUILD_ROOT" ]] && [[ "$PARENT_PROCESS" != "meson" ]]; then SUITE_PARENT=$(basename "${BASH_SOURCE[1]}") # Get the name of the script that is sourcing this file ALLOWED_SCRIPT="create_flatpak_fn.sh" # Define the ONLY script allowed to source this if [[ "$SUITE_PARENT" != "$ALLOWED_SCRIPT" ]]; then @@ -39,9 +50,9 @@ run_script() { fi source "ShellScripts/common/get_build_date_fn.sh" - local output_ver_githash=$(git rev-parse --short=7 HEAD) + local output_ver_githash=$(git -C "$REPO_ROOT" rev-parse --short=7 HEAD) local dirty_suffix="" - git diff --quiet || dirty_suffix="-dirty" + git -C "$REPO_ROOT" diff --quiet || dirty_suffix="-dirty" local lookup_hash="${output_ver_githash}${dirty_suffix}" local output_ver_full="" local output_buildtime="" @@ -62,23 +73,16 @@ run_script() { if [[ -z "$output_ver_full" ]]; then local exact_tag="" # Check for an exact Git tag first on a clean tree if [[ -z "$dirty_suffix" ]]; then - exact_tag=$(git describe --tags --exact-match HEAD 2>/dev/null) + exact_tag=$(git -C "$REPO_ROOT" describe --tags --exact-match HEAD 2>/dev/null) fi if [[ -n "$exact_tag" ]]; then output_ver_full="$exact_tag" else - if ! command -v gitversion &> /dev/null; then # exact tag didn't hit, use gitversion for ver_full - echo "❌ gitversion binary not found!" >&2 - exit 1 - fi - local gitversion_json=$(gitversion) # Run gitversion and get json output - local ver_semver=$(echo "$gitversion_json" | jq -r '.SemVer') - if [[ -z "$dirty_suffix" ]]; then - output_ver_full="$ver_semver" - else - local ver_uncommitted=$(echo "$gitversion_json" | jq -r '.UncommittedChanges') - output_ver_full="${ver_semver}+dirty.${ver_uncommitted}" - fi + # gitversion-free fallback (the gitversion binary is not available + # everywhere): nearest tag, commit distance since it, and a -dirty + # marker on modified trees; --always degrades to a short hash when + # no tag exists at all. + output_ver_full=$(git -C "$REPO_ROOT" describe --tags --always --dirty) fi fi @@ -91,16 +95,19 @@ run_script() { [[ -z "$ver_rev" ]] && ver_rev="0" if [[ -z "$dirty_suffix" ]]; then # Use git for other metrics for clean repository - local closest_tag=$(git describe --tags --abbrev=0 2>/dev/null) # Derive distance from closest Git tag + local closest_tag=$(git -C "$REPO_ROOT" describe --tags --abbrev=0 2>/dev/null) # Derive distance from closest Git tag local ver_dist="0" if [[ -n "$closest_tag" ]]; then - ver_dist=$(git rev-list --count "${closest_tag}..HEAD") + ver_dist=$(git -C "$REPO_ROOT" rev-list --count "${closest_tag}..HEAD") else - ver_dist=$(git rev-list --count HEAD) + ver_dist=$(git -C "$REPO_ROOT" rev-list --count HEAD) fi output_ver_quad="$ver_maj.$ver_min.$ver_rev.$ver_dist" else - local ver_uncommitted=$(git status --porcelain 2>/dev/null | wc -l) # Dirty repo: get uncommitted file count + # Dirty repo: get uncommitted file count. BSD wc pads its output with + # spaces, which would corrupt the version quad; the arithmetic + # expansion strips them (a no-op on GNU wc). + local ver_uncommitted=$(($(git -C "$REPO_ROOT" status --porcelain 2>/dev/null | wc -l))) output_ver_quad="$ver_maj.$ver_min.$ver_rev.$ver_uncommitted" fi diff --git a/ShellScripts/common/post_build.sh b/ShellScripts/common/post_build.sh index 4a65d39730..44ad55d99a 100755 --- a/ShellScripts/common/post_build.sh +++ b/ShellScripts/common/post_build.sh @@ -1,5 +1,11 @@ #!/bin/bash -# Processes Oolite data files after compilation +# Processes Oolite data files after compilation. +# +# Platform-specific work (binary stripping, dependency copying, bundle +# layout) lives in per-platform function scripts sourced from here — +# ShellScripts/{Darwin,Linux,Windows}/post_build_fn.sh — each defining +# post_build_platform(). This file stays the shared dispatcher plus the +# platform-independent steps. run_script() { local origprogpath="$1" @@ -15,6 +21,7 @@ run_script() { local buildtime="${11}" local gnustep_folder="${12}" local stamp_file="${13}" + local info_plist="${14}" # macOS bundle Info.plist (unused off-darwin) local script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) pushd "$script_dir" > /dev/null @@ -25,21 +32,54 @@ run_script() { set -x local appname=$(basename "$origprogpath") local appdir=$(dirname "$origprogpath") - local progpath="$progdir/$appname" - local resourcesdir="$progdir/Resources" - mkdir -p "$resourcesdir" + + # cp -u is a GNU extension; BSD cp (darwin) aborts on it. A helper keeps + # the call sites shared while each platform gets the right spelling. + if [[ "$host_os" == "darwin" ]]; then + cp_update() { cp -Rf "$@"; } + else + cp_update() { cp -rfu "$@"; } + fi + + # darwin builds a .app bundle: binary in Contents/MacOS, resources in + # Contents/Resources. Other hosts keep the flat layout. + local progpath resourcesdir + if [[ "$host_os" == "darwin" ]]; then + progpath="$progdir/Contents/MacOS/$appname" + resourcesdir="$progdir/Contents/Resources" + mkdir -p "$progdir/Contents/MacOS" "$resourcesdir" + else + progpath="$progdir/$appname" + resourcesdir="$progdir/Resources" + mkdir -p "$resourcesdir" + fi if [[ ! -f "$origprogpath" ]]; then echo "❌ 'Oolite binary at '$origprogpath' does not exist!" >&2 return 1 fi - if ! cp -fu "$origprogpath" "$progpath"; then - echo "❌ Failed to copy '$origprogpath' to '$progpath'!" >&2 - return 1 + # The binary is always fresh from the build, so the update-skip of -u is + # never taken; darwin uses plain -f. + if [[ "$host_os" == "darwin" ]]; then + if ! cp -f "$origprogpath" "$progpath"; then + echo "❌ Failed to copy '$origprogpath' to '$progpath'!" >&2 + return 1 + fi + else + if ! cp -fu "$origprogpath" "$progpath"; then + echo "❌ Failed to copy '$origprogpath' to '$progpath'!" >&2 + return 1 + fi fi generate_manifest "$resourcesdir/manifest.plist" "$deployment_release" "$ver_full" "$ver_quad" "$ver_githash" "$buildtime" if [[ "$deployment_release" == "no" ]]; then - local addonsdir="$progdir/AddOns" + local addonsdir + if [[ "$host_os" == "darwin" ]]; then + # Oolite looks for AddOns next to the bundle, not inside it. + addonsdir="$(dirname "$progdir")/AddOns" + else + addonsdir="$progdir/AddOns" + fi mkdir -p "$addonsdir" rm -rf "$addonsdir/Basic-debug.oxp" cp -rf DebugOXP/Debug.oxp "$addonsdir/Basic-debug.oxp" @@ -49,7 +89,7 @@ run_script() { if [[ "$espeak" == "yes" ]]; then if [[ "$host_os" == "windows" ]]; then # Windows espeak-ng-data - cp -rfu "$MINGW_PREFIX/share/espeak-ng-data" "$resourcesdir" + cp_update "$MINGW_PREFIX/share/espeak-ng-data" "$resourcesdir" else # Linux search paths for espeak-ng-data local SEARCH_PATHS=( @@ -58,11 +98,25 @@ run_script() { "/usr/share/espeak-ng-data" "/app/share/espeak-ng-data" ) + if [[ "$host_os" == "darwin" ]]; then + # Homebrew keg (or the /opt/homebrew default when brew is + # unavailable in the environment). The brew entry is a + # symlink into the Cellar; BSD cp -R would copy the link + # itself, so resolve it first (and clear any previously + # copied link). + local espeak_data_dir + espeak_data_dir=$(readlink -f "$(command -v brew > /dev/null 2>&1 && brew --prefix || echo /opt/homebrew)/share/espeak-ng-data") + SEARCH_PATHS+=("$espeak_data_dir") + fi local found_data=false local path for path in "${SEARCH_PATHS[@]}"; do if [[ -d "$path" ]]; then - cp -rfu "$path" "$resourcesdir" + # -rf: the destination may be the directory left by a + # previous build (rm -f would fail noisily on it); on a + # nonexistent path rm -rf is as silent as rm -f. + rm -rf "$resourcesdir/espeak-ng-data" + cp_update "$path" "$resourcesdir" found_data=true break fi @@ -79,50 +133,25 @@ run_script() { # Replace specific voices with Oolite-specific versions rm -f "$resourcesdir/espeak-ng-data/voices/!v/f2" rm -f "$resourcesdir/espeak-ng-data/voices/default" - cp -rfu Resources/. "$resourcesdir" + cp_update Resources/. "$resourcesdir" rm -f "$resourcesdir/AIReference.html" "$resourcesdir/*.icns" - # Strip binary if requested - if [[ "$strip_bin" == "yes" ]]; then - if [[ "$host_os" == "windows" ]]; then - # Windows: Standard GNU strip is safest for PE/COFF - strip "$progpath" - else - # Linux - local debugpath="$progdir/$appname.debug" - # Extract symbols to file - objcopy --only-keep-debug "$progpath" "$debugpath" - # Compress the debug sections in the symbol file - objcopy --compress-debug-sections=zlib-gnu "$debugpath" - # strip the binary - strip -R .comment "$progpath" - # Add the debug link - objcopy --add-gnu-debuglink="$debugpath" "$progpath" - fi - elif [[ "$host_os" == "linux" ]]; then - # Compress the debug sections in the binary - objcopy --compress-debug-sections=zlib-gnu "$progpath" - fi - - if [[ "$host_os" == "windows" ]]; then - # Determine and copy DLL dependencies - local unix_prefix=$(cygpath -u "$MINGW_PREFIX") - ldd "$progpath" | grep "$unix_prefix" | awk '{print $3}' | xargs -I {} cp -rfu {} "$progdir" - else - # Copy Linux-specific wrapper script - cp -fu ShellScripts/Linux/run_oolite.sh "$progdir" - local gnustep_conf="$gnustep_folder/etc/GNUstep/GNUstep.conf" - if [ ! -f "$gnustep_conf" ] && [ "$gnustep_folder" = "/usr" ] && [ -f "/etc/GNUstep/GNUstep.conf" ]; then - gnustep_conf="/etc/GNUstep/GNUstep.conf" - fi - install -D "$gnustep_conf" "$resourcesdir/GNUstep.conf.orig" || { echo "$err_msg GNUstep config" >&2; return 1; } - - # If we're using GNUstep libraries that aren't in a system folder copy them - ldd "$progpath" | \ - grep -E "libgnustep-base|libobjc\.so\." | \ - grep -vE "^[[:space:]]*.*=>[[:space:]]*/(usr/(local/)?|lib(64)?/)" | \ - awk '{print $3}' | \ - xargs -I {} cp -Lrfu {} "$progdir/" + # Stripping and dependency copying are entirely host-specific. + local platform_dir + case "$host_os" in + darwin) + platform_dir="Darwin" + ;; + windows | cygwin) + platform_dir="Windows" + ;; + *) + platform_dir="Linux" + ;; + esac + source "$script_dir/../$platform_dir/post_build_fn.sh" + if ! post_build_platform "$progpath" "$progdir" "$resourcesdir" "$strip_bin" "$gnustep_folder" "$info_plist"; then + return 1 fi echo "✅ Oolite post-build completed successfully" diff --git a/clang-darwin.ini b/clang-darwin.ini new file mode 100644 index 0000000000..b5b2b7d897 --- /dev/null +++ b/clang-darwin.ini @@ -0,0 +1,4 @@ +[binaries] +c = 'clang' +cpp = 'clang++' +objc = 'clang' diff --git a/installers/macos/Info.plist.in b/installers/macos/Info.plist.in new file mode 100644 index 0000000000..40b2e48e33 --- /dev/null +++ b/installers/macos/Info.plist.in @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + oolite + CFBundleIconFile + oolite.icns + CFBundleIdentifier + org.oolite.oolite + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Oolite + CFBundlePackageType + APPL + CFBundleShortVersionString + @ver_full@ + CFBundleVersion + @ver_quad@ + LSMinimumSystemVersion + 26.0 + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + diff --git a/installers/macos/oolite.icns b/installers/macos/oolite.icns new file mode 100644 index 0000000000..33cd7d76fe Binary files /dev/null and b/installers/macos/oolite.icns differ diff --git a/meson.build b/meson.build index 8397f071ea..89031758fa 100644 --- a/meson.build +++ b/meson.build @@ -36,11 +36,28 @@ add_project_arguments( language: c_family, ) +# macOS bundle Info.plist: substituted from the template at configure time +# (@ver_full@ / @ver_quad@, both derived from ShellScripts/common/get_version.sh +# via version_vars). Generated on every host; only the darwin post-build +# (ShellScripts/Darwin/post_build_fn.sh) installs it into the bundle. +plist_data = configuration_data() +plist_data.set('ver_full', version_string) +plist_data.set('ver_quad', version_vars.get('ver_quad')) +macos_info_plist = configure_file( + input: 'installers/macos/Info.plist.in', + output: 'Info.plist', + configuration: plist_data, +) + is_debug = (get_option('optimization') == '0' or get_option('debug')) if is_debug add_project_arguments( '-O0', - '-DDEBUG', + # NOTE: DEBUG must NOT be defined for oolite sources: js_static is + # built without DEBUG, and DEBUG flips jsapi.h's JSVAL_* / JSID_VOID + # constants to extern DEBUG-only library symbols the archive does not + # contain (link failure). Oolite's own debug features key off + # OO_DEBUG instead; see also the #ifdef DEBUG guards in OOJSConsole.m. '-DOO_DEBUG', '-DOO_CHECK_GL_HEAVY=1', language: c_family, @@ -93,7 +110,13 @@ if get_option('dev_release') endif if get_option('oo_javascript_trace') - add_project_arguments('-DMOZ_TRACE_JSCALLS=1', language: c_family) + # JS_SetFunctionCallback (jsapi.h) only exists in SpiderMonkey builds + # that include the trace-JIT; the darwin js185 artifact is + # interpreter-only, so the option is inert there (documented port + # deviation: JIT off). + if host_os != 'darwin' + add_project_arguments('-DMOZ_TRACE_JSCALLS=1', language: c_family) + endif endif gnustep_folder = '' # Not needed for Windows diff --git a/mk.sh b/mk.sh index ffbec771c2..91f24795f5 100755 --- a/mk.sh +++ b/mk.sh @@ -125,7 +125,7 @@ show_help() { # Script Help Menu echo -e " \033[36m--compile-flags=\"...\"\033[0m Pass additional arguments directly to 'meson compile'" echo -e " \033[36m--configure-flags=\"...\"\033[0m Pass additional arguments directly to 'meson configure'" echo -e " \033[36m--install-flags=\"...\"\033[0m Pass additional arguments directly to 'meson install'" - echo -e " \033[36m--native-file=\"...\"\033[0m Specify native file (defaults to clang.ini)" + echo -e " \033[36m--native-file=\"...\"\033[0m Specify native file (defaults to clang.ini; clang-darwin.ini on macOS)" echo -e " \033[36m--ver-full=\"...\"\033[0m Specify full version string" echo -e " \033[36m--buildtime=\"...\"\033[0m Specify build time" echo -e " \033[36m--github-repository=\"...\"\033[0m Specify target GitHub repository" @@ -383,7 +383,11 @@ if [[ -z "$BUILD_TYPE" ]]; then fi if [[ -z "$NATIVE_FILE" ]]; then - NATIVE_FILE="clang.ini" # Apply default if it wasn't passed as an option + if [[ "$(uname -s)" == "Darwin" ]]; then + NATIVE_FILE="clang-darwin.ini" # macOS: Apple clang + ld (no lld dependency) + else + NATIVE_FILE="clang.ini" # Apply default if it wasn't passed as an option + fi fi execute_target "$ACTION" "$BUILD_TYPE" diff --git a/src/Core/Compat/OOFoundationCompat.h b/src/Core/Compat/OOFoundationCompat.h new file mode 100644 index 0000000000..c3735eac31 --- /dev/null +++ b/src/Core/Compat/OOFoundationCompat.h @@ -0,0 +1,45 @@ +/* + +OOFoundationCompat.h + +Compatibility declarations for building Oolite against Apple's Foundation +(the Meson darwin host) where its API surface differs from GNUstep Base. + +Entries are strictly audit-driven: a declaration is added here only when a +source file needs it to compile against Apple Foundation, and each entry +must cite where it is used. This header is intentionally included by +OOFoundation.h (and only there), so every translation unit sees a single, +consistent set of compatibility definitions. + +Currently empty: as measured by the platform-surface audit and the path-A +probe (31/35 mac-gated files compile clean against Apple Foundation), no +symbol shims are required. The header exists so future audit findings have +a designated, reviewable home instead of scattering #ifdefs across sources. + +Oolite +Copyright (C) 2004-2026 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#pragma once + +#ifdef OOLITE_MACOS_APPLE_FOUNDATION + +/* (no entries required by the current audit) */ + +#endif /* OOLITE_MACOS_APPLE_FOUNDATION */ diff --git a/src/Core/Debug/OODebugMonitor.m b/src/Core/Debug/OODebugMonitor.m index 5867f0d101..8ffe33c599 100644 --- a/src/Core/Debug/OODebugMonitor.m +++ b/src/Core/Debug/OODebugMonitor.m @@ -68,7 +68,12 @@ - (NSArray*)loadSourceFile:(NSString*)filePath; @end @implementation OODebugMonitor -#if OOLITE_GNUSTEP +#if OOLITE_GNUSTEP || !OOLITE_MAC_APPKIT +/* Neither GNUstep Base nor Apple's Foundation declares this AppKit + constant, and the SDL backends never run NSApp, so nothing would post + it anyway; provide a local (never-posted) definition so the observer + registration below stays portable. +*/ NSString* NSApplicationWillTerminateNotification = @"ApplicationWillTerminate"; #endif @@ -832,7 +837,7 @@ - (oneway void)jsEngine:(in byref OOJavaScriptEngine*)engine if (errorReport->filename != NULL) filePath = [NSString stringWithUTF8String:errorReport->filename]; if ([filePath length] != 0) { - [formattedMessage appendFormat:@"\n %@, line %u", [filePath lastPathComponent], errorReport -> lineno]; + [formattedMessage appendFormat:@"\n %@, line %u", [filePath lastPathComponent], errorReport->lineno]; // Append source code sourceLine = [self sourceCodeForFile:filePath line:errorReport->lineno]; diff --git a/src/Core/Debug/OODebugTCPConsoleClient.m b/src/Core/Debug/OODebugTCPConsoleClient.m index 4aae6ebd2d..7ee91d8e56 100644 --- a/src/Core/Debug/OODebugTCPConsoleClient.m +++ b/src/Core/Debug/OODebugTCPConsoleClient.m @@ -439,6 +439,14 @@ - (void)readData while (length > 0) { data = [NSData dataWithBytesNoCopy:buffer length:length freeWhenDone:NO]; OOTCPStreamDecoderReceiveData(_decoder, data); + /* On Apple Foundation, a socket NSInputStream's -read:maxLength: + blocks (running a nested run loop) when no bytes are pending, + so an unconditional drain loop parks the main thread forever + once the console goes quiet, freezing loading and rendering. + Only re-read while the stream reports bytes available without + blocking; remaining data re-signals HasBytesAvailable. */ + if (![_inStream hasBytesAvailable]) + break; length = [_inStream read:buffer maxLength:kBufferSize]; } } diff --git a/src/Core/Entities/PlayerEntity.h b/src/Core/Entities/PlayerEntity.h index eae1c946ac..64ad80006c 100644 --- a/src/Core/Entities/PlayerEntity.h +++ b/src/Core/Entities/PlayerEntity.h @@ -174,7 +174,7 @@ enum { GUI_ROW_GAMEOPTIONS_VOLUME, #if OOLITE_SPEECH_SYNTH GUI_ROW_GAMEOPTIONS_SPEECH, -#if !OOLITE_MAC_OS_X +#if !OOLITE_MAC_APPKIT // FIXME: should have voice option for OS X GUI_ROW_GAMEOPTIONS_SPEECH_LANGUAGE, GUI_ROW_GAMEOPTIONS_SPEECH_GENDER, diff --git a/src/Core/GameController.h b/src/Core/GameController.h index 11c3139292..5ef12b62a8 100644 --- a/src/Core/GameController.h +++ b/src/Core/GameController.h @@ -29,7 +29,7 @@ MA 02110-1301, USA. #import "OOFunctionAttributes.h" #import "OOMouseInteractionMode.h" -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT #import // For PDFKit. #endif @@ -42,11 +42,11 @@ MA 02110-1301, USA. @class MyOpenGLView, OOFullScreenController; // TEMP: whether to use separate OOFullScreenController object, will hopefully be used for all builds soon. -#define OO_USE_FULLSCREEN_CONTROLLER OOLITE_MAC_OS_X +#define OO_USE_FULLSCREEN_CONTROLLER OOLITE_MAC_APPKIT @interface GameController : NSObject { @private -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT IBOutlet NSTextField* splashProgressTextField; IBOutlet NSView* splashView; IBOutlet NSWindow* gameWindow; @@ -124,7 +124,7 @@ MA 02110-1301, USA. - (void)performGameTick:(id)sender; -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT - (IBAction)showLogAction:(id)sender; - (IBAction)showLogFolderAction:(id)sender; - (IBAction)showSnapshotsAction:(id)sender; @@ -168,7 +168,7 @@ MA 02110-1301, USA. @interface GameController (FullScreen) #if OO_USE_FULLSCREEN_CONTROLLER -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT - (IBAction)toggleFullScreenAction:(id)sender; #endif diff --git a/src/Core/GameController.m b/src/Core/GameController.m index 07ad5d3cb6..944810282b 100644 --- a/src/Core/GameController.m +++ b/src/Core/GameController.m @@ -44,7 +44,7 @@ #import "legacy_random.h" #include -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT #import "JAPersistentFileReference.h" #import "OOMacJoystickManager.h" #import "OoliteApp.h" @@ -100,7 +100,7 @@ - (id)init - (void)dealloc { -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:UNIVERSE]; #endif @@ -229,7 +229,7 @@ - (void)applicationDidFinishLaunching:(NSNotification*)notification [self beginSplashScreen]; #endif -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT [OOJoystickManager setStickHandlerClass:[OOMacJoystickManager class]]; SetUpSparkle(); #endif @@ -275,7 +275,7 @@ - (void)applicationDidFinishLaunching:(NSNotification*)notification // Release anything allocated above that is not required. [pool release]; -#if !OOLITE_MAC_OS_X +#if !OOLITE_MAC_APPKIT [[NSRunLoop currentRunLoop] run]; #endif } @@ -299,7 +299,7 @@ - (void)loadPlayerIfRequired - (void)beginSplashScreen { -#if !OOLITE_MAC_OS_X +#if !OOLITE_MAC_APPKIT if (!gameView) { gameView = [MyOpenGLView alloc]; [gameView init]; @@ -311,7 +311,7 @@ - (void)beginSplashScreen #endif } -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT - (void)performGameTick:(id)sender { @@ -374,7 +374,7 @@ - (void)startAnimationTimer timer = [[NSTimer timerWithTimeInterval:ti target:self selector:@selector(performGameTick:) userInfo:nil repeats:YES] retain]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSEventTrackingRunLoopMode]; #endif @@ -390,7 +390,7 @@ - (void)stopAnimationTimer } } -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT - (void)recenterVirtualJoystick { @@ -619,7 +619,7 @@ - (void)logProgress:(NSString*)message if (![UNIVERSE doingStartUp]) return; -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT [splashProgressTextField setStringValue:message]; [splashProgressTextField display]; #endif @@ -629,7 +629,7 @@ - (void)logProgress:(NSString*)message } #if OO_DEBUG -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT - (BOOL)debugMessageTrackingIsOn { return splashProgressTextField != nil; @@ -701,7 +701,7 @@ - (void)endSplashScreen { OOLogSetDisplayMessagesInClass(@"startup.progress", NO); -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT // These views will be released when we replace the content view. splashProgressTextField = nil; splashView = nil; @@ -714,7 +714,7 @@ - (void)endSplashScreen #endif } -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT // NIB methods - (void)awakeFromNib @@ -855,7 +855,7 @@ - (void)reportUnhandledStartupException:(NSException*)exception { OOLog(@"startup.exception", @"***** Unhandled exception during startup: %@ (%@).", [exception name], [exception reason]); -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT // Display an error alert. // TODO: provide better information on reporting bugs in the manual, and refer to it here. NSRunCriticalAlertPanel(@"Oolite failed to start up, because an unhandled exception occurred.", @"An exception of type %@ occurred. If this problem persists, please file a bug report.", @"OK", NULL, NULL, [exception name]); @@ -874,7 +874,7 @@ - (BOOL)suppressClangStuff @end -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT static void SetUpSparkle(void) { diff --git a/src/Core/Materials/OOShaderMaterial.m b/src/Core/Materials/OOShaderMaterial.m index 20f584dfd0..de704e11ff 100644 --- a/src/Core/Materials/OOShaderMaterial.m +++ b/src/Core/Materials/OOShaderMaterial.m @@ -123,6 +123,13 @@ - (id)initWithName:(NSString*)name [modifiedMacros setObject:[NSNumber numberWithUnsignedInt:textureUnits] forKey:@"OO_TEXTURE_UNIT_COUNT"]; +#if OOLITE_MAC_OS_X + // Legacy GLSL has no derivatives; disable the specular-AA NDF filter + // (oolite-default-shader/planet.fragment guard on OO_LEGACY_GL). + [modifiedMacros setObject:[NSNumber numberWithBool:YES] + forKey:@"OO_LEGACY_GL"]; +#endif + // used to test for simplified shaders - OO_REDUCED_COMPLEXITY - here macroString = MacrosToString(modifiedMacros); } diff --git a/src/Core/NSUserDefaults+Override.m b/src/Core/NSUserDefaults+Override.m index 62a9fafd5b..8a3c396af9 100644 --- a/src/Core/NSUserDefaults+Override.m +++ b/src/Core/NSUserDefaults+Override.m @@ -41,7 +41,7 @@ - (BOOL)writeDictionary:(NSDictionary*)dict } else if (nil == dict) { NSFileManager* mgr = [NSFileManager defaultManager]; - return [mgr removeFileAtPath:file handler:nil]; + return [mgr removeItemAtPath:file error:nil]; } else { NSData* data; NSString* err; diff --git a/src/Core/OOFoundation.h b/src/Core/OOFoundation.h index 921f49376e..8b7e8b1198 100644 --- a/src/Core/OOFoundation.h +++ b/src/Core/OOFoundation.h @@ -56,6 +56,13 @@ MA 02110-1301, USA. #else #define OOLITE_MAC_OS_X 1 +/* Backend selector: 1 only when the AppKit (NIB/UI) backend is in use. + Apple-Foundation + SDL builds (the Meson darwin host) set OOLITE_SDL, + so all AppKit-only UI code compiles out and the SDL backend is used, + exactly as in the GNUstep/Linux build. +*/ +#define OOLITE_MAC_APPKIT (OOLITE_MAC_OS_X && !OOLITE_SDL) + /* Useful macro copied from GNUstep. */ #ifndef DESTROY @@ -107,12 +114,13 @@ MA 02110-1301, USA. #define MIN(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) #endif -#ifdef HAVE_LIBESPEAK -#define OOLITE_SPEECH_SYNTH 1 -#define OOLITE_ESPEAK 1 +#import "Comparison.h" + #endif -// Pseudo-keywords used for AppKit UI bindings. +/* Pseudo-keywords used for AppKit UI bindings. Apple's Foundation defines + these, so the no-ops only apply to other Foundation implementations. +*/ #ifndef IBOutlet #define IBOutlet /**/ #endif @@ -120,9 +128,10 @@ MA 02110-1301, USA. #define IBAction void #endif -#import "Comparison.h" - -/* Define AppKit constants for events */ +#if !OOLITE_MAC_APPKIT +/* Define AppKit constants for events. When the AppKit backend is in use, + * NSEvent.h already provides these with the same values. + */ enum { NSUpArrowFunctionKey = 0xF700, NSDownArrowFunctionKey = 0xF701, @@ -197,7 +206,6 @@ enum { NSHelpFunctionKey = 0xF746, NSModeSwitchFunctionKey = 0xF747 }; - #endif #ifndef OOLITE_GNUSTEP @@ -220,6 +228,25 @@ enum { #define OOLITE_SDL 0 #endif +#ifndef OOLITE_MAC_APPKIT +/* Not defined by an Apple-Foundation flavour block (i.e. building against + GNUstep): no AppKit backend. +*/ +#define OOLITE_MAC_APPKIT 0 +#endif + +/* Text-to-speech backend selection. HAVE_LIBESPEAK comes from the build + system when the espeak-ng option is enabled (src/meson.build); it is + deliberately platform-neutral — the mapping lives outside the + GNUstep-only block above so the Apple-Foundation darwin build gets + the espeak path as well (the AppKit backend uses NSSpeechSynthesizer + instead of espeak when both are available). +*/ +#ifdef HAVE_LIBESPEAK +#define OOLITE_SPEECH_SYNTH 1 +#define OOLITE_ESPEAK 1 +#endif + #ifndef OOLITE_SPEECH_SYNTH #define OOLITE_SPEECH_SYNTH 0 #endif @@ -364,3 +391,11 @@ typedef id instancetype; #define OO_GAME_DATA_TO_USER_FOLDER 0 #endif #endif + +/* Apple-Foundation-only compatibility declarations (audit-driven; see the + header for the rules). Skipped entirely on other Foundations, which never + see the file. + */ +#ifdef OOLITE_MACOS_APPLE_FOUNDATION +#import "OOFoundationCompat.h" +#endif diff --git a/src/Core/OOFullScreenController.h b/src/Core/OOFullScreenController.h index 653fb45f4d..bf479f2c43 100644 --- a/src/Core/OOFullScreenController.h +++ b/src/Core/OOFullScreenController.h @@ -31,13 +31,16 @@ MA 02110-1301, USA. @class MyOpenGLView; -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT #define kOODisplayWidth ((NSString*)kCGDisplayWidth) #define kOODisplayHeight ((NSString*)kCGDisplayHeight) #define kOODisplayRefreshRate ((NSString*)kCGDisplayRefreshRate) #define kOODisplayBitsPerPixel ((NSString*)kCGDisplayBitsPerPixel) #define kOODisplayIOFlags ((NSString*)kCGDisplayIOFlags) #else +/* The SDL backends (GNUstep and Apple-Foundation alike) use these macros + as plain dictionary keys; the literals only need to be consistent. + */ #define kOODisplayWidth (@"Width") #define kOODisplayHeight (@"Height") #define kOODisplayRefreshRate (@"RefreshRate") diff --git a/src/Core/OOMacroOpenGL.h b/src/Core/OOMacroOpenGL.h index b68bf8f0c0..682746430e 100644 --- a/src/Core/OOMacroOpenGL.h +++ b/src/Core/OOMacroOpenGL.h @@ -31,6 +31,12 @@ MA 02110-1301, USA. #if OOLITE_MAC_OS_X && !defined(OOLITE_NO_CGL_MACRO) +/* CGLMacro.h uses CGL types and CGLGetCurrentContext() without including + the header that declares them; import it first or every macro expansion + site fails to compile. + */ +#import + #if MAC_OS_X_VERSION_10_4 <= MAC_OS_X_VERSION_MAX_ALLOWED #define CGL_MACRO_CACHE_RENDERER diff --git a/src/Core/OOOpenGLExtensionManager.h b/src/Core/OOOpenGLExtensionManager.h index c3171ca89c..952889fb5f 100644 --- a/src/Core/OOOpenGLExtensionManager.h +++ b/src/Core/OOOpenGLExtensionManager.h @@ -118,6 +118,8 @@ SOFTWARE. unsigned major, minor, release; + BOOL legacyGLMode; // legacy-GL compatibility mode (macOS 2.1 context accepted by capability tests) + BOOL usePointSmoothing; BOOL useLineSmoothing; BOOL useDustShader; @@ -155,6 +157,7 @@ SOFTWARE. - (BOOL)vboSupported; // Vertex buffer objects - (BOOL)fboSupported; // Frame buffer objects +- (BOOL)legacyGLMode; // Version gate accepted via legacy-GL compatibility path (darwin) - (BOOL)textureCombinersSupported; - (GLint)textureUnitCount; // Fixed function multitexture limit, does not apply to shaders. (GL_MAX_TEXTURE_UNITS_ARB) diff --git a/src/Core/OOOpenGLExtensionManager.m b/src/Core/OOOpenGLExtensionManager.m index f68155768b..06c8a7edf4 100644 --- a/src/Core/OOOpenGLExtensionManager.m +++ b/src/Core/OOOpenGLExtensionManager.m @@ -203,6 +203,8 @@ - (void)reset { const GLubyte *versionString = NULL, *curr = NULL; + legacyGLMode = NO; + DESTROY(extensions); DESTROY(vendor); DESTROY(renderer); @@ -241,9 +243,29 @@ - (void)reset OOLog(@"rendering.opengl.extensions", @"OpenGL extensions (%zu):\n%@", [extensions count], [[extensions allObjects] componentsJoinedByString:@", "]); if (![self versionIsAtLeastMajor:kMinMajorVersion minor:kMinMinorVersion]) { +#if OOLITE_MAC_OS_X + /* macOS has no compatibility profile: SDL hands us a legacy 2.1 + context, which cannot satisfy the 3.3 gate but does provide the + FBO + ARB shader objects the renderer actually needs. Accept it + when the required capabilities are present (Task 8 legacy-GL + compatibility layer). */ + if (![self versionIsAtLeastMajor:2 minor:1]) { + OOLog(@"rendering.opengl.version.insufficient", @"***** Oolite requires OpenGL version 2.1 or later (legacy mode)."); + [NSException raise:@"OoliteOpenGLTooOldException" + format:@"Oolite requires at least OpenGL 2.1 in legacy mode. You have %u.%u (\"%s\").", major, minor, versionString]; + } + if (![self haveExtension:@"GL_EXT_framebuffer_object"]) { + OOLog(@"rendering.opengl.version.insufficient", @"***** Legacy mode requires GL_EXT_framebuffer_object."); + [NSException raise:@"OoliteOpenGLTooOldException" + format:@"Legacy OpenGL mode requires GL_EXT_framebuffer_object. You have %u.%u (\"%s\").", major, minor, versionString]; + } + legacyGLMode = YES; + OOLog(@"rendering.legacy.gate", @"%@ %u.%u accepted in legacy-GL compatibility mode (3.3 gate bypassed; FBO present).", @"Legacy OpenGL version:", major, minor); +#else OOLog(@"rendering.opengl.version.insufficient", @"***** Oolite requires OpenGL version %u.%u or later.", kMinMajorVersion, kMinMinorVersion); [NSException raise:@"OoliteOpenGLTooOldException" format:@"Oolite requires at least OpenGL %u.%u. You have %u.%u (\"%s\").", kMinMajorVersion, kMinMinorVersion, major, minor, versionString]; +#endif } NSString* versionStr = [[[NSString alloc] initWithUTF8String:(const char*)versionString] autorelease]; @@ -407,6 +429,11 @@ - (BOOL)fboSupported #endif } +- (BOOL)legacyGLMode +{ + return legacyGLMode; +} + - (BOOL)textureCombinersSupported { #if OO_MULTITEXTURE diff --git a/src/Core/OOOpenGLOnly.h b/src/Core/OOOpenGLOnly.h index 8168268403..27dfd3949a 100644 --- a/src/Core/OOOpenGLOnly.h +++ b/src/Core/OOOpenGLOnly.h @@ -28,8 +28,13 @@ MA 02110-1301, USA. #pragma once // SDL OpenGL includes... -// GL_GLEXT_PROTOTYPES must be defined for the Linux build to use shaders. -#if OOLITE_LINUX +// GL_GLEXT_PROTOTYPES must be defined for the Linux build to use shaders, and +// for the macOS legacy-GL build: SDL's vendored glext (SDL_opengl_glext.h) then +// declares the ARB/3.x-named entry points that Apple's no longer +// declares, giving direct linkage against OpenGL.framework (no function-pointer +// table). macOS has no or ; SDL_opengl.h is +// self-contained. +#if OOLITE_LINUX || defined(__APPLE__) #ifndef GL_GLEXT_PROTOTYPES #define GL_GLEXT_PROTOTYPES #define __DEFINED_GL_GLEXT_PROTOTYPES @@ -40,8 +45,21 @@ MA 02110-1301, USA. #include // include an up-to-date version of glext.h +// On Apple platforms, SDL_opengl.h above already bundles Mesa's glext +// (SDL_opengl_glext.h, including its prototypes when GL_GLEXT_PROTOTYPES is +// defined); the GL/ directory layout is Linux/X11-only. Apple's own +// must not be mixed in, as it typedefs GLhandleARB to +// void* while the rest of the tree uses Mesa's GLuint spelling. +// Toolchain test (__APPLE__) rather than OOLITE_MAC_OS_X: this header is +// also included from plain C data tables that do not import OOFoundation.h. +// GLU, in contrast, is not bundled by SDL and comes from the platform's +// OpenGL toolkit. +#if !defined(__APPLE__) #include #include +#else +#include +#endif #ifdef __DEFINED_GL_GLEXT_PROTOTYPES #undef GL_GLEXT_PROTOTYPES diff --git a/src/Core/OXPVerifier/OOOXPVerifier.m b/src/Core/OXPVerifier/OOOXPVerifier.m index 16f6442f02..8f860a4322 100644 --- a/src/Core/OXPVerifier/OOOXPVerifier.m +++ b/src/Core/OXPVerifier/OOOXPVerifier.m @@ -667,7 +667,7 @@ static void OpenLogFile(NSString* name) // Open log file in appropriate application / provide feedback. if ([[NSUserDefaults standardUserDefaults] oo_boolForKey:@"oxp-verifier-open-log" defaultValue:YES]) { -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT [[NSWorkspace sharedWorkspace] openFile:OOLogHandlerGetLogPath()]; #elif OOLITE_WINDOWS // ShellExecute will automatically use the app associated with .log files diff --git a/src/Core/ResourceManager.m b/src/Core/ResourceManager.m index 5013fe4e42..de565a24e5 100644 --- a/src/Core/ResourceManager.m +++ b/src/Core/ResourceManager.m @@ -191,13 +191,12 @@ + (NSArray*)userRootPaths + (NSString*)builtInPath { - NSFileManager* fileManager = [NSFileManager defaultManager]; - #if OOLITE_MAC_OS_X return [[NSBundle mainBundle] resourcePath]; // Use resourcePath directly #else + NSFileManager* fileManager = [NSFileManager defaultManager]; NSString* startingDir = [fileManager currentDirectoryPath]; // Start from cwd -#endif + // Look for a "Resources" folder (Windows & Linux) NSString* primaryResourcesPath = [startingDir stringByAppendingPathComponent:@"Resources"]; BOOL isDir = NO; @@ -207,6 +206,7 @@ + (NSString*)builtInPath // Fallback: Look in startingDir/../share/oolite/Resources NSString* fallbackPath = [[startingDir stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"share/oolite/Resources"]; return [fallbackPath stringByStandardizingPath]; +#endif } + (NSArray*)pathsWithAddOns diff --git a/src/Core/Scripting/OOJavaScriptEngine.m b/src/Core/Scripting/OOJavaScriptEngine.m index 7d1ea59a29..c9e772129f 100644 --- a/src/Core/Scripting/OOJavaScriptEngine.m +++ b/src/Core/Scripting/OOJavaScriptEngine.m @@ -80,6 +80,12 @@ #define OOJSENGINE_JSVERSION JSVERSION_ECMA_5 #ifdef DEBUG #define JIT_OPTIONS 0 +#elif defined(__aarch64__) || defined(__ARM64__) || defined(_M_ARM64) +/* The arm64 js185 build ships interpreter-only: the JSOPTION_* JIT bits + * compile, but they select JIT backends that are not present, so force + * them off instead of requesting the impossible. + */ +#define JIT_OPTIONS 0 #else #define JIT_OPTIONS JSOPTION_JIT | JSOPTION_METHODJIT | JSOPTION_PROFILING #endif diff --git a/src/Core/Universe.h b/src/Core/Universe.h index 865b945347..d44e8fc20a 100644 --- a/src/Core/Universe.h +++ b/src/Core/Universe.h @@ -319,7 +319,7 @@ enum { NSUInteger drawCounter; #if OOLITE_SPEECH_SYNTH -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT NSSpeechSynthesizer* speechSynthesizer; #elif OOLITE_ESPEAK const espeak_VOICE** espeak_voices; diff --git a/src/Core/Universe.m b/src/Core/Universe.m index 3f2dd14807..f97501ced0 100644 --- a/src/Core/Universe.m +++ b/src/Core/Universe.m @@ -337,12 +337,70 @@ - (int)colorblindMode return _colorblindMode; } +#if OOLITE_MAC_OS_X +/* Legacy-GL compatibility layer (Task 8). The darwin build runs on Apple's + legacy 2.1 context, where float16 color buffers are not guaranteed + renderable. (Re)create the texture as RGBA8 if the FBO is incomplete. + Sets *outFellBack when the format was downgraded. The framebuffer to test + must be bound. Returns YES if the framebuffer is complete after the call. */ +static BOOL sMRTAvailable = YES; // multiple render targets (bloom bright-pass) usable +#endif + +/* Internal format of the postFX color buffers. GL_RGBA16F except when the + legacy-GL fallback (darwin) downgraded to GL_RGBA8 at init; reused by + resize so the formats stay consistent. */ +static GLenum sPostFXColorFormat = GL_RGBA16F; + +#if OOLITE_MAC_OS_X +static BOOL EnsurePostFXFramebufferComplete(GLuint colorTexture, GLsizei width, GLsizei height, BOOL* outFellBack) +{ + if (outFellBack != NULL) + *outFellBack = NO; + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) + return YES; + + OOLogWARN(@"rendering.legacy.fallback", @"Float16 color buffer not renderable; falling back to RGBA8 (%ux%u).", (unsigned)width, (unsigned)height); + OOGL(glBindTexture(GL_TEXTURE_2D, colorTexture)); + OOGL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL)); + OOGL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0)); + if (outFellBack != NULL) + *outFellBack = YES; + return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; +} + +/* PostFX full-screen quad without vertex array objects (GL 3.0 core, absent + on legacy GL): bind the quad's buffers and attribute pointers explicitly. + Locations 0/1 are pinned pre-link via the programs' attributeBindings. */ +static void BindPostFXQuadArrays(GLuint quadVBO, GLuint quadEBO) +{ + OOGL(glBindBuffer(GL_ARRAY_BUFFER, quadVBO)); + OOGL(glEnableVertexAttribArray(0)); + OOGL(glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0)); // position + OOGL(glEnableVertexAttribArray(1)); + OOGL(glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)))); // texture coords + OOGL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, quadEBO)); +} + +static void UnbindPostFXQuadArrays(void) +{ + OOGL(glDisableVertexAttribArray(1)); + OOGL(glDisableVertexAttribArray(0)); + OOGL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + OOGL(glBindBuffer(GL_ARRAY_BUFFER, 0)); +} +#endif // OOLITE_MAC_OS_X + - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize { // liberate us from the 0.0 to 1.0 rgb range! +#if !OOLITE_MAC_OS_X + /* glClampColor arrived with GL 3.0; Apple GL 2.1 raises GL_INVALID_ENUM + here (a no-op, but it pollutes the error log). Unclamped colors are + the 2.1 default anyway. */ OOGL(glClampColor(GL_CLAMP_VERTEX_COLOR, GL_FALSE)); OOGL(glClampColor(GL_CLAMP_READ_COLOR, GL_FALSE)); OOGL(glClampColor(GL_CLAMP_FRAGMENT_COLOR, GL_FALSE)); +#endif // have to do this because on my machine the default framebuffer is not zero OOGL(glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &defaultDrawFBO)); @@ -351,13 +409,16 @@ - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize OOGL(glGetIntegerv(GL_CURRENT_PROGRAM, &previousProgramID)); GLint previousTextureID; OOGL(glGetIntegerv(GL_TEXTURE_BINDING_2D, &previousTextureID)); +#if !OOLITE_MAC_OS_X GLint previousVAO; OOGL(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &previousVAO)); +#endif GLint previousArrayBuffer; OOGL(glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &previousArrayBuffer)); GLint previousElementBuffer; OOGL(glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &previousElementBuffer)); +#if !OOLITE_MAC_OS_X // create MSAA framebuffer and attach MSAA texture and depth buffer to framebuffer OOGL(glGenFramebuffers(1, &msaaFramebufferID)); OOGL(glBindFramebuffer(GL_FRAMEBUFFER, msaaFramebufferID)); @@ -379,6 +440,7 @@ - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { OOLogERR(@"initTargetFramebufferWithViewSize.result", @"%@", @"***** Error: Multisample framebuffer not complete"); } +#endif // !OOLITE_MAC_OS_X // create framebuffer and attach texture and depth buffer to framebuffer OOGL(glGenFramebuffers(1, &targetFramebufferID)); @@ -397,15 +459,29 @@ - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize // create necessary depth render buffer OOGL(glGenRenderbuffers(1, &targetDepthBufferID)); OOGL(glBindRenderbuffer(GL_RENDERBUFFER, targetDepthBufferID)); +#if OOLITE_MAC_OS_X + // GL_DEPTH_COMPONENT32F is 3.0-core and unsupported on legacy GL; 24-bit depth is core since GL 1.4. + OOGL(glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, (GLsizei)viewSize.width, (GLsizei)viewSize.height)); +#else OOGL(glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32F, (GLsizei)viewSize.width, (GLsizei)viewSize.height)); +#endif OOGL(glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, targetDepthBufferID)); GLenum attachment[1] = { GL_COLOR_ATTACHMENT0 }; OOGL(glDrawBuffers(1, attachment)); +#if OOLITE_MAC_OS_X + BOOL targetFellBack = NO; + if (!EnsurePostFXFramebufferComplete(targetTextureID, (GLsizei)viewSize.width, (GLsizei)viewSize.height, &targetFellBack)) { + OOLogERR(@"initTargetFramebufferWithViewSize.result", @"%@", @"***** Error: Framebuffer not complete (even after RGBA8 fallback)"); + } + if (targetFellBack) + sPostFXColorFormat = GL_RGBA8; +#else if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { OOLogERR(@"initTargetFramebufferWithViewSize.result", @"%@", @"***** Error: Framebuffer not complete"); } +#endif OOGL(glBindFramebuffer(GL_FRAMEBUFFER, defaultDrawFBO)); @@ -433,11 +509,41 @@ - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize } GLenum attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; +#if OOLITE_MAC_OS_X + /* MRT probe: bloom's bright-pass needs two color attachments. If ARB + draw buffers is absent (or the 2-attachment FBO is incomplete on + legacy GL), fall back to a single target and disable bloom. */ + sMRTAvailable = [[OOOpenGLExtensionManager sharedManager] haveExtension:@"GL_ARB_draw_buffers"]; + OOGL(glDrawBuffers(sMRTAvailable ? 2 : 1, attachments)); + if (sMRTAvailable && glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + sMRTAvailable = NO; + OOGL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, 0, 0)); // detach C1 + OOGL(glDrawBuffers(1, attachments)); + OOLogWARN(@"rendering.legacy.fallback", @"Multiple render targets unavailable; bloom disabled (single-target passthrough)."); + } + OOLog(@"rendering.legacy.mrt", @"Multiple render targets %@.", sMRTAvailable ? @"available" : @"unavailable (single target)"); +#else OOGL(glDrawBuffers(2, attachments)); +#endif +#if OOLITE_MAC_OS_X + BOOL passthroughFellBack = NO; + if (!EnsurePostFXFramebufferComplete(passthroughTextureID[0], (GLsizei)viewSize.width, (GLsizei)viewSize.height, &passthroughFellBack)) { + OOLogERR(@"initTargetFramebufferWithViewSize.result", @"%@", @"***** Error: Passthrough framebuffer not complete (even after RGBA8 fallback)"); + } + if (passthroughFellBack) { + // keep COLOR_ATTACHMENT1 format-consistent with COLOR_ATTACHMENT0 + OOGL(glBindTexture(GL_TEXTURE_2D, passthroughTextureID[1])); + OOGL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, (GLsizei)viewSize.width, (GLsizei)viewSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL)); + OOGL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, passthroughTextureID[1], 0)); + OOGL(glBindTexture(GL_TEXTURE_2D, 0)); + sPostFXColorFormat = GL_RGBA8; + } +#else if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { OOLogERR(@"initTargetFramebufferWithViewSize.result", @"%@", @"***** Error: Passthrough framebuffer not complete"); } +#endif OOGL(glBindFramebuffer(GL_FRAMEBUFFER, defaultDrawFBO)); // ping-pong-framebuffer for blurring @@ -453,13 +559,23 @@ - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize OOGL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); OOGL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongColorbuffers[i], 0)); // check if framebuffers are complete (no need for depth buffer) +#if OOLITE_MAC_OS_X + if (!EnsurePostFXFramebufferComplete(pingpongColorbuffers[i], (GLsizei)viewSize.width, (GLsizei)viewSize.height, NULL)) { + OOLogERR(@"initTargetFramebufferWithViewSize.result", @"%@", @"***** Error: Pingpong framebuffers not complete (even after RGBA8 fallback)"); + } +#else if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { OOLogERR(@"initTargetFramebufferWithViewSize.result", @"%@", @"***** Error: Pingpong framebuffers not complete"); } +#endif } OOGL(glBindFramebuffer(GL_FRAMEBUFFER, defaultDrawFBO)); _bloom = [self detailLevel] >= DETAIL_LEVEL_EXTRAS; +#if OOLITE_MAC_OS_X + // bloom's bright-pass writes a second color attachment; without MRT it cannot run. + _bloom = _bloom && sMRTAvailable; +#endif _currentPostFX = _colorblindMode = OO_POSTFX_NONE; /* TODO: in OOEnvironmentCubeMap.m call these bind functions not with 0 but with "previousXxxID"s: @@ -470,15 +586,34 @@ - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize // shader for drawing a textured quad on the passthrough framebuffer and preparing it for bloom using MRT if (![[OOOpenGLExtensionManager sharedManager] shadersForceDisabled]) { +#if OOLITE_MAC_OS_X + /* Legacy GLSL 1.20: attribute locations are pinned pre-link via + attributeBindings (glBindAttribLocationARB) instead of layout() + qualifiers. The five shared postFX shaders are dual-mode: with + OO_LEGACY_GL defined they compile their GLSL 1.20 branch, without + it the upstream GLSL 330 content, byte-identical to what the + non-darwin prefixes below expect. OO_POSTFX_MRT selects the + gl_FragData[] bright-pass variant when multiple render targets + are available. */ + NSString* texturePrefix = sMRTAvailable ? @"#version 120\n#define OO_LEGACY_GL 1\n#define OO_POSTFX_MRT 1\n" : @"#version 120\n#define OO_LEGACY_GL 1\n"; + NSString* postFXPrefix = @"#version 120\n#define OO_LEGACY_GL 1\n"; + NSDictionary* textureAttribs = @{ @"vert_position" : @0, @"texture_coordinate" : @1 }; + NSDictionary* uvQuadAttribs = @{ @"aPos" : @0, @"aTexCoords" : @1 }; +#else + NSString* texturePrefix = @"#version 330\n"; + NSString* postFXPrefix = @"#version 330\n"; + NSDictionary* textureAttribs = [NSDictionary dictionary]; + NSDictionary* uvQuadAttribs = [NSDictionary dictionary]; +#endif textureProgram = [[OOShaderProgram shaderProgramWithVertexShaderName:@"oolite-texture.vertex" fragmentShaderName:@"oolite-texture.fragment" - prefix:@"#version 330\n" - attributeBindings:[NSDictionary dictionary]] retain]; + prefix:texturePrefix + attributeBindings:textureAttribs] retain]; // shader for blurring the over-threshold brightness image generated from the previous step using Gaussian filter blurProgram = [[OOShaderProgram shaderProgramWithVertexShaderName:@"oolite-blur.vertex" fragmentShaderName:@"oolite-blur.fragment" - prefix:@"#version 330\n" - attributeBindings:[NSDictionary dictionary]] retain]; + prefix:postFXPrefix + attributeBindings:uvQuadAttribs] retain]; // shader for applying bloom and any necessary post-proc fx, tonemapping and gamma correction finalProgram = [[OOShaderProgram shaderProgramWithVertexShaderName:@"oolite-final.vertex" #if OOLITE_WINDOWS @@ -486,15 +621,22 @@ - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize #else fragmentShaderName:@"oolite-final.fragment" #endif - prefix:@"#version 330\n" - attributeBindings:[NSDictionary dictionary]] retain]; + prefix:postFXPrefix + attributeBindings:uvQuadAttribs] retain]; } +#if !OOLITE_MAC_OS_X OOGL(glGenVertexArrays(1, &quadTextureVAO)); +#endif OOGL(glGenBuffers(1, &quadTextureVBO)); OOGL(glGenBuffers(1, &quadTextureEBO)); +#if OOLITE_MAC_OS_X + /* Legacy GL has no vertex array objects; the quad's attribute pointers + are bound explicitly at each draw site (BindPostFXQuadArrays). */ +#else OOGL(glBindVertexArray(quadTextureVAO)); +#endif OOGL(glBindBuffer(GL_ARRAY_BUFFER, quadTextureVBO)); OOGL(glBufferData(GL_ARRAY_BUFFER, sizeof(framebufferQuadVertices), framebufferQuadVertices, GL_STATIC_DRAW)); @@ -502,17 +644,21 @@ - (void)initTargetFramebufferWithViewSize:(NSSize)viewSize OOGL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, quadTextureEBO)); OOGL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(framebufferQuadIndices), framebufferQuadIndices, GL_STATIC_DRAW)); +#if !OOLITE_MAC_OS_X OOGL(glEnableVertexAttribArray(0)); // position attribute OOGL(glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0)); OOGL(glEnableVertexAttribArray(1)); // texture coord attribute OOGL(glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)))); +#endif // restoring previous bindings OOGL(glUseProgram(previousProgramID)); OOGL(glBindTexture(GL_TEXTURE_2D, previousTextureID)); +#if !OOLITE_MAC_OS_X OOGL(glBindVertexArray(previousVAO)); +#endif OOGL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, previousElementBuffer)); OOGL(glBindBuffer(GL_ARRAY_BUFFER, previousArrayBuffer)); } @@ -529,7 +675,9 @@ - (void)deleteOpenGLObjects OOGL(glDeleteFramebuffers(1, &targetFramebufferID)); OOGL(glDeleteFramebuffers(2, pingpongFBO)); OOGL(glDeleteFramebuffers(1, &passthroughFramebufferID)); +#if !OOLITE_MAC_OS_X OOGL(glDeleteVertexArrays(1, &quadTextureVAO)); +#endif OOGL(glDeleteBuffers(1, &quadTextureVBO)); OOGL(glDeleteBuffers(1, &quadTextureEBO)); [textureProgram release]; @@ -540,6 +688,7 @@ - (void)deleteOpenGLObjects - (void)resizeTargetFramebufferWithViewSize:(NSSize)viewSize { int i; +#if !OOLITE_MAC_OS_X // resize MSAA color attachment OOGL(glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, msaaTextureID)); OOGL(glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4, GL_RGBA16F, (GLsizei)viewSize.width, (GLsizei)viewSize.height, GL_TRUE)); @@ -549,27 +698,33 @@ - (void)resizeTargetFramebufferWithViewSize:(NSSize)viewSize OOGL(glBindRenderbuffer(GL_RENDERBUFFER, msaaDepthBufferID)); OOGL(glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT32F, (GLsizei)viewSize.width, (GLsizei)viewSize.height)); OOGL(glBindRenderbuffer(GL_RENDERBUFFER, 0)); +#endif - // resize color attachments + // resize color attachments (using the format decided at init time) OOGL(glBindTexture(GL_TEXTURE_2D, targetTextureID)); - OOGL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, (GLsizei)viewSize.width, (GLsizei)viewSize.height, 0, GL_RGBA, GL_FLOAT, NULL)); + OOGL(glTexImage2D(GL_TEXTURE_2D, 0, sPostFXColorFormat, (GLsizei)viewSize.width, (GLsizei)viewSize.height, 0, GL_RGBA, sPostFXColorFormat == GL_RGBA16F ? GL_FLOAT : GL_UNSIGNED_BYTE, NULL)); OOGL(glBindTexture(GL_TEXTURE_2D, 0)); for (i = 0; i < 2; i++) { OOGL(glBindTexture(GL_TEXTURE_2D, pingpongColorbuffers[i])); - OOGL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, (GLsizei)viewSize.width, (GLsizei)viewSize.height, 0, GL_RGBA, GL_FLOAT, NULL)); + OOGL(glTexImage2D(GL_TEXTURE_2D, 0, sPostFXColorFormat, (GLsizei)viewSize.width, (GLsizei)viewSize.height, 0, GL_RGBA, sPostFXColorFormat == GL_RGBA16F ? GL_FLOAT : GL_UNSIGNED_BYTE, NULL)); OOGL(glBindTexture(GL_TEXTURE_2D, 0)); } for (i = 0; i < 2; i++) { OOGL(glBindTexture(GL_TEXTURE_2D, passthroughTextureID[i])); - OOGL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, (GLsizei)viewSize.width, (GLsizei)viewSize.height, 0, GL_RGBA, GL_FLOAT, NULL)); + OOGL(glTexImage2D(GL_TEXTURE_2D, 0, sPostFXColorFormat, (GLsizei)viewSize.width, (GLsizei)viewSize.height, 0, GL_RGBA, sPostFXColorFormat == GL_RGBA16F ? GL_FLOAT : GL_UNSIGNED_BYTE, NULL)); OOGL(glBindTexture(GL_TEXTURE_2D, 0)); } // resize depth attachment OOGL(glBindRenderbuffer(GL_RENDERBUFFER, targetDepthBufferID)); +#if OOLITE_MAC_OS_X + // GL_DEPTH_COMPONENT32F is 3.0-core and unsupported on legacy GL; 24-bit depth is core since GL 1.4. + OOGL(glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, (GLsizei)viewSize.width, (GLsizei)viewSize.height)); +#else OOGL(glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32F, (GLsizei)viewSize.width, (GLsizei)viewSize.height)); +#endif OOGL(glBindRenderbuffer(GL_RENDERBUFFER, 0)); targetFramebufferSize.width = viewSize.width; @@ -585,8 +740,10 @@ - (void)drawTargetTextureIntoDefaultFramebuffer OOGL(glGetIntegerv(GL_CURRENT_PROGRAM, &previousProgramID)); GLint previousTextureID; OOGL(glGetIntegerv(GL_TEXTURE_BINDING_2D, &previousTextureID)); +#if !OOLITE_MAC_OS_X GLint previousVAO; OOGL(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &previousVAO)); +#endif GLint previousActiveTexture; OOGL(glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture)); @@ -594,9 +751,12 @@ - (void)drawTargetTextureIntoDefaultFramebuffer // fixes transparency issue for some reason OOGL(glDisable(GL_BLEND)); - GLhandleARB program = [textureProgram program]; - GLhandleARB blur = [blurProgram program]; - GLhandleARB final = [finalProgram program]; + /* GLuint, not GLhandleARB: Apple's GL headers typedef GLhandleARB to + void*, while every entry point used below takes GLuint (Mesa's + spelling, used by the rest of the tree). */ + GLuint program = (GLuint)(uintptr_t)[textureProgram program]; + GLuint blur = (GLuint)(uintptr_t)[blurProgram program]; + GLuint final = (GLuint)(uintptr_t)[finalProgram program]; NSSize viewSize = [gameView backingViewSize]; float fboResolution[2] = { viewSize.width, viewSize.height }; @@ -607,25 +767,40 @@ - (void)drawTargetTextureIntoDefaultFramebuffer OOGL(glBindTexture(GL_TEXTURE_2D, targetTextureID)); OOGL(glUniform1i(glGetUniformLocation(program, "image"), 0)); +#if OOLITE_MAC_OS_X + BindPostFXQuadArrays(quadTextureVBO, quadTextureEBO); + OOGL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)); + UnbindPostFXQuadArrays(); +#else OOGL(glBindVertexArray(quadTextureVAO)); OOGL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)); OOGL(glBindVertexArray(0)); +#endif OOGL(glBindFramebuffer(GL_FRAMEBUFFER, defaultDrawFBO)); BOOL horizontal = YES, firstIteration = YES; unsigned int amount = [self bloom] ? 10 : 0; // if not blooming, why bother with the heavy calculations? OOGL(glUseProgram(blur)); + // legacy GLSL has no textureSize(); the 1.20 blur shader takes the texel size as a uniform + // (setting a nonexistent uniform is a no-op on the 3.3 path) + OOGL(glUniform2f(glGetUniformLocation(blur, "texelSize"), 1.0f / viewSize.width, 1.0f / viewSize.height)); for (unsigned int i = 0; i < amount; i++) { OOGL(glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal])); OOGL(glUniform1i(glGetUniformLocation(blur, "horizontal"), horizontal)); OOGL(glActiveTexture(GL_TEXTURE0)); // bind texture of other framebuffer (or scene if first iteration) OOGL(glBindTexture(GL_TEXTURE_2D, firstIteration ? passthroughTextureID[1] : pingpongColorbuffers[!horizontal])); - OOGL(glUniform1i(glGetUniformLocation([blurProgram program], "imageIn"), 0)); + OOGL(glUniform1i(glGetUniformLocation((GLuint)(uintptr_t)[blurProgram program], "imageIn"), 0)); +#if OOLITE_MAC_OS_X + BindPostFXQuadArrays(quadTextureVBO, quadTextureEBO); + OOGL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)); + UnbindPostFXQuadArrays(); +#else OOGL(glBindVertexArray(quadTextureVAO)); OOGL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)); OOGL(glBindVertexArray(0)); +#endif horizontal = !horizontal; firstIteration = NO; } @@ -654,8 +829,15 @@ - (void)drawTargetTextureIntoDefaultFramebuffer OOGL(glUniform1i(glGetUniformLocation(final, "bloomBlur"), 1)); OOGL(glUniform1f(glGetUniformLocation(final, "uSaturation"), [gameView colorSaturation])); +#if OOLITE_MAC_OS_X + BindPostFXQuadArrays(quadTextureVBO, quadTextureEBO); + OOGL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)); + // restoring attribute-enable state protects the fixed-function material path + UnbindPostFXQuadArrays(); +#else OOGL(glBindVertexArray(quadTextureVAO)); OOGL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)); +#endif // restore GL_TEXTURE1 to 0, just in case we are returning from a // DETAIL_LEVEL_NORMAL to DETAIL_LEVEL_SHADERS @@ -666,7 +848,9 @@ - (void)drawTargetTextureIntoDefaultFramebuffer OOGL(glActiveTexture(previousActiveTexture)); OOGL(glBindTexture(GL_TEXTURE_2D, previousTextureID)); OOGL(glUseProgram(previousProgramID)); +#if !OOLITE_MAC_OS_X OOGL(glBindVertexArray(previousVAO)); +#endif OOGL(glEnable(GL_BLEND)); } @@ -731,9 +915,16 @@ - (id)initWithGameView:(MyOpenGLView*)inGameView autoSave = [prefs oo_boolForKey:@"autosave" defaultValue:NO]; wireframeGraphics = [prefs oo_boolForKey:@"wireframe-graphics" defaultValue:NO]; doProcedurallyTexturedPlanets = [prefs oo_boolForKey:@"procedurally-textured-planets" defaultValue:YES]; +#if OOLITE_MAC_OS_X + // Legacy GL has no glTexImage2DMultisample; postFX runs non-MSAA (Task 8). + [inGameView setMsaa:NO]; + OOLog(@"MSAA.setup", @"Multisample anti-aliasing disabled (legacy-GL mode)."); +#else [inGameView setMsaa:[prefs oo_boolForKey:@"anti-aliasing" defaultValue:NO]]; OOLog(@"MSAA.setup", @"Multisample anti-aliasing %@requested.", [inGameView msaa] ? @"" : @"not "); - [inGameView setFov:OOClamp_0_max_f([prefs oo_floatForKey:@"fov-value" defaultValue:57.2f], MAX_FOV_DEG) fromFraction:NO]; +#endif + [inGameView setFov:OOClamp_0_max_f([prefs oo_floatForKey:@"fov-value" defaultValue:57.2f], MAX_FOV_DEG) + fromFraction:NO]; if ([inGameView fov:NO] < MIN_FOV_DEG) [inGameView setFov:MIN_FOV_DEG fromFraction:NO]; @@ -741,7 +932,7 @@ - (id)initWithGameView:(MyOpenGLView*)inGameView // Set up speech synthesizer. #if OOLITE_SPEECH_SYNTH -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ /* @@ -887,7 +1078,7 @@ - (void)dealloc #if OOLITE_SPEECH_SYNTH [speechArray release]; -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT [speechSynthesizer release]; #elif OOLITE_ESPEAK espeak_Cancel(); @@ -6296,7 +6487,7 @@ - (void)speakWithSubstitutions:(NSString*)text NSString* original_phrase = [thePair oo_stringAtIndex:0]; NSUInteger replacementIndex; -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT replacementIndex = 1; #elif OOLITE_ESPEAK replacementIndex = [thePair count] > 2 ? 2 : 1; @@ -9050,7 +9241,7 @@ - (void)setAirResistanceFactor:(GLfloat)newFactor } // speech routines -#if OOLITE_MAC_OS_X +#if OOLITE_MAC_APPKIT - (void)startSpeakingString:(NSString*)text { diff --git a/src/Core/meson.build b/src/Core/meson.build index dbdf0c1bb3..299d7f27f4 100644 --- a/src/Core/meson.build +++ b/src/Core/meson.build @@ -48,6 +48,7 @@ oolite_sources += files( 'OOExcludeObjectEnumerator.m', 'OOFilteringEnumerator.m', 'OOGraphicsResetManager.m', + 'OOFullScreenController.m', 'OOHPVector.m', 'OOIsNumberLiteral.m', 'OOJoystickManager.m', diff --git a/src/SDL/MyOpenGLView.m b/src/SDL/MyOpenGLView.m index babcc202ef..939bb3a61a 100644 --- a/src/SDL/MyOpenGLView.m +++ b/src/SDL/MyOpenGLView.m @@ -41,6 +41,10 @@ #include #include +#if OOLITE_MAC_OS_X +#import +#endif + #define STB_IMAGE_WRITE_IMPLEMENTATION #import "stb_image_write.h" @@ -50,8 +54,28 @@ #include +#if OOLITE_MAC_OS_X +/* Refresh-period swap pacing state (see createWindowWithSize for why the + regular swap-interval mechanisms are unusable on this OS). Written and + read only from the main thread. */ +static double sSwapPacingSeconds = 0.0; // 0 = pacing disabled +static uint64_t sLastSwapMachTime = 0; + +static double OODisplayRefreshSeconds(SDL_Window* window) +{ + const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(window)); + if (mode != NULL && mode->refresh_rate > 10.0f) + return 1.0 / (double)mode->refresh_rate; + return 1.0 / 60.0; // sane fallback +} +#endif + @interface MyOpenGLView (OOPrivate) +#if OOLITE_MAC_OS_X +- (void)swapWindow; // all darwin buffer swaps go through here (paces to display refresh) +#endif + @end @implementation MyOpenGLView @@ -190,11 +214,34 @@ - (void)createWindowWithSize:(NSSize)size OOLog(@"sdl.create_context", @"%@", @"Could not create OpenGL context"); exit(1); } +#if OOLITE_MAC_OS_X + /* SDL_SetWindowSurfaceVSync below only affects the SDL_GetWindowSurface + presentation path, and on macOS the window never enters surface mode + (see the guarded block above). SDL_GL_SetSwapInterval is also + unusable here: SDL3 implements cocoa GL v-sync in software through a + CVDisplayLink whose creation/start return values are never checked, + and on this OS CVDisplayLink creation fails outright, so the first + swap waits on its condition forever (boot hangs in the splash + screen); setting the legacy driver-level swap interval + (NSOpenGLCPSwapInterval) is accepted but not enforced by Apple's + Metal-backed GL either. Pace buffer swaps against the display's + refresh period in -swapWindow instead; that caps the frame rate to + the display refresh without ever hard-blocking on a signal that + this system may never deliver. */ + if (vSyncPreference) + sSwapPacingSeconds = OODisplayRefreshSeconds(window); +#endif +#if !OOLITE_MAC_OS_X + /* SDL3 puts a window into surface mode as soon as SDL_GetWindowSurface + is called; on macOS that competes with the NSOpenGLContext's own + presentation and the window stays black (legacy-GL build, Task 8). + The colorspace dance only matters for the Windows HDR path. */ SDL_Surface* surface = SDL_GetWindowSurface(window); if (!SDL_SetSurfaceColorspace(surface, SDL_COLORSPACE_SRGB_LINEAR)) { OOLogWARN(@"sdl.use_edr_surface", @"%@ %s", @"Failed to set SDR linear surface - falling back to SDR. Error was:", SDL_GetError()); SDL_SetSurfaceColorspace(surface, SDL_COLORSPACE_SRGB); } +#endif #if OOLITE_WINDOWS // capture the window handle for later (only needed for ugly hack later when transitioning between @@ -255,6 +302,13 @@ - (void)createWindowWithSize:(NSSize)size OOLog(@"display.initGL", @"Window Pixel Format: %s", SDL_GetPixelFormatName(format)); // Verify V-sync successfully set - report it if not +#if OOLITE_MAC_OS_X + if (vSyncPreference && sSwapPacingSeconds > 0.0) { + OOLog(@"display.initGL", @"%@", @"V-Sync set (refresh-period swap pacing)."); + } else if (vSyncPreference) { + OOLogWARN(@"display.initGL", @"%@", @"Could not determine display refresh rate; V-Sync pacing disabled."); + } +#else int hasVsync; if (vSyncPreference && (!SDL_GetWindowSurfaceVSync(window, &hasVsync) || !hasVsync)) { @@ -263,6 +317,7 @@ - (void)createWindowWithSize:(NSSize)size } else { OOLog(@"display.initGL", @"%@", @"V-Sync set"); } +#endif int width, height; SDL_GetWindowSizeInPixels(window, &width, &height); @@ -513,7 +568,11 @@ - (void)initialiseGLWithSize:(NSSize)v_size OOLog(@"display.initGL", @"Requested a new surface of %d x %d, %@.", (int)viewSize.width, (int)viewSize.height, (fullScreen ? @"fullscreen" : @"windowed")); +#if OOLITE_MAC_OS_X + [self swapWindow]; // clear the buffer before resize +#else SDL_GL_SwapWindow(window); // clear the buffer before resize +#endif SDL_SetWindowFullscreen(window, fullScreen); @@ -530,6 +589,11 @@ - (void)initialiseGLWithSize:(NSSize)v_size } #endif +#if OOLITE_MAC_OS_X + if (vSyncPreference) + sSwapPacingSeconds = OODisplayRefreshSeconds(window); // display/fullscreen switches can change the refresh rate +#endif + int pixelWidth, pixelHeight; SDL_GetWindowSizeInPixels(window, &pixelWidth, &pixelHeight); @@ -545,7 +609,11 @@ - (void)initialiseGLWithSize:(NSSize)v_size [self setUpBasicOpenGLStateWithSize]; +#if OOLITE_MAC_OS_X + [self swapWindow]; +#else SDL_GL_SwapWindow(window); +#endif squareX = 0.0f; } @@ -626,7 +694,10 @@ - (BOOL)inFullScreenMode return fullScreen; } -#ifdef GNUSTEP_BASE_LIBRARY +// The SDL fullscreen mechanism methods. Historically gated on GNUstep +// because src/SDL was the GNUstep backend; any SDL build needs them +// (OOLITE_SDL covers the Apple-Foundation SDL host too). +#if OOLITE_SDL - (void)setFullScreenMode:(BOOL)fsm { fullScreen = fsm; @@ -682,6 +753,31 @@ - (NSSize)modeAsSize:(int)sizeIndex #endif +#if OOLITE_MAC_OS_X +/* Pace to the display refresh, then swap. A pure sleep can overshoot by + about a millisecond, which is acceptable jitter for pacing; the final + sub-millisecond stretch busy-waits so the average frame rate lands on + the refresh rate rather than a millisecond or two under it. */ +- (void)swapWindow +{ + if (sSwapPacingSeconds > 0.0 && sLastSwapMachTime != 0) { + static mach_timebase_info_data_t timebase; + if (timebase.denom == 0) + mach_timebase_info(&timebase); + + uint64_t now = mach_absolute_time(); + double elapsed = ((double)(now - sLastSwapMachTime) * (double)timebase.numer / (double)timebase.denom) * 1e-9; + double remaining = sSwapPacingSeconds - elapsed; + if (remaining > 0.0015) + [NSThread sleepForTimeInterval:remaining - 0.0015]; + while ((uint64_t)(sSwapPacingSeconds * 1e9) > ((mach_absolute_time() - sLastSwapMachTime) * timebase.numer / timebase.denom)) + ; // spin the final stretch for phase accuracy + } + sLastSwapMachTime = mach_absolute_time(); + SDL_GL_SwapWindow(window); +} +#endif + - (void)updateScreen { if (UNIVERSE) { @@ -691,7 +787,11 @@ - (void)updateScreen glClear(GL_COLOR_BUFFER_BIT); } +#if OOLITE_MAC_OS_X + [self swapWindow]; +#else SDL_GL_SwapWindow(window); +#endif } - (void)initSplashScreen @@ -791,7 +891,11 @@ - (void)initSplashScreen glEnd(); glFinish(); +#if OOLITE_MAC_OS_X + [self swapWindow]; +#else SDL_GL_SwapWindow(window); +#endif [matrixManager resetModelView]; [matrixManager syncModelView]; @@ -1008,23 +1112,49 @@ - (BOOL)snapShot:(NSString*)filename imageNo = tmpImageNo; } +#if OOLITE_MAC_OS_X + /* SDL_GetWindowSurface would flip the window into surface mode, which + competes with the NSOpenGLContext's own presentation on macOS (see + createWindowWithSize), and the GL framebuffer is never copied into + it: snapshots came out black. Describe the pixel data ourselves and + read the default framebuffer directly, mirroring the SDL path below. */ + int surfaceW = 0, surfaceH = 0; + SDL_GetWindowSizeInPixels(window, &surfaceW, &surfaceH); + Uint32 surfaceFormat = SDL_PIXELFORMAT_BGRA32; // matches GL_BGRA/GL_UNSIGNED_BYTE below + int pitch = surfaceW * 4; + OOLog(@"screenshot", @"Saving screen shot \"%@\" (%u x %u pixels).", pathToPic, surfaceW, surfaceH); +#else SDL_Surface* surface = SDL_GetWindowSurface(window); OOLog(@"screenshot", @"Saving screen shot \"%@\" (%u x %u pixels).", pathToPic, surface->w, surface->h); + int surfaceW = surface->w; + int surfaceH = surface->h; + Uint32 surfaceFormat = surface->format; int pitch = surface->pitch; - unsigned char* pixls = malloc(pitch * surface->h); +#endif + + unsigned char* pixls = malloc(pitch * surfaceH); int y; int off; - if (surface->w % 4) + if (surfaceW % 4) glPixelStorei(GL_PACK_ALIGNMENT, 1); else glPixelStorei(GL_PACK_ALIGNMENT, 4); - for (y = surface->h - 1, off = 0; y >= 0; y--, off += pitch) { - glReadPixels(0, y, surface->w, 1, GL_BGRA, GL_UNSIGNED_BYTE, pixls + off); +#if OOLITE_MAC_OS_X + /* The snapshot runs between frames, after the last buffer swap. Apple's + Metal-backed GL invalidates the back buffer on swap, so read the front + buffer (the last presented frame) instead; the back buffer reads black. */ + OOGL(glReadBuffer(GL_FRONT)); +#endif + for (y = surfaceH - 1, off = 0; y >= 0; y--, off += pitch) { + glReadPixels(0, y, surfaceW, 1, GL_BGRA, GL_UNSIGNED_BYTE, pixls + off); } +#if OOLITE_MAC_OS_X + OOGL(glReadBuffer(GL_BACK)); +#endif - tmpSurface = SDL_CreateSurfaceFrom(surface->w, surface->h, surface->format, pixls, surface->pitch); + tmpSurface = SDL_CreateSurfaceFrom(surfaceW, surfaceH, surfaceFormat, pixls, pitch); #if SNAPSHOTS_PNG_FORMAT if (!SDL_SavePNG(tmpSurface, [pathToPic UTF8String])) { OOLog(@"screenshotPNG", @"Failed to save %@", pathToPic); @@ -1052,15 +1182,15 @@ - (BOOL)snapShot:(NSString*)filename fileExtension = SNAPSHOTHDR_EXTENSION_DEFAULT; } - NSString* pathToPicHDR = [pathToPic stringByReplacingString:@".png" withString:fileExtension]; - OOLog(@"screenshot", @"Saving screen shot \"%@\" (%u x %u pixels).", pathToPicHDR, surface->w, surface->h); - GLfloat* pixlsf = (GLfloat*)malloc(pitch * surface->h * sizeof(GLfloat)); - for (y = surface->h - 1, off = 0; y >= 0; y--, off += pitch) { - glReadPixels(0, y, surface->w, 1, GL_RGB, GL_FLOAT, pixlsf + off); + NSString* pathToPicHDR = [pathToPic stringByReplacingOccurrencesOfString:@".png" withString:fileExtension]; + OOLog(@"screenshot", @"Saving screen shot \"%@\" (%u x %u pixels).", pathToPicHDR, surfaceW, surfaceH); + GLfloat* pixlsf = (GLfloat*)malloc(pitch * surfaceH * sizeof(GLfloat)); + for (y = surfaceH - 1, off = 0; y >= 0; y--, off += pitch) { + glReadPixels(0, y, surfaceW, 1, GL_RGB, GL_FLOAT, pixlsf + off); } - if (([fileExtension isEqual:SNAPSHOTHDR_EXTENSION_EXR] && SaveEXRSnapshot([pathToPicHDR cStringUsingEncoding:NSUTF8StringEncoding], surface -> w, surface -> h, pixlsf) != 0) // TINYEXR_SUCCESS - || ([fileExtension isEqual:SNAPSHOTHDR_EXTENSION_HDR] && !stbi_write_hdr([pathToPicHDR cStringUsingEncoding:NSUTF8StringEncoding], surface -> w, surface -> h, 3, pixlsf))) { + if (([fileExtension isEqual:SNAPSHOTHDR_EXTENSION_EXR] && SaveEXRSnapshot([pathToPicHDR cStringUsingEncoding:NSUTF8StringEncoding], surfaceW, surfaceH, pixlsf) != 0) // TINYEXR_SUCCESS + || ([fileExtension isEqual:SNAPSHOTHDR_EXTENSION_HDR] && !stbi_write_hdr([pathToPicHDR cStringUsingEncoding:NSUTF8StringEncoding], surfaceW, surfaceH, 3, pixlsf))) { OOLog(@"screenshotHDR", @"Failed to save %@", pathToPicHDR); snapShotOK = NO; } diff --git a/src/SDL/main.m b/src/SDL/main.m index 1e85444e9e..79e8105b74 100644 --- a/src/SDL/main.m +++ b/src/SDL/main.m @@ -22,17 +22,18 @@ */ -#ifdef GNUSTEP_BASE_LIBRARY -#import -#if (GNUSTEP_BASE_MAJOR_VERSION == 1 && (GNUSTEP_BASE_MINOR_VERSION == 24 && GNUSTEP_BASE_SUBMINOR_VERSION >= 9) || (GNUSTEP_BASE_MINOR_VERSION > 24)) || (GNUSTEP_BASE_MAJOR_VERSION > 1) -#import -#endif +// SDL3 contract: on platforms where SDL supplies the process entry point +// (Windows), SDL_main.h renames main and provides the real one; on Linux +// and macOS it only adds declarations. +#include + +#import +#include + #import "GameController.h" #import "OOLoggingExtended.h" -#import #if OOLITE_WINDOWS -#include #include #include // Make sure that a high performance GPU is @@ -43,9 +44,14 @@ #include #endif -GameController* controller; +// Apple Foundation does not define this convenience macro (GNUstep does). +#ifndef STRINGIFY +#define OO_STRINGIFY_INNER(x) #x +#define STRINGIFY(x) OO_STRINGIFY_INNER(x) #endif +GameController* controller; + #ifndef NDEBUG uint32_t gDebugFlags = 0; #endif @@ -57,22 +63,24 @@ /** * \ingroup cli - * Entry point for Linux and Windows systems. - * Initializes logging. If -load is passed, the argument after that is loaded - * as savegame. + * Entry point for the SDL backends (Linux, Windows and the Apple-Foundation + * build). Initializes logging. If -load is passed, the argument after that is + * loaded as savegame. * * @param argc the number of command line arguments * @param argv the string array values of the command line arguments * @return returns 0 on success, or EXITFAILURE when an exception is caught */ +#if OOLITE_SDL int main(int argc, char* argv[]) { -#ifdef GNUSTEP_BASE_LIBRARY int i; +#ifdef GNUSTEP_BASE_LIBRARY #if (GNUSTEP_BASE_MAJOR_VERSION == 1 && (GNUSTEP_BASE_MINOR_VERSION == 24 && GNUSTEP_BASE_SUBMINOR_VERSION >= 9) || (GNUSTEP_BASE_MINOR_VERSION > 24)) || (GNUSTEP_BASE_MAJOR_VERSION > 1) [NSDate class]; // See github issue #202 #endif +#endif #if OOLITE_WINDOWS @@ -218,14 +226,14 @@ int main(int argc, char* argv[]) DESTROY(pool); // Call applicationDidFinishLaunching because NSApp is not running in - // GNUstep port. + // the SDL ports; this explicit bootstrap replaces the NIB-driven call. [controller applicationDidFinishLaunching:nil]; } @catch (NSException* exception) { OOLogERR(kOOLogException, @"Root exception handler hit - terminating. This is an internal error, please report it. Exception name: %@, reason: %@", [exception name], [exception reason]); return EXIT_FAILURE; } -#endif +#endif /* OOLITE_SDL */ // never reached return 0; diff --git a/src/meson.build b/src/meson.build index 6c3dd2fb36..a5c24e0296 100644 --- a/src/meson.build +++ b/src/meson.build @@ -30,8 +30,8 @@ oolite_bin = executable( override_options: oolite_override_options, link_args: oolite_link_args, win_subsystem: 'windows', - build_rpath: '$ORIGIN', - install_rpath: '$ORIGIN', + build_rpath: host_os == 'darwin' ? '@executable_path/../Frameworks' : '$ORIGIN', + install_rpath: host_os == 'darwin' ? '@executable_path/../Frameworks' : '$ORIGIN', install: false, ) @@ -52,11 +52,15 @@ post_build_args = [ version_vars.get('buildtime'), gnustep_folder, stamp_file, + # 14th argument: the substituted macOS bundle Info.plist (see root + # meson.build); consumed by ShellScripts/Darwin/post_build_fn.sh, ignored + # by the other platform post-builds. + meson.global_build_root() / 'Info.plist', ] post_build_script = files('../ShellScripts/common/post_build.sh') generate_manifest_script = files('../ShellScripts/common/generate_manifest_fn.sh') oolite_app_bundle = custom_target('post_build', - input: oolite_bin, + input: [oolite_bin, macos_info_plist], output: stamp_file, command: [ bash_prog, @@ -67,12 +71,18 @@ oolite_app_bundle = custom_target('post_build', build_by_default: true, install: false, ) -install_args = [ - prog_dir / appname, - get_option('bindir'), - get_option('datadir'), - host_os, - get_option('deployment_release') ? 'yes' : 'no', -] -install_script = files('../ShellScripts/common/install.sh') -meson.add_install_script(bash_prog, install_script, install_args) +# The install script targets an FHS-style prefix layout. On darwin the product +# of the build is the .app bundle assembled by the post_build step above, so +# there is nothing for `meson install` to do. (Upstream question: a future +# bundle-install rule could replace this skip.) +if host_os != 'darwin' + install_args = [ + prog_dir / appname, + get_option('bindir'), + get_option('datadir'), + host_os, + get_option('deployment_release') ? 'yes' : 'no', + ] + install_script = files('../ShellScripts/common/install.sh') + meson.add_install_script(bash_prog, install_script, install_args) +endif diff --git a/src/meson/darwin/meson.build b/src/meson/darwin/meson.build new file mode 100644 index 0000000000..eea43af695 --- /dev/null +++ b/src/meson/darwin/meson.build @@ -0,0 +1,100 @@ +# Darwin (macOS) specific — Apple Foundation + Apple ld. No GNUstep. +# +# Foundation is hard-selected: there is deliberately no 'foundation' build +# option (upstream doctrine — no speculative second-config surface). The +# GNUstep-on-macOS path survives only as the coexistence-probe contingency +# documented in the port notes, not as a build configuration; its code is +# preserved in this file's git history and in src/meson/linux/meson.build. + +add_project_arguments('-DXP_UNIX', language: c_family) + +# NOTE: -DLINUX is deliberately NOT set. OOFoundation.h maps "#ifdef LINUX" +# to OOLITE_LINUX=1, which would misdefine the platform on darwin. + +oolite_dependencies += declare_dependency(link_args: [ + '-framework', 'OpenGL', '-framework', 'Cocoa', '-framework', 'CoreAudio', + '-framework', 'AudioToolbox', '-framework', 'AudioUnit', '-framework', + 'IOKit', '-framework', 'Carbon', '-framework', 'Foundation', + '-framework', 'CoreServices', +]) +# Defensive: the ObjC runtime is not guaranteed transitive through Foundation. +oolite_dependencies += cc.find_library('objc', required: true) + +# Apple Foundation path. House prefix is OOLITE_MACOS_, not OO_. +# - OOLITE_SDL must come from the command line: OOFoundation.h defines it only +# under OOLITE_GNUSTEP, and its #ifndef fallback is 0 — without this define +# the SDL backend silently compiles away. +# - GL_GLEXT_PROTOTYPES makes SDL3's vendored glext header supply the +# GL 3.0-era prototypes/tokens that Apple's legacy gl.h omits; without it +# OOOpenGLOnly.h fails to compile. +# - OOLITE_MAC_APPKIT is deliberately NOT defined here. It lives only in +# OOFoundation.h as a compound macro (with #ifndef->0 fallback); defining it +# on the command line too is a redefinition-warning storm. +# - OOLITE_NO_CGL_MACRO keeps OO_ENTER_OPENGL a no-op: the Meson tree's GL +# call sites follow the portable pattern (plain GL functions), not the +# Xcode backend's OO_ENTER_OPENGL discipline the CGL macros require, and +# the CGL macro table is mutually exclusive with the GL 3.3 core renderer +# (Apple's CGLMacro.h #errors if gl3.h was included first). +add_project_arguments( + '-DOOLITE_MACOS_APPLE_FOUNDATION=1', + '-DOOLITE_SDL=1', + '-DGL_GLEXT_PROTOTYPES=1', + '-DOOLITE_NO_CGL_MACRO=1', + language: c_family, +) + +# Apple-Foundation compatibility shims (src/Core/Compat/OOFoundationCompat.h, +# included by OOFoundation.h under OOLITE_MACOS_APPLE_FOUNDATION). Entries are +# strictly audit-driven; the header documents the rules. +oolite_includes += include_directories('../../../src/Core/Compat') +# Spelling note: meson rejects absolute paths into the source tree ("Tried to +# form an absolute path to a dir in the source tree"), so the path is relative +# to THIS subdir (src/meson/darwin/): three levels up is the project root. + +fs = import('fs') +project_root = meson.project_source_root() + +# ===================================================================== +# MOZILLA JS BLOCK (Scan & Resolve) +# ===================================================================== +# Pinned to the release artifact name on BOTH flavors: jsdbg_static is never +# packaged (the debug artifact ships the same js_static lib — darwin debug +# builds link the release lib). +js_name = 'js_static' +js_search_roots = [ + project_root / 'build/mozilla_js', + fs.expanduser('~') / '.local', + '/usr/local', +] +found_js_dir = '' +found_js_inc = '' + +foreach root : js_search_roots + if found_js_dir == '' + foreach sub_dir : ['lib', 'lib64'] + current_dir = root / sub_dir + if fs.is_file(current_dir / 'lib' + js_name + '.a') + found_js_dir = current_dir + found_js_inc = root / 'include' + break + endif + endforeach + endif +endforeach + +if found_js_dir != '' + js_lib = cc.find_library(js_name, dirs: [found_js_dir], static: true, required: true) + # Meson rejects absolute include paths into the source tree ("Tried to + # form an absolute path to a dir in the source tree"), and the primary + # artifact root (build/mozilla_js) is in-tree. Spell that case relative to + # THIS subdir (src/meson/darwin/); out-of-tree roots pass through as-is. + if found_js_dir.startswith(project_root) + js_inc = '../../../' + fs.relative_to(found_js_inc, project_root) + else + js_inc = found_js_inc + endif + oolite_includes += include_directories(js_inc, is_system: true) + oolite_dependencies += declare_dependency(dependencies: [js_lib]) +else + error('Required dependency "@0@" was not found in SEARCH_ROOTS. Run ShellScripts/Darwin/install_mozilla_js.sh to fetch the prebuilt artifact.'.format(js_name)) +endif diff --git a/src/meson/meson.build b/src/meson/meson.build index 320aa899dd..24b0f6ce11 100644 --- a/src/meson/meson.build +++ b/src/meson/meson.build @@ -3,6 +3,8 @@ ccid = cc.get_id() if (host_os == 'windows' or host_os == 'cygwin') subdir('windows') +elif host_os == 'darwin' + subdir('darwin') else subdir('linux') endif @@ -14,38 +16,46 @@ endif if get_option('b_lto') if ccid == 'clang' add_project_arguments('-flto=thin', language: c_family) - oolite_link_args += [ - '-flto=thin', - '-Wl,--thinlto-cache-dir=thinlto_cache', - ] + oolite_link_args += ['-flto=thin'] + # --thinlto-cache-dir is lld-only; Apple ld spells the same option + # -cache_path_lto (see ld(1)). Both resolve relative to the build dir. + if host_os == 'darwin' + oolite_link_args += ['-Wl,-cache_path_lto,thinlto_cache'] + else + oolite_link_args += ['-Wl,--thinlto-cache-dir=thinlto_cache'] + endif else add_project_arguments('-flto=auto', language: c_family) oolite_link_args += ['-flto=auto'] endif endif -add_project_arguments( - '-DGNUSTEP_BASE_LIBRARY=1', - language: c_family, -) +if host_os != 'darwin' + add_project_arguments( + '-DGNUSTEP_BASE_LIBRARY=1', + language: c_family, + ) +endif add_project_arguments( '-fexceptions', '-fobjc-exceptions', language: 'objc', ) -if ccid == 'clang' - add_project_arguments( - '-DGNUSTEP_RUNTIME=1', - '-D_NONFRAGILE_ABI=1', - '-fobjc-runtime=gnustep-2.2', - '-fblocks', - language: 'objc', - ) -else - add_project_arguments( - '-DGNU_RUNTIME=1', - '-fconstant-string-class=NSConstantString', - language: 'objc', - ) +if host_os != 'darwin' + if ccid == 'clang' + add_project_arguments( + '-DGNUSTEP_RUNTIME=1', + '-D_NONFRAGILE_ABI=1', + '-fobjc-runtime=gnustep-2.2', + '-fblocks', + language: 'objc', + ) + else + add_project_arguments( + '-DGNU_RUNTIME=1', + '-fconstant-string-class=NSConstantString', + language: 'objc', + ) + endif endif diff --git a/tests/t7_speech_probe.py b/tests/t7_speech_probe.py new file mode 100644 index 0000000000..a0b4ffa436 --- /dev/null +++ b/tests/t7_speech_probe.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Task 7 (audio+speech parity) speech probe: attach to the Oolite debug +console over TCP and trigger the espeak-ng speech path. + +Uses the debug-flavor-only callObjC bridge (OOJSConsole.m, #if OO_DEBUG) to +invoke -[Universe startSpeakingString:] / -[Universe isSpeaking] directly, +bypassing the per-savegame speech_on gate (no JS/ObjC setter exists). + +Protocol mirrors tests/launch_snapshot.py (Oolite debug console plist +framing). Real audio is used deliberately (no SDL dummy driver, no +ALSOFT_DRIVERS=null): espeak-ng outputs via pcaudiolib/CoreAudio and OpenAL +Soft opens the CoreAudio device directly. +""" + +import os +import plistlib +import select +import socket +import struct +import subprocess +import sys +import time + +PORT = 8563 +HOST = "127.0.0.1" +REQUEST_CONNECTION = "Request Connection" +APPROVE_CONNECTION = "Approve Connection" +PERFORM_COMMAND = "Perform Command" +CONSOLE_OUTPUT = "Console Output" +PACKET_TYPE_KEY = "packet type" +MESSAGE_KEY = "message" +CONSOLE_IDENTITY_KEY = "console identity" + + +def send_plist_packet(sock, packet): + data = plistlib.dumps(packet, fmt=plistlib.FMT_XML) + sock.sendall(struct.pack(">I", len(data)) + data) + + +def receive_plist_packet(sock, timeout=5.0): + sock.settimeout(timeout) + header = b"" + while len(header) < 4: + chunk = sock.recv(4 - len(header)) + if not chunk: + return None + header += chunk + length = struct.unpack(">I", header)[0] + data = b"" + while len(data) < length: + chunk = sock.recv(length - len(data)) + if not chunk: + break + data += chunk + return plistlib.loads(data) + + +def drain_console_output(sock, seconds): + """Collect Console Output packets for a bounded window.""" + lines = [] + end = time.time() + seconds + while time.time() < end: + r, _, _ = select.select([sock], [], [], 0.5) + if not r: + continue + try: + pkt = receive_plist_packet(sock, timeout=1.0) + except Exception: + continue + if pkt is None: + break + if pkt.get(PACKET_TYPE_KEY) == CONSOLE_OUTPUT: + msg = pkt.get(MESSAGE_KEY, "") + lines.append(msg if isinstance(msg, str) else str(msg)) + return lines + + +def main(): + binary = sys.argv[1] + server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_sock.bind((HOST, PORT)) + server_sock.listen(1) + + env = os.environ.copy() + env["LIBGL_ALWAYS_SOFTWARE"] = "1" + # Real audio: SDL audio + OpenAL Soft + espeak all hit CoreAudio. Real + # video: the offscreen SDL driver cannot provide a GL context on macOS. + env.pop("SDL_AUDIODRIVER", None) + env.pop("ALSOFT_DRIVERS", None) + + print(f"[*] Launching {binary}") + proc = subprocess.Popen( + [binary, "--no-splash"], env=env, stdout=sys.stdout, stderr=sys.stderr + ) + + conn = None + ok = False + try: + timeout = time.time() + 30 + while time.time() < timeout: + readable, _, _ = select.select([server_sock], [], [], 1) + if readable: + conn, _ = server_sock.accept() + conn.setblocking(True) + break + if not conn: + print("[!] Oolite failed to connect to the console server.") + return 1 + + pkt = receive_plist_packet(conn) + if not pkt or pkt.get(PACKET_TYPE_KEY) != REQUEST_CONNECTION: + print("[!] Handshake failed.") + return 1 + send_plist_packet( + conn, + { + PACKET_TYPE_KEY: APPROVE_CONNECTION, + CONSOLE_IDENTITY_KEY: "T7SpeechProbe", + }, + ) + print("[+] Connected; waiting for universe init...") + time.sleep(8) + + # Trigger the espeak path directly on the Universe object. + speak_js = ( + "var _t7r = universe.callObjC('startSpeakingString:', " + "'Oolite speech parity probe. Alpha one, two, three.'); " + "console.log('T7_SPEAK_RETURN=' + _t7r);" + ) + send_plist_packet(conn, {PACKET_TYPE_KEY: PERFORM_COMMAND, MESSAGE_KEY: speak_js}) + out1 = drain_console_output(conn, 4) + for line in out1: + print(f"[console] {line.strip()}") + + # While synthesis should still be playing, ask for the state. + send_plist_packet( + conn, + { + PACKET_TYPE_KEY: PERFORM_COMMAND, + MESSAGE_KEY: "console.log('T7_IS_SPEAKING=' + universe.callObjC('isSpeaking'));", + }, + ) + out2 = drain_console_output(conn, 6) + for line in out2: + print(f"[console] {line.strip()}") + + blob = "\n".join(out1 + out2) + speak_seen = "T7_SPEAK_RETURN=" in blob + speaking_seen = "T7_IS_SPEAKING=" in blob + print(f"[*] speak call dispatched: {speak_seen}; isSpeaking probe: {speaking_seen}") + ok = speak_seen and speaking_seen + + send_plist_packet( + conn, + {PACKET_TYPE_KEY: PERFORM_COMMAND, MESSAGE_KEY: "quit();"}, + ) + try: + proc.wait(timeout=15) + except subprocess.TimeoutExpired: + proc.kill() + return 0 if ok else 1 + finally: + if conn: + conn.close() + server_sock.close() + if proc.poll() is None: + proc.kill() + + +if __name__ == "__main__": + sys.exit(main())