Last active
November 23, 2025 10:20
-
-
Save g-l-i-t-c-h-o-r-s-e/59eefeef5fd3355a0ccef6fe2dd5c30f to your computer and use it in GitHub Desktop.
Render ISF Fragment Shaders as an Image in Quartz Composer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # --- config (override via env) --- | |
| NAME="${NAME:-ISFRenderer}" | |
| CLASS="${CLASS:-ISFRendererPlugIn}" | |
| # All source files for this plug-in (override with SRCS in env if needed) | |
| SRCS=(${SRCS:-${CLASS}.m ISFRendererPlugInViewController.m}) | |
| PLUG="$NAME.plugin" | |
| OUT="$(pwd)/build-manual" | |
| INST="$HOME/Library/Graphics/Quartz Composer Plug-Ins" | |
| XCODE_APP="${XCODE_APP:-/Applications/Xcode_9.4.1.app}" | |
| DEV="$XCODE_APP/Contents/Developer" | |
| SDKDIR="$DEV/Platforms/MacOSX.platform/Developer/SDKs" | |
| # Allow env to override; if still empty later, we auto-select based on SDK | |
| DO_I386="${DO_I386:-}" | |
| DO_X64="${DO_X64:-1}" | |
| # Clean output directory first (set CLEAN=0 to keep) | |
| CLEAN="${CLEAN:-1}" | |
| # Prefer 10.14, fall back to 10.13, else xcrun | |
| SDK="${SDK:-}" | |
| if [[ -z "${SDK}" ]]; then | |
| if [[ -d "$SDKDIR/MacOSX10.14.sdk" ]]; then SDK="$SDKDIR/MacOSX10.14.sdk" | |
| elif [[ -d "$SDKDIR/MacOSX10.13.sdk" ]]; then SDK="$SDKDIR/MacOSX10.13.sdk" | |
| else SDK="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)" | |
| fi | |
| fi | |
| [[ -d "$DEV" ]] || { echo "Xcode not found: $XCODE_APP"; exit 1; } | |
| # Verify all sources exist | |
| for s in "${SRCS[@]}"; do | |
| [[ -f "$s" ]] || { echo "Source not found: $s"; exit 1; } | |
| done | |
| [[ -n "${SDK:-}" && -d "$SDK" ]] || { echo "macOS SDK not found."; exit 1; } | |
| export DEVELOPER_DIR="$DEV" | |
| echo "Using SDK: $SDK" | |
| # Clean build output so there are no stale slices | |
| if [[ "$CLEAN" == "1" ]]; then | |
| echo "Cleaning: rm -rf '$OUT'" | |
| rm -rf "$OUT" | |
| fi | |
| mkdir -p "$OUT/i386" "$OUT/x86_64" "$OUT/universal/$PLUG/Contents/MacOS" | |
| # Arch toggles | |
| # Default: skip i386 on 10.14 SDK; allow override with DO_I386=0/1 | |
| if [[ -z "${DO_I386}" ]]; then | |
| if [[ "$SDK" == *"10.14.sdk"* ]]; then | |
| DO_I386=0 | |
| else | |
| DO_I386=1 | |
| fi | |
| fi | |
| # Minimum OS (override with DEPLOY=10.10, etc.) | |
| DEPLOY="${DEPLOY:-10.14}" | |
| COMMON_CFLAGS=( | |
| -bundle -fobjc-arc -fobjc-link-runtime | |
| -isysroot "$SDK" | |
| -mmacosx-version-min="$DEPLOY" | |
| -I . | |
| ) | |
| COMMON_LIBS=( | |
| -framework Foundation | |
| -framework Quartz | |
| -framework OpenGL | |
| -framework AppKit | |
| -framework QuartzCore | |
| ) | |
| # ---- compile (track what we actually built) ---- | |
| BUILT_I386=0 | |
| BUILT_X64=0 | |
| if [[ "$DO_I386" == "1" ]]; then | |
| echo "Compiling i386…" | |
| clang -arch i386 "${COMMON_CFLAGS[@]}" "${SRCS[@]}" "${COMMON_LIBS[@]}" -o "$OUT/i386/$NAME" || { | |
| echo "i386 build failed (likely unsupported by this SDK/clang). Set DO_I386=0 to skip." | |
| exit 1 | |
| } | |
| BUILT_I386=1 | |
| else | |
| echo "Skipping i386 (set DO_I386=1 to attempt, e.g. with 10.13 SDK)." | |
| fi | |
| if [[ "$DO_X64" == "1" ]]; then | |
| echo "Compiling x86_64…" | |
| clang -arch x86_64 "${COMMON_CFLAGS[@]}" "${SRCS[@]}" "${COMMON_LIBS[@]}" -o "$OUT/x86_64/$NAME" | |
| BUILT_X64=1 | |
| else | |
| echo "Skipping x86_64 (DO_X64=0)." | |
| fi | |
| # ---- gather only slices from this run ---- | |
| BINARIES=() | |
| [[ "$BUILT_I386" == "1" && -f "$OUT/i386/$NAME" ]] && BINARIES+=("$OUT/i386/$NAME") | |
| [[ "$BUILT_X64" == "1" && -f "$OUT/x86_64/$NAME" ]] && BINARIES+=("$OUT/x86_64/$NAME") | |
| if [[ "${#BINARIES[@]}" -eq 0 ]]; then | |
| echo "No binaries were built. Aborting." | |
| exit 1 | |
| fi | |
| echo "Creating bundle…" | |
| mkdir -p "$OUT/universal/$PLUG/Contents/MacOS" | |
| lipo -create "${BINARIES[@]}" -output "$OUT/universal/$PLUG/Contents/MacOS/$NAME" | |
| # ---- Info.plist ---- | |
| cat >"$OUT/universal/$PLUG/Contents/Info.plist" <<PLIST | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"><dict> | |
| <key>CFBundleDevelopmentRegion</key> <string>English</string> | |
| <key>CFBundleExecutable</key> <string>${NAME}</string> | |
| <key>CFBundleIdentifier</key> <string>com.yourdomain.${NAME}</string> | |
| <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> | |
| <key>CFBundleName</key> <string>${NAME}</string> | |
| <key>CFBundlePackageType</key> <string>BNDL</string> | |
| <key>CFBundleShortVersionString</key> <string>1.0</string> | |
| <key>CFBundleSupportedPlatforms</key> <array><string>MacOSX</string></array> | |
| <key>CFBundleVersion</key> <string>1</string> | |
| <key>QCPlugInClasses</key> | |
| <array> | |
| <string>${CLASS}</string> | |
| </array> | |
| <key>NSPrincipalClass</key> <string>QCPlugIn</string> | |
| <key>NSRequiresAquaSystemAppearance</key> <false/> | |
| </dict></plist> | |
| PLIST | |
| echo "Final binary slices:" | |
| lipo -info "$OUT/universal/$PLUG/Contents/MacOS/$NAME" || true | |
| echo "Signing…" | |
| codesign --force -s - "$OUT/universal/$PLUG" >/dev/null || true | |
| echo "Installing to: $INST" | |
| mkdir -p "$INST" | |
| rsync -a "$OUT/universal/$PLUG" "$INST/" | |
| echo "Installed: $INST/$PLUG" | |
| echo "Restart Quartz Composer to load the new plug-in." | |
| echo | |
| echo "Notes:" | |
| echo " • CLEAN=$CLEAN (set CLEAN=0 to keep previous builds)." | |
| echo " • i386 default depends on SDK (10.14 → OFF unless DO_I386=1)." | |
| echo " • Override with DO_I386=0/1, DO_X64=0/1." | |
| echo " • DEPLOY=${DEPLOY} (override with DEPLOY=10.10, etc.)." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ISFRendererPlugIn.h | |
| // Quartz Composer ISF Renderer | |
| // | |
| #import <Quartz/Quartz.h> | |
| @interface ISFRendererPlugIn : QCPlugIn | |
| { | |
| @private | |
| // GL context associated with QC | |
| CGLContextObj _cglContext; | |
| // Shader program + shaders | |
| GLuint _program; | |
| GLuint _vertexShader; | |
| GLuint _fragmentShader; | |
| // Fullscreen quad geometry (VBO only, no VAO to stay GL2-friendly) | |
| GLuint _vbo; | |
| // Render target | |
| GLuint _fbo; | |
| GLuint _colorTex; | |
| size_t _texWidth; | |
| size_t _texHeight; | |
| // ISF file tracking | |
| NSString *_currentISFPath; | |
| NSDate *_currentISFModDate; | |
| BOOL _needsReload; | |
| // Timing | |
| NSTimeInterval _startTime; | |
| // ISF metadata + dynamic QC ports | |
| NSArray *_isfInputs; // array of dicts describing ISF INPUTS | |
| NSMutableArray *_dynamicInputKeys; // QC input port keys we added | |
| // Plug-in setting: path to ISF file (on Settings tab, not a QC port) | |
| NSString *_isfPath; | |
| } | |
| // QC ports | |
| @property (assign) id<QCPlugInOutputImageProvider> outputImage; | |
| // Output size ports | |
| @property double inputWidth; | |
| @property double inputHeight; | |
| // Enable / disable rendering | |
| @property (assign) BOOL inputEnabled; | |
| // Plug-in setting (shown in Settings tab, not as a patch port): | |
| // Path to ISF fragment shader on disk (e.g. “/Users/you/Shaders/myShader.fs”) | |
| @property (copy) NSString *isfPath; | |
| @end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ISFRendererPlugIn.m | |
| // Quartz Composer ISF Renderer | |
| // | |
| // Multi-pass ISF renderer with persistent buffer ping-pong. | |
| // macOS Mojave (10.14), OpenGL 2.1 / GLSL 1.20. | |
| // | |
| #import "ISFRendererPlugIn.h" | |
| #import "ISFRendererPlugInViewController.h" | |
| #import <OpenGL/gl.h> | |
| #import <math.h> | |
| #define kQCPlugIn_Name @"ISF Renderer" | |
| #define kQCPlugIn_Description @"Renders an ISF fragment shader (single or multi-pass) to an image" | |
| // Texture release callback for texture-based QC images. | |
| // Correct QCPlugInTextureReleaseCallback signature: | |
| // void (*)(CGLContextObj cgl_ctx, GLuint name, void *releaseContext) | |
| // We keep the texture & FBO alive for the plug-in lifetime; QC just notifies us here. | |
| static void ISFTextureReleaseCallback(CGLContextObj cgl_ctx, GLuint name, void *releaseContext) | |
| { | |
| (void)cgl_ctx; | |
| (void)name; | |
| (void)releaseContext; | |
| // No-op: plugin owns texture lifetime. | |
| } | |
| // Simple FNV-1a hash of a string, used to build a stable per-ISF signature. | |
| // We hash the full, standardized path so two different files with the same | |
| // filename in different directories do NOT collide. | |
| static NSString *ISFPortSignatureForPath(NSString *path) | |
| { | |
| if (path.length == 0) { | |
| return @""; | |
| } | |
| NSString *standardPath = [path stringByStandardizingPath]; | |
| NSData *data = [standardPath dataUsingEncoding:NSUTF8StringEncoding]; | |
| if (!data) { | |
| return @""; | |
| } | |
| const uint8_t *bytes = (const uint8_t *)[data bytes]; | |
| NSUInteger len = [data length]; | |
| uint32_t hash = 2166136261u; // FNV-1a 32-bit | |
| for (NSUInteger i = 0; i < len; ++i) { | |
| hash ^= bytes[i]; | |
| hash *= 16777619u; | |
| } | |
| return [NSString stringWithFormat:@"%08x", (unsigned int)hash]; | |
| } | |
| static NSString *ISFPortKeyForInputName(NSString *inputName, NSString *signature) | |
| { | |
| if (signature.length > 0) { | |
| return [NSString stringWithFormat:@"isf_%@_%@", signature, inputName]; | |
| } else { | |
| return [NSString stringWithFormat:@"isf_%@", inputName]; | |
| } | |
| } | |
| #pragma mark - ISFBuffer helper (multi-pass targets) | |
| // Buffer object that represents a named TARGET buffer from PASSES. | |
| // For non-persistent buffers, readTex == writeTex; for persistent buffers, | |
| // we use true ping-pong: readTex is "current contents", writeTex is render | |
| // destination for the next pass that targets this buffer. | |
| @interface ISFBuffer : NSObject | |
| @property (nonatomic, assign) GLuint readTex; | |
| @property (nonatomic, assign) GLuint readFBO; | |
| @property (nonatomic, assign) GLuint writeTex; | |
| @property (nonatomic, assign) GLuint writeFBO; | |
| @property (nonatomic, assign) size_t width; | |
| @property (nonatomic, assign) size_t height; | |
| @property (nonatomic, assign) BOOL isFloat; | |
| @property (nonatomic, assign) BOOL isPersistent; | |
| @end | |
| @implementation ISFBuffer | |
| @end | |
| #pragma mark - Private interface | |
| @interface ISFRendererPlugIn () | |
| // Multi-pass / persistent state | |
| @property (nonatomic, strong) NSArray<NSDictionary *> *isfPasses; // Parsed PASSES array (each item is a pass dict) | |
| @property (nonatomic, strong) NSMutableDictionary<NSString *, ISFBuffer *> *passTargets; // TARGET name -> ISFBuffer | |
| @property (nonatomic, assign) NSUInteger frameIndex; // FRAMEINDEX uniform | |
| @property (nonatomic, assign) NSTimeInterval lastTime; // For TIMEDELTA | |
| // Scratch buffer for vertically flipped QC image inputs | |
| @property (nonatomic, assign) GLuint flipTex; | |
| @property (nonatomic, assign) GLuint flipFBO; | |
| @property (nonatomic, assign) size_t flipWidth; | |
| @property (nonatomic, assign) size_t flipHeight; | |
| @end | |
| // Helper to restore GL state at the end / on early errors. | |
| static void ISFRestoreGLState(CGLContextObj cglContext, | |
| GLint prevFBO, | |
| const GLint prevViewport[4], | |
| GLint prevProgram, | |
| GLint prevArrayBuffer, | |
| GLboolean prevDepthTest, | |
| GLboolean prevBlend) | |
| { | |
| if (!cglContext) return; | |
| CGLSetCurrentContext(cglContext); | |
| if (prevDepthTest) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); | |
| if (prevBlend) glEnable(GL_BLEND); else glDisable(GL_BLEND); | |
| glBindBuffer(GL_ARRAY_BUFFER, prevArrayBuffer); | |
| glUseProgram(prevProgram); | |
| glBindFramebuffer(GL_FRAMEBUFFER, prevFBO); | |
| glViewport(prevViewport[0], prevViewport[1], | |
| prevViewport[2], prevViewport[3]); | |
| } | |
| #pragma mark - ISFRendererPlugIn implementation | |
| // ====================================================================== | |
| // QC Plug-in metadata | |
| // ====================================================================== | |
| @implementation ISFRendererPlugIn | |
| @dynamic outputImage; | |
| @dynamic inputWidth; | |
| @dynamic inputHeight; | |
| @dynamic inputEnabled; | |
| @synthesize isfPath = _isfPath; | |
| @synthesize isfPasses = _isfPasses; | |
| @synthesize passTargets = _passTargets; | |
| @synthesize frameIndex = _frameIndex; | |
| @synthesize lastTime = _lastTime; | |
| @synthesize flipTex = _flipTex; | |
| @synthesize flipFBO = _flipFBO; | |
| @synthesize flipWidth = _flipWidth; | |
| @synthesize flipHeight = _flipHeight; | |
| // ------------------------------------------------------------------ | |
| // Plug-in metadata | |
| // ------------------------------------------------------------------ | |
| + (NSDictionary *)attributes | |
| { | |
| return @{ | |
| QCPlugInAttributeNameKey : kQCPlugIn_Name, | |
| QCPlugInAttributeDescriptionKey : kQCPlugIn_Description | |
| }; | |
| } | |
| // Attributes for input/output ports | |
| + (NSDictionary *)attributesForPropertyPortWithKey:(NSString *)key | |
| { | |
| // NOTE: there is NO inputISFPath port anymore; shader path is now | |
| // a plug-in setting (Settings tab), not a composition input. | |
| if ([key isEqualToString:@"inputWidth"]) { | |
| return @{ | |
| QCPortAttributeNameKey : @"Width", | |
| QCPortAttributeDefaultValueKey : @640.0 | |
| }; | |
| } | |
| else if ([key isEqualToString:@"inputHeight"]) { | |
| return @{ | |
| QCPortAttributeNameKey : @"Height", | |
| QCPortAttributeDefaultValueKey : @360.0 | |
| }; | |
| } | |
| else if ([key isEqualToString:@"inputEnabled"]) { | |
| return @{ | |
| QCPortAttributeNameKey : @"Enable", | |
| QCPortAttributeDefaultValueKey : @YES | |
| }; | |
| } | |
| else if ([key isEqualToString:@"outputImage"]) { | |
| return @{ | |
| QCPortAttributeNameKey : @"Image" | |
| }; | |
| } | |
| return nil; | |
| } | |
| + (QCPlugInExecutionMode)executionMode | |
| { | |
| // We are an image *provider* (no input image, only output) | |
| return kQCPlugInExecutionModeProvider; | |
| } | |
| + (QCPlugInTimeMode)timeMode | |
| { | |
| // Use the composition timebase | |
| return kQCPlugInTimeModeTimeBase; | |
| } | |
| #pragma mark - Plug-in Settings (Settings tab) | |
| // These keys define per-plug-in settings (shown in the Settings tab) | |
| + (NSArray *)plugInKeys | |
| { | |
| // A single setting: "isfPath" | |
| return @[ @"isfPath" ]; | |
| } | |
| // Human-readable label & description for each plug-in key | |
| + (NSDictionary *)attributesForPlugInKey:(NSString *)key | |
| { | |
| if ([key isEqualToString:@"isfPath"]) { | |
| return @{ | |
| QCPlugInAttributeNameKey : @"ISF Path", | |
| QCPlugInAttributeDescriptionKey : @"Filesystem path to the ISF .fs shader file" | |
| }; | |
| } | |
| return nil; | |
| } | |
| // Tell QC to use our custom Settings-tab view controller | |
| + (Class)plugInViewControllerClass | |
| { | |
| return [ISFRendererPlugInViewController class]; | |
| } | |
| // Instance method QC may call to create the view controller | |
| - (QCPlugInViewController *)createViewController | |
| { | |
| return [[ISFRendererPlugInViewController alloc] initWithPlugIn:self | |
| viewNibName:nil]; | |
| } | |
| #pragma mark - Life cycle | |
| // ====================================================================== | |
| // Life cycle | |
| // ====================================================================== | |
| - (id)init | |
| { | |
| self = [super init]; | |
| if (self) { | |
| _cglContext = NULL; | |
| _program = 0; | |
| _vertexShader = 0; | |
| _fragmentShader = 0; | |
| _vbo = 0; | |
| _fbo = 0; | |
| _colorTex = 0; | |
| _texWidth = 0; | |
| _texHeight = 0; | |
| _currentISFPath = nil; | |
| _currentISFModDate = nil; | |
| _needsReload = YES; | |
| _startTime = 0.0; | |
| _isfInputs = nil; | |
| _dynamicInputKeys = [[NSMutableArray alloc] init]; | |
| _isfPath = @""; // Settings tab default | |
| _isfPasses = nil; | |
| _passTargets = [[NSMutableDictionary alloc] init]; | |
| _frameIndex = 0; | |
| _lastTime = 0.0; | |
| _flipTex = 0; | |
| _flipFBO = 0; | |
| _flipWidth = 0; | |
| _flipHeight = 0; | |
| } | |
| return self; | |
| } | |
| - (void)dealloc | |
| { | |
| // GL cleanup happens in -stopExecution: | |
| } | |
| #pragma mark - Dynamic ports helpers | |
| // ====================================================================== | |
| // Plug-in Settings: ISF path & dynamic QC ports | |
| // ====================================================================== | |
| // Remove all previously-added dynamic ports | |
| - (void)_removeDynamicInputPorts | |
| { | |
| if (!_dynamicInputKeys) { | |
| return; | |
| } | |
| for (NSString *key in _dynamicInputKeys) { | |
| @try { | |
| [self removeInputPortForKey:key]; | |
| } | |
| @catch (NSException *ex) { | |
| } | |
| } | |
| [_dynamicInputKeys removeAllObjects]; | |
| } | |
| // Build dynamic ports from _isfInputs (parsed JSON header), | |
| // giving each ISF its own unique port key namespace so QC | |
| // does NOT reuse values across different ISF files. | |
| - (void)_rebuildDynamicInputPortsFromCurrentISFInputs | |
| { | |
| // First blow away anything that was there before | |
| [self _removeDynamicInputPorts]; | |
| if (!_isfInputs || [_isfInputs count] == 0) | |
| return; | |
| if (!_dynamicInputKeys) | |
| _dynamicInputKeys = [[NSMutableArray alloc] init]; | |
| // Compute a per-ISF signature once, based on the current isfPath. | |
| NSString *path = self.isfPath ?: @""; | |
| NSString *signature = ISFPortSignatureForPath(path); | |
| NSLog(@"[ISFRenderer] Rebuilding dynamic ports for ISF '%@' (signature %@)", path, signature); | |
| for (NSDictionary *input in _isfInputs) { | |
| NSString *name = input[@"NAME"]; | |
| NSString *type = input[@"TYPE"]; | |
| id defaultVal = input[@"DEFAULT"]; | |
| if (![name isKindOfClass:[NSString class]] || | |
| ![type isKindOfClass:[NSString class]] || | |
| name.length == 0) { | |
| continue; | |
| } | |
| // Make a QC port key that is unique per (ISF file, uniform name). | |
| NSString *portKey = ISFPortKeyForInputName(name, signature); | |
| // Decide QC port type and default value. | |
| NSString *portType = nil; | |
| id qcDefault = nil; | |
| NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; | |
| attrs[QCPortAttributeNameKey] = name; | |
| if ([type isEqualToString:@"float"] || | |
| [type isEqualToString:@"event"]) { | |
| portType = QCPortTypeNumber; | |
| double v = 0.0; | |
| if ([defaultVal respondsToSelector:@selector(doubleValue)]) | |
| v = [defaultVal doubleValue]; | |
| qcDefault = @(v); | |
| } | |
| else if ([type isEqualToString:@"bool"]) { | |
| portType = QCPortTypeBoolean; | |
| BOOL b = defaultVal ? [defaultVal boolValue] : NO; | |
| qcDefault = @(b ? 1.0 : 0.0); | |
| } | |
| else if ([type isEqualToString:@"long"] || | |
| [type isEqualToString:@"int"]) { | |
| portType = QCPortTypeNumber; | |
| NSInteger v = defaultVal ? [defaultVal integerValue] : 0; | |
| qcDefault = @(v); | |
| } | |
| else if ([type isEqualToString:@"color"]) { | |
| // Represent colors as a structure { r, g, b, a } | |
| portType = QCPortTypeStructure; | |
| CGFloat r = 0.0, g = 0.0, b = 0.0, a = 1.0; | |
| if ([defaultVal isKindOfClass:[NSArray class]]) { | |
| NSArray *arr = (NSArray *)defaultVal; | |
| if (arr.count > 0) r = [arr[0] doubleValue]; | |
| if (arr.count > 1) g = [arr[1] doubleValue]; | |
| if (arr.count > 2) b = [arr[2] doubleValue]; | |
| if (arr.count > 3) a = [arr[3] doubleValue]; | |
| } | |
| qcDefault = @{ | |
| @"r" : @(r), | |
| @"g" : @(g), | |
| @"b" : @(b), | |
| @"a" : @(a) | |
| }; | |
| } | |
| else if ([type isEqualToString:@"point2D"]) { | |
| // Represent point2D as structure { x, y } | |
| portType = QCPortTypeStructure; | |
| CGFloat x = 0.0, y = 0.0; | |
| if ([defaultVal isKindOfClass:[NSArray class]]) { | |
| NSArray *arr = (NSArray *)defaultVal; | |
| if (arr.count > 0) x = [arr[0] doubleValue]; | |
| if (arr.count > 1) y = [arr[1] doubleValue]; | |
| } | |
| qcDefault = @{ | |
| @"x" : @(x), | |
| @"y" : @(y) | |
| }; | |
| } | |
| else if ([type isEqualToString:@"image"] || | |
| [type isEqualToString:@"audio"] || | |
| [type isEqualToString:@"audioFFT"]) { | |
| // Expose these as QC image ports | |
| portType = QCPortTypeImage; | |
| NSLog(@"[ISFRenderer] Creating IMAGE INPUT port '%@' (portKey '%@') for ISF '%@'", | |
| name, portKey, path); | |
| // No default image | |
| } | |
| if (!portType) | |
| continue; | |
| if (qcDefault) | |
| attrs[QCPortAttributeDefaultValueKey] = qcDefault; | |
| @try { | |
| [self addInputPortWithType:portType | |
| forKey:portKey | |
| withAttributes:attrs]; | |
| [_dynamicInputKeys addObject:portKey]; | |
| } | |
| @catch (NSException *ex) { | |
| NSLog(@"[ISFRenderer] Exception while adding port '%@' for input '%@': %@", | |
| portKey, name, ex); | |
| } | |
| } | |
| } | |
| // Helper: read ISF file, parse JSON header, rebuild dynamic ports. | |
| // This is called whenever the Settings "ISF Path" changes. | |
| - (void)_reloadISFInputsAndDynamicPortsFromPath:(NSString *)path | |
| { | |
| // Always nuke all previous dynamic ports when the Settings path changes. | |
| [self _removeDynamicInputPorts]; | |
| _isfInputs = nil; | |
| _isfPasses = nil; | |
| if (!path || [path length] == 0) { | |
| return; | |
| } | |
| NSLog(@"[ISFRenderer] Reloading ISF inputs for path '%@'", path); | |
| NSError *readError = nil; | |
| NSString *fragSourceString = | |
| [NSString stringWithContentsOfFile:path | |
| encoding:NSUTF8StringEncoding | |
| error:&readError]; | |
| if (!fragSourceString) { | |
| NSLog(@"[ISFRenderer] Failed to read ISF at '%@': %@", path, readError); | |
| return; | |
| } | |
| // decoratedFragmentSourceFromRaw: will parse JSON header and | |
| // populate _isfInputs / _isfPasses as a side-effect. | |
| @try { | |
| (void)[self decoratedFragmentSourceFromRaw:fragSourceString]; | |
| } | |
| @catch (NSException *ex) { | |
| NSLog(@"[ISFRenderer] Exception while decorating ISF source for '%@': %@", path, ex); | |
| _isfInputs = nil; | |
| _isfPasses = nil; | |
| } | |
| if (_isfInputs && [_isfInputs count] > 0) { | |
| NSLog(@"[ISFRenderer] Parsed %lu INPUT(s) from ISF '%@'", | |
| (unsigned long)[_isfInputs count], path); | |
| [self _rebuildDynamicInputPortsFromCurrentISFInputs]; | |
| } else { | |
| NSLog(@"[ISFRenderer] No INPUTS found in ISF '%@'", path); | |
| } | |
| } | |
| // Settings property: ISF path | |
| - (void)setIsfPath:(NSString *)path | |
| { | |
| if ((_isfPath == path) || | |
| (_isfPath && path && [_isfPath isEqualToString:path])) { | |
| return; | |
| } | |
| _isfPath = [path copy] ?: @""; | |
| NSLog(@"[ISFRenderer] isfPath changed to '%@'", _isfPath); | |
| // Force shader reload on next execute | |
| _needsReload = YES; | |
| _currentISFPath = nil; | |
| _currentISFModDate = nil; | |
| // Drop any old multi-pass buffers | |
| if (_cglContext) { | |
| CGLSetCurrentContext(_cglContext); | |
| for (ISFBuffer *buf in [_passTargets allValues]) { | |
| GLuint readTex = buf.readTex; | |
| GLuint writeTex = buf.writeTex; | |
| GLuint readFBO = buf.readFBO; | |
| GLuint writeFBO = buf.writeFBO; | |
| if (readTex) { | |
| glDeleteTextures(1, &readTex); | |
| } | |
| if (writeTex && writeTex != readTex) { | |
| glDeleteTextures(1, &writeTex); | |
| } | |
| if (readFBO) { | |
| glDeleteFramebuffers(1, &readFBO); | |
| } | |
| if (writeFBO && writeFBO != readFBO) { | |
| glDeleteFramebuffers(1, &writeFBO); | |
| } | |
| buf.readTex = 0; | |
| buf.writeTex = 0; | |
| buf.readFBO = 0; | |
| buf.writeFBO = 0; | |
| } | |
| } | |
| [_passTargets removeAllObjects]; | |
| _isfPasses = nil; | |
| // Parse JSON header and rebuild QC ports immediately for this path. | |
| [self _reloadISFInputsAndDynamicPortsFromPath:_isfPath]; | |
| } | |
| - (NSString *)isfPath | |
| { | |
| return _isfPath; | |
| } | |
| #pragma mark - GL helpers | |
| // ====================================================================== | |
| // GL helpers | |
| // ====================================================================== | |
| - (void)destroyPassTargetBuffers | |
| { | |
| if (!_cglContext) | |
| return; | |
| CGLSetCurrentContext(_cglContext); | |
| for (ISFBuffer *buf in [_passTargets allValues]) { | |
| GLuint readTex = buf.readTex; | |
| GLuint writeTex = buf.writeTex; | |
| GLuint readFBO = buf.readFBO; | |
| GLuint writeFBO = buf.writeFBO; | |
| if (readTex) { | |
| glDeleteTextures(1, &readTex); | |
| } | |
| if (writeTex && writeTex != readTex) { | |
| glDeleteTextures(1, &writeTex); | |
| } | |
| if (readFBO) { | |
| glDeleteFramebuffers(1, &readFBO); | |
| } | |
| if (writeFBO && writeFBO != readFBO) { | |
| glDeleteFramebuffers(1, &writeFBO); | |
| } | |
| buf.readTex = 0; | |
| buf.writeTex = 0; | |
| buf.readFBO = 0; | |
| buf.writeFBO = 0; | |
| } | |
| [_passTargets removeAllObjects]; | |
| } | |
| - (void)destroyGLResources | |
| { | |
| if (_cglContext) { | |
| CGLSetCurrentContext(_cglContext); | |
| if (_program) { | |
| glDeleteProgram(_program); | |
| _program = 0; | |
| } | |
| if (_vertexShader) { | |
| glDeleteShader(_vertexShader); | |
| _vertexShader = 0; | |
| } | |
| if (_fragmentShader) { | |
| glDeleteShader(_fragmentShader); | |
| _fragmentShader = 0; | |
| } | |
| if (_vbo) { | |
| glDeleteBuffers(1, &_vbo); | |
| _vbo = 0; | |
| } | |
| if (_colorTex) { | |
| glDeleteTextures(1, &_colorTex); | |
| _colorTex = 0; | |
| } | |
| if (_fbo) { | |
| glDeleteFramebuffers(1, &_fbo); | |
| _fbo = 0; | |
| } | |
| _texWidth = 0; | |
| _texHeight = 0; | |
| // Multi-pass targets | |
| [self destroyPassTargetBuffers]; | |
| // Flip buffer | |
| if (_flipTex) { | |
| glDeleteTextures(1, &_flipTex); | |
| _flipTex = 0; | |
| } | |
| if (_flipFBO) { | |
| glDeleteFramebuffers(1, &_flipFBO); | |
| _flipFBO = 0; | |
| } | |
| _flipWidth = 0; | |
| _flipHeight = 0; | |
| } | |
| } | |
| - (BOOL)createQuadIfNeeded | |
| { | |
| if (!_cglContext) | |
| return NO; | |
| CGLSetCurrentContext(_cglContext); | |
| if (_vbo != 0) | |
| return YES; | |
| // Fullscreen quad in clip space (triangle strip): | |
| // (-1,-1), (1,-1), (-1,1), (1,1) | |
| static const GLfloat vertices[] = { | |
| -1.0f, -1.0f, | |
| 1.0f, -1.0f, | |
| -1.0f, 1.0f, | |
| 1.0f, 1.0f | |
| }; | |
| glGenBuffers(1, &_vbo); | |
| glBindBuffer(GL_ARRAY_BUFFER, _vbo); | |
| glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); | |
| glBindBuffer(GL_ARRAY_BUFFER, 0); | |
| return YES; | |
| } | |
| // Use GL_TEXTURE_RECTANGLE_EXT for the FBO render target. | |
| // QC sees this texture directly via outputImageProviderFromTextureWithPixelFormat:. | |
| - (BOOL)ensureRenderTargetWidth:(size_t)w height:(size_t)h | |
| { | |
| if (!_cglContext) | |
| return NO; | |
| if (w == 0 || h == 0) | |
| return NO; | |
| if (_colorTex != 0 && _fbo != 0 && _texWidth == w && _texHeight == h) | |
| return YES; | |
| // Destroy old FBO/texture, but keep program/VBO | |
| CGLSetCurrentContext(_cglContext); | |
| if (_colorTex) { | |
| glDeleteTextures(1, &_colorTex); | |
| _colorTex = 0; | |
| } | |
| if (_fbo) { | |
| glDeleteFramebuffers(1, &_fbo); | |
| _fbo = 0; | |
| } | |
| // Create rectangle texture | |
| glGenTextures(1, &_colorTex); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, _colorTex); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
| glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, | |
| 0, | |
| GL_RGBA8, | |
| (GLsizei)w, | |
| (GLsizei)h, | |
| 0, | |
| GL_BGRA, | |
| GL_UNSIGNED_INT_8_8_8_8_REV, | |
| NULL); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, 0); | |
| // Create FBO attached to that texture | |
| glGenFramebuffers(1, &_fbo); | |
| glBindFramebuffer(GL_FRAMEBUFFER, _fbo); | |
| glFramebufferTexture2D(GL_FRAMEBUFFER, | |
| GL_COLOR_ATTACHMENT0, | |
| GL_TEXTURE_RECTANGLE_EXT, | |
| _colorTex, | |
| 0); | |
| GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); | |
| if (status != GL_FRAMEBUFFER_COMPLETE) { | |
| NSLog(@"[ISFRenderer] FBO incomplete after ensureRenderTargetWidth: %u", status); | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| return NO; | |
| } | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| _texWidth = w; | |
| _texHeight = h; | |
| return YES; | |
| } | |
| // Create or resize a rectangle texture + FBO used to hold a vertically | |
| // flipped copy of a QC input image. | |
| - (BOOL)ensureInputFlipTargetWidth:(size_t)w height:(size_t)h | |
| { | |
| if (!_cglContext || w == 0 || h == 0) | |
| return NO; | |
| CGLSetCurrentContext(_cglContext); | |
| if (_flipTex != 0 && | |
| _flipFBO != 0 && | |
| _flipWidth == w && | |
| _flipHeight == h) { | |
| return YES; | |
| } | |
| // Destroy old | |
| if (_flipTex) { | |
| glDeleteTextures(1, &_flipTex); | |
| _flipTex = 0; | |
| } | |
| if (_flipFBO) { | |
| glDeleteFramebuffers(1, &_flipFBO); | |
| _flipFBO = 0; | |
| } | |
| // Create rectangle texture | |
| glGenTextures(1, &_flipTex); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, _flipTex); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
| glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, | |
| 0, | |
| GL_RGBA8, | |
| (GLsizei)w, | |
| (GLsizei)h, | |
| 0, | |
| GL_BGRA, | |
| GL_UNSIGNED_INT_8_8_8_8_REV, | |
| NULL); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, 0); | |
| // Create FBO | |
| glGenFramebuffers(1, &_flipFBO); | |
| glBindFramebuffer(GL_FRAMEBUFFER, _flipFBO); | |
| glFramebufferTexture2D(GL_FRAMEBUFFER, | |
| GL_COLOR_ATTACHMENT0, | |
| GL_TEXTURE_RECTANGLE_EXT, | |
| _flipTex, | |
| 0); | |
| GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); | |
| if (status != GL_FRAMEBUFFER_COMPLETE) { | |
| NSLog(@"[ISFRenderer] Input flip FBO incomplete (status=%u)", status); | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| glDeleteTextures(1, &_flipTex); | |
| glDeleteFramebuffers(1, &_flipFBO); | |
| _flipTex = 0; | |
| _flipFBO = 0; | |
| _flipWidth = 0; | |
| _flipHeight = 0; | |
| return NO; | |
| } | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| _flipWidth = w; | |
| _flipHeight = h; | |
| return YES; | |
| } | |
| // Create or resize an offscreen buffer for a named PASS TARGET. | |
| // For persistent buffers we set up true ping-pong (readTex/writeTex). | |
| - (ISFBuffer *)bufferForTargetName:(NSString *)name | |
| persistent:(BOOL)isPersistent | |
| float:(BOOL)isFloat | |
| width:(size_t)w | |
| height:(size_t)h | |
| { | |
| if (!name || name.length == 0 || !_cglContext || w == 0 || h == 0) | |
| return nil; | |
| CGLSetCurrentContext(_cglContext); | |
| ISFBuffer *buf = self.passTargets[name]; | |
| if (buf && | |
| (buf.width != w || buf.height != h || buf.isFloat != isFloat || buf.isPersistent != isPersistent)) { | |
| // Destroy and recreate if config changed | |
| GLuint readTex = buf.readTex; | |
| GLuint writeTex = buf.writeTex; | |
| GLuint readFBO = buf.readFBO; | |
| GLuint writeFBO = buf.writeFBO; | |
| if (readTex) { | |
| glDeleteTextures(1, &readTex); | |
| } | |
| if (writeTex && writeTex != readTex) { | |
| glDeleteTextures(1, &writeTex); | |
| } | |
| if (readFBO) { | |
| glDeleteFramebuffers(1, &readFBO); | |
| } | |
| if (writeFBO && writeFBO != readFBO) { | |
| glDeleteFramebuffers(1, &writeFBO); | |
| } | |
| buf.readTex = 0; | |
| buf.writeTex = 0; | |
| buf.readFBO = 0; | |
| buf.writeFBO = 0; | |
| buf = nil; | |
| } | |
| GLenum internalFormat = isFloat ? GL_RGBA32F_ARB : GL_RGBA8; | |
| GLenum dataType = isFloat ? GL_FLOAT : GL_UNSIGNED_INT_8_8_8_8_REV; | |
| GLenum dataFormat = isFloat ? GL_RGBA : GL_BGRA; | |
| if (!buf) { | |
| buf = [[ISFBuffer alloc] init]; | |
| buf.width = w; | |
| buf.height = h; | |
| buf.isFloat = isFloat; | |
| buf.isPersistent = isPersistent; | |
| // First texture / FBO | |
| GLuint texA = 0, fboA = 0; | |
| glGenTextures(1, &texA); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, texA); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
| glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, | |
| 0, | |
| internalFormat, | |
| (GLsizei)w, | |
| (GLsizei)h, | |
| 0, | |
| dataFormat, | |
| dataType, | |
| NULL); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, 0); | |
| glGenFramebuffers(1, &fboA); | |
| glBindFramebuffer(GL_FRAMEBUFFER, fboA); | |
| glFramebufferTexture2D(GL_FRAMEBUFFER, | |
| GL_COLOR_ATTACHMENT0, | |
| GL_TEXTURE_RECTANGLE_EXT, | |
| texA, | |
| 0); | |
| GLenum statusA = glCheckFramebufferStatus(GL_FRAMEBUFFER); | |
| if (statusA != GL_FRAMEBUFFER_COMPLETE) { | |
| NSLog(@"[ISFRenderer] FBO incomplete for PASS TARGET '%@' (status=%u)", name, statusA); | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| if (texA) glDeleteTextures(1, &texA); | |
| if (fboA) glDeleteFramebuffers(1, &fboA); | |
| return nil; | |
| } | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| if (isPersistent) { | |
| // Second texture / FBO for ping-pong | |
| GLuint texB = 0, fboB = 0; | |
| glGenTextures(1, &texB); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, texB); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | |
| glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
| glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, | |
| 0, | |
| internalFormat, | |
| (GLsizei)w, | |
| (GLsizei)h, | |
| 0, | |
| dataFormat, | |
| dataType, | |
| NULL); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, 0); | |
| glGenFramebuffers(1, &fboB); | |
| glBindFramebuffer(GL_FRAMEBUFFER, fboB); | |
| glFramebufferTexture2D(GL_FRAMEBUFFER, | |
| GL_COLOR_ATTACHMENT0, | |
| GL_TEXTURE_RECTANGLE_EXT, | |
| texB, | |
| 0); | |
| GLenum statusB = glCheckFramebufferStatus(GL_FRAMEBUFFER); | |
| if (statusB != GL_FRAMEBUFFER_COMPLETE) { | |
| NSLog(@"[ISFRenderer] FBO incomplete (B) for PASS TARGET '%@' (status=%u)", name, statusB); | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| if (texA) glDeleteTextures(1, &texA); | |
| if (fboA) glDeleteFramebuffers(1, &fboA); | |
| if (texB) glDeleteTextures(1, &texB); | |
| if (fboB) glDeleteFramebuffers(1, &fboB); | |
| return nil; | |
| } | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| // For persistent ping-pong: readTex starts as texA, writeTex as texB | |
| buf.readTex = texA; | |
| buf.readFBO = fboA; // rarely used | |
| buf.writeTex = texB; | |
| buf.writeFBO = fboB; | |
| // Clear both once on creation | |
| glBindFramebuffer(GL_FRAMEBUFFER, fboA); | |
| glViewport(0, 0, (GLsizei)w, (GLsizei)h); | |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| glBindFramebuffer(GL_FRAMEBUFFER, fboB); | |
| glViewport(0, 0, (GLsizei)w, (GLsizei)h); | |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| } | |
| else { | |
| // Non-persistent: we only need a single texture; we use the same | |
| // texture for reading and writing within a frame. | |
| buf.readTex = texA; | |
| buf.readFBO = 0; | |
| buf.writeTex = texA; | |
| buf.writeFBO = fboA; | |
| // Clear once on creation (we also clear on each pass before drawing) | |
| glBindFramebuffer(GL_FRAMEBUFFER, fboA); | |
| glViewport(0, 0, (GLsizei)w, (GLsizei)h); | |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| } | |
| self.passTargets[name] = buf; | |
| } | |
| return buf; | |
| } | |
| // Swap read/write textures for a persistent buffer (ping-pong) | |
| - (void)swapPersistentBuffer:(ISFBuffer *)buf | |
| { | |
| if (!buf || !buf.isPersistent) | |
| return; | |
| GLuint tmpTex = buf.readTex; | |
| buf.readTex = buf.writeTex; | |
| buf.writeTex = tmpTex; | |
| GLuint tmpFBO = buf.readFBO; | |
| buf.readFBO = buf.writeFBO; | |
| buf.writeFBO = tmpFBO; | |
| } | |
| #pragma mark - Shader compilation | |
| // ====================================================================== | |
| // Shader compilation | |
| // ====================================================================== | |
| - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type source:(const GLchar *)src | |
| { | |
| *shader = glCreateShader(type); | |
| if (!*shader) | |
| return NO; | |
| glShaderSource(*shader, 1, &src, NULL); | |
| glCompileShader(*shader); | |
| GLint compiled = 0; | |
| glGetShaderiv(*shader, GL_COMPILE_STATUS, &compiled); | |
| if (!compiled) { | |
| GLint logLen = 0; | |
| glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLen); | |
| if (logLen > 1) { | |
| char *log = (char *)malloc(logLen); | |
| if (log) { | |
| glGetShaderInfoLog(*shader, logLen, NULL, log); | |
| NSLog(@"[ISFRenderer] Shader compile error (%s): %s", | |
| (type == GL_VERTEX_SHADER ? "vertex" : "fragment"), log); | |
| free(log); | |
| } | |
| } else { | |
| NSLog(@"[ISFRenderer] Shader compile failed with no log (%s)", | |
| (type == GL_VERTEX_SHADER ? "vertex" : "fragment")); | |
| } | |
| glDeleteShader(*shader); | |
| *shader = 0; | |
| return NO; | |
| } | |
| return YES; | |
| } | |
| - (BOOL)linkProgram | |
| { | |
| if (_program) | |
| glDeleteProgram(_program); | |
| _program = glCreateProgram(); | |
| if (!_program) | |
| return NO; | |
| glAttachShader(_program, _vertexShader); | |
| glAttachShader(_program, _fragmentShader); | |
| glLinkProgram(_program); | |
| GLint linked = 0; | |
| glGetProgramiv(_program, GL_LINK_STATUS, &linked); | |
| if (!linked) { | |
| GLint logLen = 0; | |
| glGetProgramiv(_program, GL_INFO_LOG_LENGTH, &logLen); | |
| if (logLen > 1) { | |
| char *log = (char *)malloc(logLen); | |
| if (log) { | |
| glGetProgramInfoLog(_program, logLen, NULL, log); | |
| NSLog(@"[ISFRenderer] Program link error: %s", log); | |
| free(log); | |
| } | |
| } else { | |
| NSLog(@"[ISFRenderer] Program link failed with no log"); | |
| } | |
| glDeleteProgram(_program); | |
| _program = 0; | |
| return NO; | |
| } | |
| return YES; | |
| } | |
| // Decorate raw ISF fragment source with a preamble: | |
| // - built-in uniforms (TIME, RENDERSIZE, etc.) | |
| // - varying isf_FragNormCoord | |
| // - ISF image helper macros (IMG_PIXEL / IMG_NORM_PIXEL / etc.) | |
| // - uniform declarations for JSON INPUTS | |
| // - sampler2DRect uniforms for PASSES TARGET buffers | |
| // | |
| // Also caches parsed INPUT / PASSES definitions into _isfInputs / _isfPasses. | |
| - (NSString *)decoratedFragmentSourceFromRaw:(NSString *)raw | |
| { | |
| if (!raw) { | |
| return nil; | |
| } | |
| NSMutableString *preamble = [NSMutableString string]; | |
| [preamble appendString:@"// ISF host preamble injected by ISFRendererPlugIn\n"]; | |
| [preamble appendString:@"#extension GL_ARB_texture_rectangle : enable\n"]; | |
| [preamble appendString:@"varying vec2 isf_FragNormCoord;\n"]; | |
| [preamble appendString:@"uniform vec2 RENDERSIZE;\n"]; | |
| [preamble appendString:@"uniform float TIME;\n"]; | |
| [preamble appendString:@"uniform float TIMEDELTA;\n"]; | |
| [preamble appendString:@"uniform vec4 DATE;\n"]; | |
| [preamble appendString:@"uniform int FRAMEINDEX;\n"]; | |
| [preamble appendString:@"uniform int PASSINDEX;\n"]; | |
| // Minimal ISF image helpers (using rectangle textures + RENDERSIZE) | |
| [preamble appendString:@"// Minimal ISF helper macros for images\n"]; | |
| [preamble appendString: | |
| @"vec4 _ISFImgPixel(sampler2DRect img, vec2 pixelCoord) { return texture2DRect(img, pixelCoord); }\n"]; | |
| [preamble appendString: | |
| @"vec4 _ISFImgNormPixel(sampler2DRect img, vec2 normCoord) { return texture2DRect(img, normCoord * RENDERSIZE); }\n"]; | |
| [preamble appendString: | |
| @"vec2 _ISFImgSize(sampler2DRect img) { return RENDERSIZE; }\n"]; | |
| [preamble appendString:@"#define IMG_PIXEL(img, coord) _ISFImgPixel(img, coord)\n"]; | |
| [preamble appendString:@"#define IMG_NORM_PIXEL(img, coord) _ISFImgNormPixel(img, coord)\n"]; | |
| [preamble appendString:@"#define IMG_THIS_PIXEL(img) IMG_PIXEL(img, gl_FragCoord.xy)\n"]; | |
| [preamble appendString:@"#define IMG_NORM_THIS_PIXEL(img) IMG_NORM_PIXEL(img, isf_FragNormCoord)\n"]; | |
| [preamble appendString:@"#define IMG_SIZE(img) _ISFImgSize(img)\n"]; | |
| // Reset cached definitions | |
| _isfInputs = nil; | |
| _isfPasses = nil; | |
| NSMutableArray *parsedInputs = [NSMutableArray array]; | |
| NSMutableArray *parsedPasses = [NSMutableArray array]; | |
| // --- Try to find JSON header in first /* ... */ comment --- | |
| @try { | |
| NSRange commentStart = [raw rangeOfString:@"/*"]; | |
| if (commentStart.location != NSNotFound) { | |
| NSRange searchRange = NSMakeRange(commentStart.location + 2, | |
| raw.length - (commentStart.location + 2)); | |
| NSRange commentEnd = [raw rangeOfString:@"*/" | |
| options:0 | |
| range:searchRange]; | |
| if (commentEnd.location != NSNotFound) { | |
| NSUInteger innerStart = commentStart.location + 2; | |
| NSUInteger innerLen = commentEnd.location - innerStart; | |
| if (innerStart + innerLen <= raw.length) { | |
| NSString *commentBody = [raw substringWithRange:NSMakeRange(innerStart, innerLen)]; | |
| // Find JSON { ... } inside comment body | |
| NSRange braceStart = [commentBody rangeOfString:@"{"]; | |
| NSRange braceEnd = [commentBody rangeOfString:@"}" | |
| options:NSBackwardsSearch]; | |
| if (braceStart.location != NSNotFound && braceEnd.location != NSNotFound) { | |
| NSUInteger jsonStart = braceStart.location; | |
| NSUInteger jsonLen = braceEnd.location - jsonStart + 1; | |
| if (jsonStart + jsonLen <= commentBody.length) { | |
| NSString *jsonString = | |
| [commentBody substringWithRange:NSMakeRange(jsonStart, jsonLen)]; | |
| NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; | |
| if (jsonData) { | |
| NSError *jsonError = nil; | |
| id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData | |
| options:0 | |
| error:&jsonError]; | |
| if (jsonError) { | |
| NSLog(@"[ISFRenderer] JSON parse error in ISF header: %@", jsonError); | |
| } | |
| if (!jsonError && [jsonObj isKindOfClass:[NSDictionary class]]) { | |
| NSDictionary *dict = (NSDictionary *)jsonObj; | |
| // INPUTS | |
| id inputsObj = dict[@"INPUTS"]; | |
| if ([inputsObj isKindOfClass:[NSArray class]]) { | |
| NSArray *inputs = (NSArray *)inputsObj; | |
| for (id inp in inputs) { | |
| if (![inp isKindOfClass:[NSDictionary class]]) | |
| continue; | |
| NSDictionary *inputDict = (NSDictionary *)inp; | |
| NSString *name = inputDict[@"NAME"]; | |
| NSString *type = inputDict[@"TYPE"]; | |
| if (![name isKindOfClass:[NSString class]] || | |
| ![type isKindOfClass:[NSString class]]) | |
| continue; | |
| if (name.length == 0) | |
| continue; | |
| // Avoid colliding with built-ins we already declared | |
| if ([name isEqualToString:@"TIME"] || | |
| [name isEqualToString:@"RENDERSIZE"] || | |
| [name isEqualToString:@"TIMEDELTA"] || | |
| [name isEqualToString:@"DATE"] || | |
| [name isEqualToString:@"FRAMEINDEX"] || | |
| [name isEqualToString:@"PASSINDEX"] || | |
| [name isEqualToString:@"isf_FragNormCoord"]) { | |
| continue; | |
| } | |
| NSString *glslType = nil; | |
| if ([type isEqualToString:@"float"] || | |
| [type isEqualToString:@"event"]) { | |
| glslType = @"float"; | |
| } | |
| else if ([type isEqualToString:@"bool"]) { | |
| glslType = @"bool"; | |
| } | |
| else if ([type isEqualToString:@"long"] || | |
| [type isEqualToString:@"int"]) { | |
| glslType = @"int"; | |
| } | |
| else if ([type isEqualToString:@"color"]) { | |
| glslType = @"vec4"; | |
| } | |
| else if ([type isEqualToString:@"point2D"]) { | |
| glslType = @"vec2"; | |
| } | |
| else if ([type isEqualToString:@"image"] || | |
| [type isEqualToString:@"audio"] || | |
| [type isEqualToString:@"audioFFT"]) { | |
| // Host uses rectangle textures for images | |
| glslType = @"sampler2DRect"; | |
| } | |
| // Record this INPUT (with DEFAULT etc.) for later uniform uploads | |
| NSMutableDictionary *stored = [NSMutableDictionary dictionary]; | |
| stored[@"NAME"] = name; | |
| stored[@"TYPE"] = type; | |
| id defVal = inputDict[@"DEFAULT"]; | |
| if (defVal) { | |
| stored[@"DEFAULT"] = defVal; | |
| } | |
| [parsedInputs addObject:stored]; | |
| if (glslType) { | |
| // Simple duplicate check: does the shader already declare this uniform? | |
| NSString *pattern = | |
| [NSString stringWithFormat:@"uniform[^;]*\\b%@\\b", name]; | |
| NSRange existing = | |
| [raw rangeOfString:pattern | |
| options:NSRegularExpressionSearch]; | |
| if (existing.location == NSNotFound) { | |
| [preamble appendFormat:@"uniform %@ %@;\n", glslType, name]; | |
| } | |
| } | |
| } // for inputs | |
| if ([parsedInputs count] > 0) { | |
| _isfInputs = [parsedInputs copy]; | |
| } | |
| } | |
| // PASSES (multi-pass ISF) | |
| id passesObj = dict[@"PASSES"]; | |
| if ([passesObj isKindOfClass:[NSArray class]]) { | |
| for (id p in (NSArray *)passesObj) { | |
| if (![p isKindOfClass:[NSDictionary class]]) | |
| continue; | |
| NSDictionary *passDict = (NSDictionary *)p; | |
| [parsedPasses addObject:passDict]; | |
| NSString *targetName = passDict[@"TARGET"]; | |
| if ([targetName isKindOfClass:[NSString class]] && | |
| targetName.length > 0) { | |
| // If shader doesn?t already declare a sampler for this buffer, add one. | |
| NSString *pattern = | |
| [NSString stringWithFormat:@"uniform[^;]*\\b%@\\b", targetName]; | |
| NSRange existing = | |
| [raw rangeOfString:pattern | |
| options:NSRegularExpressionSearch]; | |
| if (existing.location == NSNotFound) { | |
| [preamble appendFormat:@"uniform sampler2DRect %@;\n", targetName]; | |
| } | |
| } | |
| } | |
| if ([parsedPasses count] > 0) { | |
| _isfPasses = [parsedPasses copy]; | |
| NSLog(@"[ISFRenderer] Parsed %lu PASS(es) from ISF", | |
| (unsigned long)_isfPasses.count); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| @catch (NSException *exception) { | |
| NSLog(@"[ISFRenderer] Exception while parsing ISF JSON header: %@", exception); | |
| // fall through and just use built-ins | |
| } | |
| NSString *result = [NSString stringWithFormat:@"%@\n%@", preamble, raw]; | |
| return result; | |
| } | |
| // Decorate raw ISF vertex source with: | |
| // - built-in uniforms (TIME, RENDERSIZE, etc.) | |
| // - attribute vec2 position | |
| // - varying isf_FragNormCoord | |
| // - ISF image helper macros | |
| // - uniforms for INPUTS and PASS TARGETS | |
| // - an isf_vertShaderInit() implementation | |
| - (NSString *)decoratedVertexSourceFromRaw:(NSString *)rawVertex | |
| { | |
| if (!rawVertex) { | |
| return nil; | |
| } | |
| NSMutableString *preamble = [NSMutableString string]; | |
| [preamble appendString:@"// ISF host vertex preamble injected by ISFRendererPlugIn\n"]; | |
| [preamble appendString:@"#extension GL_ARB_texture_rectangle : enable\n"]; | |
| [preamble appendString:@"attribute vec2 position;\n"]; | |
| [preamble appendString:@"varying vec2 isf_FragNormCoord;\n"]; | |
| [preamble appendString:@"uniform vec2 RENDERSIZE;\n"]; | |
| [preamble appendString:@"uniform float TIME;\n"]; | |
| [preamble appendString:@"uniform float TIMEDELTA;\n"]; | |
| [preamble appendString:@"uniform vec4 DATE;\n"]; | |
| [preamble appendString:@"uniform int FRAMEINDEX;\n"]; | |
| [preamble appendString:@"uniform int PASSINDEX;\n"]; | |
| [preamble appendString:@"// Minimal ISF helper macros for images (vertex)\n"]; | |
| [preamble appendString: | |
| @"vec4 _ISFImgPixelVS(sampler2DRect img, vec2 pixelCoord) { return texture2DRect(img, pixelCoord); }\n"]; | |
| [preamble appendString: | |
| @"vec4 _ISFImgNormPixelVS(sampler2DRect img, vec2 normCoord) { return texture2DRect(img, normCoord * RENDERSIZE); }\n"]; | |
| [preamble appendString: | |
| @"vec2 _ISFImgSizeVS(sampler2DRect img) { return RENDERSIZE; }\n"]; | |
| [preamble appendString:@"#define IMG_PIXEL(img, coord) _ISFImgPixelVS(img, coord)\n"]; | |
| [preamble appendString:@"#define IMG_NORM_PIXEL(img, coord) _ISFImgNormPixelVS(img, coord)\n"]; | |
| [preamble appendString:@"#define IMG_SIZE(img) _ISFImgSizeVS(img)\n"]; | |
| [preamble appendString:@"#define IMG_THIS_PIXEL(img) _ISFImgPixelVS(img, isf_FragNormCoord * RENDERSIZE)\n"]; | |
| [preamble appendString:@"#define IMG_NORM_THIS_PIXEL(img) _ISFImgNormPixelVS(img, isf_FragNormCoord)\n"]; | |
| // Inject uniforms for INPUTS and PASS TARGETS if they aren?t already present in the vertex source | |
| @try { | |
| for (NSDictionary *input in _isfInputs) { | |
| NSString *name = input[@"NAME"]; | |
| NSString *type = input[@"TYPE"]; | |
| if (![name isKindOfClass:[NSString class]] || | |
| ![type isKindOfClass:[NSString class]] || | |
| name.length == 0) { | |
| continue; | |
| } | |
| NSString *glslType = nil; | |
| if ([type isEqualToString:@"float"] || | |
| [type isEqualToString:@"event"]) { | |
| glslType = @"float"; | |
| } | |
| else if ([type isEqualToString:@"bool"]) { | |
| glslType = @"bool"; | |
| } | |
| else if ([type isEqualToString:@"long"] || | |
| [type isEqualToString:@"int"]) { | |
| glslType = @"int"; | |
| } | |
| else if ([type isEqualToString:@"color"]) { | |
| glslType = @"vec4"; | |
| } | |
| else if ([type isEqualToString:@"point2D"]) { | |
| glslType = @"vec2"; | |
| } | |
| else if ([type isEqualToString:@"image"] || | |
| [type isEqualToString:@"audio"] || | |
| [type isEqualToString:@"audioFFT"]) { | |
| glslType = @"sampler2DRect"; | |
| } | |
| if (!glslType) | |
| continue; | |
| NSString *pattern = | |
| [NSString stringWithFormat:@"uniform[^;]*\\b%@\\b", name]; | |
| NSRange existing = | |
| [rawVertex rangeOfString:pattern | |
| options:NSRegularExpressionSearch]; | |
| if (existing.location == NSNotFound) { | |
| [preamble appendFormat:@"uniform %@ %@;\n", glslType, name]; | |
| } | |
| } | |
| for (NSDictionary *pass in _isfPasses) { | |
| NSString *targetName = pass[@"TARGET"]; | |
| if (![targetName isKindOfClass:[NSString class]] || | |
| targetName.length == 0) { | |
| continue; | |
| } | |
| NSString *pattern = | |
| [NSString stringWithFormat:@"uniform[^;]*\\b%@\\b", targetName]; | |
| NSRange existing = | |
| [rawVertex rangeOfString:pattern | |
| options:NSRegularExpressionSearch]; | |
| if (existing.location == NSNotFound) { | |
| [preamble appendFormat:@"uniform sampler2DRect %@;\n", targetName]; | |
| } | |
| } | |
| } | |
| @catch (NSException *ex) { | |
| NSLog(@"[ISFRenderer] Exception while decorating vertex source: %@", ex); | |
| } | |
| // Host-provided initialization helper. | |
| // We support both the newer isf_vertShaderInit() name | |
| // and the older vv_vertShaderInit() used by some legacy ISF shaders. | |
| [preamble appendString:@"void _ISFHostVertInitCore() {\n"]; | |
| [preamble appendString:@" isf_FragNormCoord = (position + vec2(1.0)) * 0.5;\n"]; | |
| [preamble appendString:@" gl_Position = vec4(position, 0.0, 1.0);\n"]; | |
| [preamble appendString:@"}\n"]; | |
| [preamble appendString:@"void isf_vertShaderInit() { _ISFHostVertInitCore(); }\n"]; | |
| [preamble appendString:@"void vv_vertShaderInit() { _ISFHostVertInitCore(); }\n"]; | |
| NSString *result = [NSString stringWithFormat:@"%@\n%@", preamble, rawVertex]; | |
| return result; | |
| } | |
| // Evaluate pass WIDTH/HEIGHT expressions like "$WIDTH/16.0" or "$blurAmount * 0.5" | |
| - (CGFloat)evaluateSizeExpression:(NSString *)expr | |
| defaultVal:(CGFloat)defaultVal | |
| outputWidthVal:(CGFloat)outWidth | |
| outputHeightVal:(CGFloat)outHeight | |
| { | |
| if (!expr || expr.length == 0) | |
| return defaultVal; | |
| @try { | |
| NSMutableString *mutableExpr = [expr mutableCopy]; | |
| // Replace $WIDTH/$HEIGHT with variables WIDTH/HEIGHT | |
| [mutableExpr replaceOccurrencesOfString:@"$WIDTH" | |
| withString:@"WIDTH" | |
| options:0 | |
| range:NSMakeRange(0, mutableExpr.length)]; | |
| [mutableExpr replaceOccurrencesOfString:@"$HEIGHT" | |
| withString:@"HEIGHT" | |
| options:0 | |
| range:NSMakeRange(0, mutableExpr.length)]; | |
| // Replace $InputName with InputName for all numeric INPUTS | |
| NSString *path = self.isfPath ?: @""; | |
| NSString *signature = ISFPortSignatureForPath(path); | |
| NSMutableDictionary *vars = [@{ | |
| @"WIDTH" : @(outWidth), | |
| @"HEIGHT" : @(outHeight) | |
| } mutableCopy]; | |
| for (NSDictionary *input in _isfInputs) { | |
| NSString *name = input[@"NAME"]; | |
| NSString *type = input[@"TYPE"]; | |
| if (![name isKindOfClass:[NSString class]] || | |
| ![type isKindOfClass:[NSString class]]) { | |
| continue; | |
| } | |
| BOOL numeric = | |
| [type isEqualToString:@"float"] || | |
| [type isEqualToString:@"event"] || | |
| [type isEqualToString:@"long"] || | |
| [type isEqualToString:@"int"] || | |
| [type isEqualToString:@"bool"]; | |
| if (!numeric) | |
| continue; | |
| NSString *marker = [@"$" stringByAppendingString:name]; | |
| [mutableExpr replaceOccurrencesOfString:marker | |
| withString:name | |
| options:0 | |
| range:NSMakeRange(0, mutableExpr.length)]; | |
| // Figure out numeric value (port value or DEFAULT) | |
| NSString *portKey = ISFPortKeyForInputName(name, signature); | |
| id portValue = nil; | |
| @try { | |
| portValue = [self valueForInputKey:portKey]; | |
| } | |
| @catch (NSException *ex) { | |
| portValue = nil; | |
| } | |
| double v = 0.0; | |
| if ([type isEqualToString:@"bool"]) { | |
| if (portValue) v = [portValue boolValue] ? 1.0 : 0.0; | |
| else if (input[@"DEFAULT"]) v = [input[@"DEFAULT"] boolValue] ? 1.0 : 0.0; | |
| } | |
| else { | |
| if (portValue && [portValue respondsToSelector:@selector(doubleValue)]) | |
| v = [portValue doubleValue]; | |
| else if (input[@"DEFAULT"] && [input[@"DEFAULT"] respondsToSelector:@selector(doubleValue)]) | |
| v = [input[@"DEFAULT"] doubleValue]; | |
| } | |
| vars[name] = @(v); | |
| } | |
| NSExpression *expression = [NSExpression expressionWithFormat:mutableExpr]; | |
| id value = [expression expressionValueWithObject:vars context:nil]; | |
| if ([value respondsToSelector:@selector(doubleValue)]) { | |
| return (CGFloat)[value doubleValue]; | |
| } | |
| } | |
| @catch (NSException *ex) { | |
| NSLog(@"[ISFRenderer] Exception while evaluating size expression '%@': %@", expr, ex); | |
| } | |
| return defaultVal; | |
| } | |
| // Apply JSON DEFAULT values OR QC port values to uniforms for all ISF INPUTS, | |
| // and bind any PASS TARGET buffers as sampler uniforms. | |
| // Called every pass before drawing. | |
| - (void)applyISFInputDefaultsWithContext:(id<QCPlugInContext>)context | |
| { | |
| if (!_isfInputs || _program == 0) | |
| return; | |
| NSString *path = self.isfPath ?: @""; | |
| NSString *signature = ISFPortSignatureForPath(path); | |
| // Remember currently active texture unit so we can restore it after binding images. | |
| GLint initialActiveTex = 0; | |
| glGetIntegerv(GL_ACTIVE_TEXTURE, &initialActiveTex); | |
| // Determine the *desired* image size from the current RENDERSIZE uniform | |
| // (this is the ISF shader width/height for the current pass). | |
| size_t destW = _texWidth; | |
| size_t destH = _texHeight; | |
| GLint renderSizeLoc = glGetUniformLocation(_program, "RENDERSIZE"); | |
| if (renderSizeLoc >= 0) { | |
| GLfloat rs[2] = {0.0f, 0.0f}; | |
| glGetUniformfv(_program, renderSizeLoc, rs); | |
| if (rs[0] > 0.0f && rs[1] > 0.0f) { | |
| destW = (size_t)llroundf(rs[0]); | |
| destH = (size_t)llroundf(rs[1]); | |
| } | |
| } | |
| if (destW == 0) destW = 1; | |
| if (destH == 0) destH = 1; | |
| GLint currentTexUnit = 0; | |
| for (NSDictionary *input in _isfInputs) { | |
| NSString *name = input[@"NAME"]; | |
| NSString *type = input[@"TYPE"]; | |
| id defaultVal = input[@"DEFAULT"]; | |
| if (![name isKindOfClass:[NSString class]] || | |
| ![type isKindOfClass:[NSString class]]) { | |
| continue; | |
| } | |
| GLint loc = glGetUniformLocation(_program, [name UTF8String]); | |
| if (loc < 0) | |
| continue; // not active in this program | |
| // Look up matching QC port value, if it exists. | |
| NSString *portKey = ISFPortKeyForInputName(name, signature); | |
| id portValue = nil; | |
| @try { | |
| portValue = [self valueForInputKey:portKey]; | |
| } | |
| @catch (NSException *ex) { | |
| portValue = nil; | |
| } | |
| // Helper blocks: choose port value when available, otherwise DEFAULT. | |
| double (^numVal)(id,id) = ^double(id p, id d) { | |
| if ([p respondsToSelector:@selector(doubleValue)]) | |
| return [p doubleValue]; | |
| if ([d respondsToSelector:@selector(doubleValue)]) | |
| return [d doubleValue]; | |
| return 0.0; | |
| }; | |
| NSDictionary* (^structVal)(id) = ^NSDictionary* (id v) { | |
| return [v isKindOfClass:[NSDictionary class]] ? (NSDictionary *)v : nil; | |
| }; | |
| // Float / event | |
| if ([type isEqualToString:@"float"] || | |
| [type isEqualToString:@"event"]) { | |
| GLfloat v = (GLfloat)numVal(portValue, defaultVal); | |
| glUniform1f(loc, v); | |
| } | |
| // Bool | |
| else if ([type isEqualToString:@"bool"]) { | |
| BOOL b = NO; | |
| if (portValue) | |
| b = [portValue boolValue]; | |
| else if (defaultVal) | |
| b = [defaultVal boolValue]; | |
| glUniform1i(loc, b ? 1 : 0); | |
| } | |
| // Int / long | |
| else if ([type isEqualToString:@"long"] || | |
| [type isEqualToString:@"int"]) { | |
| GLint v = 0; | |
| if (portValue) | |
| v = (GLint)[portValue intValue]; | |
| else if (defaultVal) | |
| v = (GLint)[defaultVal intValue]; | |
| glUniform1i(loc, v); | |
| } | |
| // Color | |
| else if ([type isEqualToString:@"color"]) { | |
| CGFloat r = 0.0, g = 0.0, b = 0.0, a = 1.0; | |
| NSDictionary *s = structVal(portValue); | |
| if (s) { | |
| if (s[@"r"]) r = [s[@"r"] doubleValue]; | |
| if (s[@"g"]) g = [s[@"g"] doubleValue]; | |
| if (s[@"b"]) b = [s[@"b"] doubleValue]; | |
| if (s[@"a"]) a = [s[@"a"] doubleValue]; | |
| } else if ([defaultVal isKindOfClass:[NSArray class]]) { | |
| NSArray *arr = (NSArray *)defaultVal; | |
| if (arr.count > 0) r = [arr[0] doubleValue]; | |
| if (arr.count > 1) g = [arr[1] doubleValue]; | |
| if (arr.count > 2) b = [arr[2] doubleValue]; | |
| if (arr.count > 3) a = [arr[3] doubleValue]; | |
| } | |
| glUniform4f(loc, (GLfloat)r, (GLfloat)g, (GLfloat)b, (GLfloat)a); | |
| } | |
| // point2D | |
| else if ([type isEqualToString:@"point2D"]) { | |
| CGFloat x = 0.0, y = 0.0; | |
| NSDictionary *s = structVal(portValue); | |
| if (s) { | |
| id xv = s[@"x"] ?: s[@"X"]; | |
| id yv = s[@"y"] ?: s[@"Y"]; | |
| if (xv) x = [xv doubleValue]; | |
| if (yv) y = [yv doubleValue]; | |
| } else if ([defaultVal isKindOfClass:[NSArray class]]) { | |
| NSArray *arr = (NSArray *)defaultVal; | |
| if (arr.count > 0) x = [arr[0] doubleValue]; | |
| if (arr.count > 1) y = [arr[1] doubleValue]; | |
| } | |
| glUniform2f(loc, (GLfloat)x, (GLfloat)y); | |
| } | |
| // image samplers (single-pass inputs from QC) | |
| else if ([type isEqualToString:@"image"]) { | |
| id img = portValue; | |
| if (img && | |
| [img respondsToSelector:@selector(lockTextureRepresentationWithColorSpace:forBounds:)] && | |
| [img respondsToSelector:@selector(unlockTextureRepresentation)] && | |
| [img respondsToSelector:@selector(textureTarget)] && | |
| [img respondsToSelector:@selector(textureName)] && | |
| [img respondsToSelector:@selector(imageBounds)]) { | |
| CGRect bounds = [img imageBounds]; | |
| size_t srcW = (size_t)CGRectGetWidth(bounds); | |
| size_t srcH = (size_t)CGRectGetHeight(bounds); | |
| if (srcW == 0 || srcH == 0) { | |
| continue; | |
| } | |
| if ([img lockTextureRepresentationWithColorSpace:[context colorSpace] | |
| forBounds:bounds]) { | |
| GLenum srcTarget = [img textureTarget]; | |
| GLuint srcTex = [img textureName]; | |
| // Ask QC whether this texture is already flipped. | |
| BOOL srcIsFlipped = NO; | |
| if ([img respondsToSelector:@selector(textureFlipped)]) { | |
| srcIsFlipped = [img textureFlipped]; | |
| } | |
| // Allocate flip FBO/texture at *RENDERSIZE*, not at the source size. | |
| if ([self ensureInputFlipTargetWidth:destW height:destH]) { | |
| GLint prevFBO = 0; | |
| GLint prevProgram = 0; | |
| GLint prevViewport[4] = {0,0,0,0}; | |
| glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFBO); | |
| glGetIntegerv(GL_CURRENT_PROGRAM, &prevProgram); | |
| glGetIntegerv(GL_VIEWPORT, prevViewport); | |
| glUseProgram(0); | |
| glBindFramebuffer(GL_FRAMEBUFFER, self.flipFBO); | |
| glViewport(0, 0, (GLsizei)destW, (GLsizei)destH); | |
| glMatrixMode(GL_PROJECTION); | |
| glPushMatrix(); | |
| glLoadIdentity(); | |
| glOrtho(0.0, (GLdouble)destW, 0.0, (GLdouble)destH, -1.0, 1.0); | |
| glMatrixMode(GL_MODELVIEW); | |
| glPushMatrix(); | |
| glLoadIdentity(); | |
| glDisable(GL_DEPTH_TEST); | |
| glDisable(GL_BLEND); | |
| glEnable(srcTarget); | |
| glBindTexture(srcTarget, srcTex); | |
| // Draw a quad that covers the whole destination FBO (destW x destH), | |
| // sampling across the full source image (srcW x srcH). This both | |
| // flips (if needed) and rescales the input image to RENDERSIZE. | |
| glBegin(GL_QUADS); | |
| if (srcIsFlipped) { | |
| // Source is vertically flipped already -> unflip it. | |
| glTexCoord2f(0.0f, (GLfloat)srcH); glVertex2f(0.0f, 0.0f); | |
| glTexCoord2f((GLfloat)srcW, (GLfloat)srcH); glVertex2f((GLfloat)destW, 0.0f); | |
| glTexCoord2f((GLfloat)srcW, 0.0f); glVertex2f((GLfloat)destW, (GLfloat)destH); | |
| glTexCoord2f(0.0f, 0.0f); glVertex2f(0.0f, (GLfloat)destH); | |
| } else { | |
| // Source is already in GL-style orientation -> just copy & scale. | |
| glTexCoord2f(0.0f, 0.0f); glVertex2f(0.0f, 0.0f); | |
| glTexCoord2f((GLfloat)srcW, 0.0f); glVertex2f((GLfloat)destW, 0.0f); | |
| glTexCoord2f((GLfloat)srcW, (GLfloat)srcH); glVertex2f((GLfloat)destW, (GLfloat)destH); | |
| glTexCoord2f(0.0f, (GLfloat)srcH); glVertex2f(0.0f, (GLfloat)destH); | |
| } | |
| glEnd(); | |
| glDisable(srcTarget); | |
| glBindTexture(srcTarget, 0); | |
| glMatrixMode(GL_MODELVIEW); | |
| glPopMatrix(); | |
| glMatrixMode(GL_PROJECTION); | |
| glPopMatrix(); | |
| glBindFramebuffer(GL_FRAMEBUFFER, prevFBO); | |
| glViewport(prevViewport[0], prevViewport[1], | |
| prevViewport[2], prevViewport[3]); | |
| glUseProgram(prevProgram); | |
| // Bind the flipped+rescaled rectangle texture to the ISF uniform | |
| GLint texUnit = currentTexUnit; | |
| currentTexUnit++; | |
| glActiveTexture(GL_TEXTURE0 + texUnit); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, self.flipTex); | |
| glUniform1i(loc, texUnit); | |
| // Expose "<inputName>Size" as the ISF shader size (RENDERSIZE) | |
| NSString *sizeName = [name stringByAppendingString:@"Size"]; | |
| GLint sizeLoc = glGetUniformLocation(_program, [sizeName UTF8String]); | |
| if (sizeLoc >= 0) { | |
| glUniform2f(sizeLoc, (GLfloat)destW, (GLfloat)destH); | |
| } | |
| } else { | |
| // Fallback: bind original QC texture (no rescale) | |
| GLint texUnit = currentTexUnit; | |
| currentTexUnit++; | |
| glActiveTexture(GL_TEXTURE0 + texUnit); | |
| glBindTexture(srcTarget, srcTex); | |
| glUniform1i(loc, texUnit); | |
| NSString *sizeName = [name stringByAppendingString:@"Size"]; | |
| GLint sizeLoc = glGetUniformLocation(_program, [sizeName UTF8String]); | |
| if (sizeLoc >= 0) { | |
| // In this rare failure case, report real source size. | |
| glUniform2f(sizeLoc, (GLfloat)srcW, (GLfloat)srcH); | |
| } | |
| } | |
| [img unlockTextureRepresentation]; | |
| } | |
| } | |
| } | |
| else { | |
| // audio/audioFFT or unknown sampler types are not handled here yet | |
| } | |
| } | |
| // Bind multi-pass TARGET buffers (from PASSES) as sampler uniforms. | |
| if (_isfPasses && [_isfPasses count] > 0) { | |
| NSMutableSet *seenTargets = [NSMutableSet set]; | |
| for (NSDictionary *pass in _isfPasses) { | |
| NSString *targetName = pass[@"TARGET"]; | |
| if (![targetName isKindOfClass:[NSString class]] || targetName.length == 0) | |
| continue; | |
| if ([seenTargets containsObject:targetName]) | |
| continue; | |
| [seenTargets addObject:targetName]; | |
| ISFBuffer *buf = self.passTargets[targetName]; | |
| if (!buf || buf.readTex == 0) | |
| continue; | |
| GLint loc = glGetUniformLocation(_program, [targetName UTF8String]); | |
| if (loc < 0) | |
| continue; | |
| GLint texUnit = currentTexUnit; | |
| currentTexUnit++; | |
| glActiveTexture(GL_TEXTURE0 + texUnit); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, buf.readTex); | |
| glUniform1i(loc, texUnit); | |
| // Optional: expose "<bufferName>Size" uniform | |
| NSString *sizeName = [targetName stringByAppendingString:@"Size"]; | |
| GLint sizeLoc = glGetUniformLocation(_program, [sizeName UTF8String]); | |
| if (sizeLoc >= 0) { | |
| glUniform2f(sizeLoc, | |
| (GLfloat)buf.width, | |
| (GLfloat)buf.height); | |
| } | |
| } | |
| } | |
| // Restore previously active texture unit | |
| glActiveTexture(initialActiveTex); | |
| } | |
| #pragma mark - ISF loading | |
| // ====================================================================== | |
| // ISF loading | |
| // ====================================================================== | |
| - (BOOL)loadISFInContext:(id<QCPlugInContext>)context | |
| { | |
| if (!_cglContext) | |
| _cglContext = [context CGLContextObj]; | |
| if (!_cglContext) | |
| return NO; | |
| NSString *path = self.isfPath; // from Settings tab | |
| if (!path || [path length] == 0) | |
| return NO; | |
| // Check file modification date, reload only when changed | |
| NSFileManager *fm = [NSFileManager defaultManager]; | |
| NSDictionary *attrs = [fm attributesOfItemAtPath:path error:NULL]; | |
| NSDate *modDate = [attrs fileModificationDate]; | |
| if (!attrs || !modDate) { | |
| NSLog(@"[ISFRenderer] Could not stat ISF at '%@'", path); | |
| return NO; | |
| } | |
| BOOL pathChanged = (!_currentISFPath || ![_currentISFPath isEqualToString:path]); | |
| BOOL dateChanged = (!_currentISFModDate || ![_currentISFModDate isEqualToDate:modDate]); | |
| if (!_needsReload && !pathChanged && !dateChanged && _program != 0) | |
| return YES; // Nothing to do | |
| NSLog(@"[ISFRenderer] Loading / reloading ISF from '%@' (pathChanged=%d, dateChanged=%d, needsReload=%d)", | |
| path, (int)pathChanged, (int)dateChanged, (int)_needsReload); | |
| // New ISF -> drop old multi-pass targets | |
| [self destroyPassTargetBuffers]; | |
| _isfPasses = nil; | |
| _needsReload = NO; | |
| _currentISFPath = [path copy]; | |
| _currentISFModDate = modDate; | |
| NSError *readError = nil; | |
| NSString *fragSourceString = [NSString stringWithContentsOfFile:path | |
| encoding:NSUTF8StringEncoding | |
| error:&readError]; | |
| if (!fragSourceString) { | |
| NSLog(@"[ISFRenderer] Failed to read ISF at '%@' during load: %@", path, readError); | |
| return NO; | |
| } | |
| // Decorate with ISF preamble + INPUT / PASSES uniforms, and cache INPUTS/PASSES. | |
| NSString *fullFragSource = fragSourceString; | |
| @try { | |
| NSString *decorated = [self decoratedFragmentSourceFromRaw:fragSourceString]; | |
| if (decorated) { | |
| fullFragSource = decorated; | |
| } | |
| } | |
| @catch (NSException *ex) { | |
| NSLog(@"[ISFRenderer] Exception while decorating ISF source during load: %@", ex); | |
| fullFragSource = fragSourceString; | |
| } | |
| NSLog(@"[ISFRenderer] Decorated fragment source length for '%@': %lu chars", | |
| path, (unsigned long)[fullFragSource length]); | |
| // Count image inputs (for logging/debugging) | |
| NSUInteger imageCount = 0; | |
| for (NSDictionary *inp in _isfInputs) { | |
| if ([[inp[@"TYPE"] description] isEqualToString:@"image"]) | |
| imageCount++; | |
| } | |
| if (imageCount > 0) { | |
| NSLog(@"[ISFRenderer] ISF '%@' reports %lu image INPUT(s)", path, (unsigned long)imageCount); | |
| } | |
| const GLchar *fragSrc = (const GLchar *)[fullFragSource UTF8String]; | |
| // --- Vertex shader: load .vs if present, otherwise default, both decorated --- | |
| NSString *vertexSourceString = nil; | |
| NSString *vsPath = [[path stringByDeletingPathExtension] stringByAppendingPathExtension:@"vs"]; | |
| // Reuse the existing NSFileManager *fm defined earlier in this method | |
| if ([fm fileExistsAtPath:vsPath]) { | |
| NSError *vsError = nil; | |
| NSString *rawVS = [NSString stringWithContentsOfFile:vsPath | |
| encoding:NSUTF8StringEncoding | |
| error:&vsError]; | |
| if (!rawVS) { | |
| NSLog(@"[ISFRenderer] Failed to read ISF vertex shader at '%@': %@", vsPath, vsError); | |
| } else { | |
| vertexSourceString = [self decoratedVertexSourceFromRaw:rawVS]; | |
| } | |
| } | |
| if (!vertexSourceString) { | |
| // Spec-compatible default vertex shader that just calls isf_vertShaderInit() | |
| NSString *defaultVSRaw = @"void main() { isf_vertShaderInit(); }\n"; | |
| vertexSourceString = [self decoratedVertexSourceFromRaw:defaultVSRaw]; | |
| } | |
| const GLchar *vertSrc = (const GLchar *)[vertexSourceString UTF8String]; | |
| CGLSetCurrentContext(_cglContext); | |
| // Destroy old program/shaders, but keep FBO/texture (size dependent) | |
| if (_program) { | |
| glDeleteProgram(_program); | |
| _program = 0; | |
| } | |
| if (_vertexShader) { | |
| glDeleteShader(_vertexShader); | |
| _vertexShader = 0; | |
| } | |
| if (_fragmentShader) { | |
| glDeleteShader(_fragmentShader); | |
| _fragmentShader = 0; | |
| } | |
| if (![self compileShader:&_vertexShader type:GL_VERTEX_SHADER source:vertSrc]) { | |
| NSLog(@"[ISFRenderer] Vertex shader compile FAILED for ISF '%@'", path); | |
| return NO; | |
| } | |
| if (![self compileShader:&_fragmentShader type:GL_FRAGMENT_SHADER source:fragSrc]) { | |
| NSLog(@"[ISFRenderer] Fragment shader compile FAILED for ISF '%@'", path); | |
| glDeleteShader(_vertexShader); _vertexShader = 0; | |
| return NO; | |
| } | |
| if (![self linkProgram]) { | |
| NSLog(@"[ISFRenderer] Program link FAILED for ISF '%@'", path); | |
| glDeleteShader(_vertexShader); _vertexShader = 0; | |
| glDeleteShader(_fragmentShader); _fragmentShader = 0; | |
| return NO; | |
| } | |
| // Ensure quad VBO exists | |
| if (![self createQuadIfNeeded]) { | |
| NSLog(@"[ISFRenderer] Failed to create fullscreen quad VBO for ISF '%@'", path); | |
| return NO; | |
| } | |
| NSLog(@"[ISFRenderer] ISF program successfully compiled and linked for '%@'", path); | |
| return YES; | |
| } | |
| #pragma mark - QC execution | |
| // ====================================================================== | |
| // QC execution | |
| // ====================================================================== | |
| - (BOOL)startExecution:(id<QCPlugInContext>)context | |
| { | |
| _cglContext = [context CGLContextObj]; | |
| _needsReload = YES; | |
| _startTime = 0.0; | |
| self.lastTime = 0.0; | |
| self.frameIndex = 0; | |
| NSLog(@"[ISFRenderer] startExecution, context=%p, isfPath='%@'", context, self.isfPath); | |
| // NOTE: we intentionally keep _isfInputs, _isfPasses and dynamic ports here. | |
| // They are driven by the Settings "isfPath" and are persistent. | |
| return YES; | |
| } | |
| - (void)stopExecution:(id<QCPlugInContext>)context | |
| { | |
| (void)context; | |
| NSLog(@"[ISFRenderer] stopExecution"); | |
| [self destroyGLResources]; | |
| _cglContext = NULL; | |
| } | |
| // Multi-pass execute: honors PASSES, PASSINDEX, FRAMEINDEX, persistent buffers with ping-pong. | |
| - (BOOL)execute:(id<QCPlugInContext>)context | |
| atTime:(NSTimeInterval)time | |
| withArguments:(NSDictionary *)arguments | |
| { | |
| (void)arguments; | |
| // New "Enable" port: when false, skip all rendering and output nil. | |
| if (!self.inputEnabled) { | |
| self.outputImage = nil; | |
| return YES; | |
| } | |
| if (self.isfPath == nil || [self.isfPath length] == 0) { | |
| self.outputImage = nil; | |
| return YES; | |
| } | |
| if (!_cglContext) | |
| _cglContext = [context CGLContextObj]; | |
| if (!_cglContext) { | |
| self.outputImage = nil; | |
| return YES; | |
| } | |
| CGLSetCurrentContext(_cglContext); | |
| // ---------- SAVE GL STATE ---------- | |
| GLint prevFBO = 0; | |
| GLint prevViewport[4] = {0,0,0,0}; | |
| GLint prevProgram = 0; | |
| GLint prevArrayBuffer = 0; | |
| GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST); | |
| GLboolean prevBlend = glIsEnabled(GL_BLEND); | |
| glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFBO); | |
| glGetIntegerv(GL_VIEWPORT, prevViewport); | |
| glGetIntegerv(GL_CURRENT_PROGRAM, &prevProgram); | |
| glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &prevArrayBuffer); | |
| // Initialize start time once | |
| if (_startTime == 0.0) | |
| _startTime = time; | |
| // TIMEDELTA | |
| GLfloat deltaTime = 0.0f; | |
| if (self.lastTime > 0.0) { | |
| deltaTime = (GLfloat)(time - self.lastTime); | |
| } | |
| self.lastTime = time; | |
| // Ensure shader is compiled/linked | |
| if (![self loadISFInContext:context]) { | |
| self.outputImage = nil; | |
| ISFRestoreGLState(_cglContext, prevFBO, prevViewport, prevProgram, prevArrayBuffer, prevDepthTest, prevBlend); | |
| return YES; | |
| } | |
| // Determine requested output size (for default pass) | |
| double wD = self.inputWidth; | |
| double hD = self.inputHeight; | |
| if (wD <= 0.0) wD = 640.0; | |
| if (hD <= 0.0) hD = 360.0; | |
| size_t w = (size_t)llround(wD); | |
| size_t h = (size_t)llround(hD); | |
| if (w == 0 || h == 0) { | |
| self.outputImage = nil; | |
| ISFRestoreGLState(_cglContext, prevFBO, prevViewport, prevProgram, prevArrayBuffer, prevDepthTest, prevBlend); | |
| return YES; | |
| } | |
| if (![self ensureRenderTargetWidth:w height:h]) { | |
| self.outputImage = nil; | |
| ISFRestoreGLState(_cglContext, prevFBO, prevViewport, prevProgram, prevArrayBuffer, prevDepthTest, prevBlend); | |
| return YES; | |
| } | |
| glDisable(GL_DEPTH_TEST); | |
| glDisable(GL_BLEND); | |
| // Built-in time values | |
| GLfloat tVal = (GLfloat)(time - _startTime); | |
| self.frameIndex += 1; | |
| // DATE (year, month, day, seconds since midnight) | |
| NSDate *now = [NSDate date]; | |
| NSCalendar *cal = [NSCalendar currentCalendar]; | |
| NSDateComponents *comp = | |
| [cal components:(NSCalendarUnitYear | | |
| NSCalendarUnitMonth | | |
| NSCalendarUnitDay | | |
| NSCalendarUnitHour | | |
| NSCalendarUnitMinute | | |
| NSCalendarUnitSecond) | |
| fromDate:now]; | |
| GLfloat year = (GLfloat)[comp year]; | |
| GLfloat month = (GLfloat)[comp month]; | |
| GLfloat day = (GLfloat)[comp day]; | |
| GLfloat sec = (GLfloat)([comp hour] * 3600 + | |
| [comp minute] * 60 + | |
| [comp second]); | |
| // How many passes to execute? | |
| NSArray *passes = self.isfPasses; | |
| NSInteger passCount = (passes && passes.count > 0) ? passes.count : 1; | |
| GLuint finalTexName = 0; | |
| size_t finalTexWidth = 0; | |
| size_t finalTexHeight = 0; | |
| for (NSInteger passIndex = 0; passIndex < passCount; ++passIndex) { | |
| // For any non-empty PASSES array, use the pass descriptor at this index. | |
| NSDictionary *passDesc = (passes && passIndex < passes.count) ? passes[passIndex] : nil; | |
| NSString *targetName = nil; | |
| BOOL persistent = NO; | |
| BOOL isFloat = NO; | |
| if (passDesc) { | |
| id t = passDesc[@"TARGET"]; | |
| if ([t isKindOfClass:[NSString class]] && [t length] > 0) | |
| targetName = (NSString *)t; | |
| id p = passDesc[@"PERSISTENT"]; | |
| if ([p respondsToSelector:@selector(boolValue)]) | |
| persistent = [p boolValue]; | |
| id f = passDesc[@"FLOAT"]; | |
| if ([f respondsToSelector:@selector(boolValue)]) | |
| isFloat = [f boolValue]; | |
| } | |
| BOOL isLastPass = (passIndex == passCount - 1); | |
| // Evaluate WIDTH/HEIGHT expressions for this pass if present | |
| CGFloat passWf = (CGFloat)w; | |
| CGFloat passHf = (CGFloat)h; | |
| if (passDesc) { | |
| NSString *wExpr = passDesc[@"WIDTH"]; | |
| NSString *hExpr = passDesc[@"HEIGHT"]; | |
| if ([wExpr isKindOfClass:[NSString class]] && wExpr.length > 0) { | |
| passWf = [self evaluateSizeExpression:wExpr | |
| defaultVal:(CGFloat)w | |
| outputWidthVal:(CGFloat)w | |
| outputHeightVal:(CGFloat)h]; | |
| } | |
| if ([hExpr isKindOfClass:[NSString class]] && hExpr.length > 0) { | |
| passHf = [self evaluateSizeExpression:hExpr | |
| defaultVal:(CGFloat)h | |
| outputWidthVal:(CGFloat)w | |
| outputHeightVal:(CGFloat)h]; | |
| } | |
| } | |
| size_t passW = (size_t)llround(fmax(passWf, 1.0)); | |
| size_t passH = (size_t)llround(fmax(passHf, 1.0)); | |
| GLuint passFBO = 0; | |
| GLuint passTex = 0; | |
| ISFBuffer *targetBuf = nil; | |
| if (targetName && [targetName length] > 0) { | |
| // Render into named buffer | |
| targetBuf = [self bufferForTargetName:targetName | |
| persistent:persistent | |
| float:isFloat | |
| width:passW | |
| height:passH]; | |
| if (!targetBuf) { | |
| NSLog(@"[ISFRenderer] Failed to create PASS TARGET '%@'", targetName); | |
| continue; | |
| } | |
| passFBO = targetBuf.writeFBO; | |
| passTex = targetBuf.writeTex; | |
| passW = targetBuf.width; | |
| passH = targetBuf.height; | |
| glBindFramebuffer(GL_FRAMEBUFFER, passFBO); | |
| glViewport(0, 0, (GLsizei)passW, (GLsizei)passH); | |
| // For non-persistent, clear each pass. For persistent, clearing writeTex is safe: | |
| // shader samples from readTex, not from writeTex. | |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| } | |
| else { | |
| // No explicit TARGET: render to the main FBO (typically last pass) | |
| glBindFramebuffer(GL_FRAMEBUFFER, _fbo); | |
| glViewport(0, 0, (GLsizei)w, (GLsizei)h); | |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| passFBO = _fbo; | |
| passTex = _colorTex; | |
| passW = w; | |
| passH = h; | |
| } | |
| glUseProgram(_program); | |
| // Built-in uniforms for this pass | |
| GLint timeLoc = glGetUniformLocation(_program, "TIME"); | |
| if (timeLoc >= 0) | |
| glUniform1f(timeLoc, tVal); | |
| GLint deltaLoc = glGetUniformLocation(_program, "TIMEDELTA"); | |
| if (deltaLoc >= 0) | |
| glUniform1f(deltaLoc, deltaTime); | |
| GLint renderSizeLoc = glGetUniformLocation(_program, "RENDERSIZE"); | |
| if (renderSizeLoc >= 0) | |
| glUniform2f(renderSizeLoc, (GLfloat)passW, (GLfloat)passH); | |
| GLint dateLoc = glGetUniformLocation(_program, "DATE"); | |
| if (dateLoc >= 0) | |
| glUniform4f(dateLoc, year, month, day, sec); | |
| GLint frameIndexLoc = glGetUniformLocation(_program, "FRAMEINDEX"); | |
| if (frameIndexLoc >= 0) | |
| glUniform1i(frameIndexLoc, (GLint)self.frameIndex); | |
| GLint passIndexLoc = glGetUniformLocation(_program, "PASSINDEX"); | |
| if (passIndexLoc >= 0) | |
| glUniform1i(passIndexLoc, (GLint)passIndex); | |
| // Apply all ISF INPUTS (images, floats, etc.), plus bind any PASS TARGET buffers as samplers. | |
| [self applyISFInputDefaultsWithContext:context]; | |
| // Draw fullscreen quad | |
| glBindBuffer(GL_ARRAY_BUFFER, _vbo); | |
| GLint posLoc = glGetAttribLocation(_program, "position"); | |
| if (posLoc >= 0) { | |
| glEnableVertexAttribArray((GLuint)posLoc); | |
| glVertexAttribPointer((GLuint)posLoc, | |
| 2, | |
| GL_FLOAT, | |
| GL_FALSE, | |
| 2 * sizeof(GLfloat), | |
| (const GLvoid *)0); | |
| } | |
| glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); | |
| if (posLoc >= 0) { | |
| glDisableVertexAttribArray((GLuint)posLoc); | |
| } | |
| glBindBuffer(GL_ARRAY_BUFFER, 0); | |
| // If this pass writes to a persistent buffer, swap read/write so | |
| // subsequent passes (or next frame) see the just-rendered contents. | |
| if (targetBuf && targetBuf.isPersistent) { | |
| [self swapPersistentBuffer:targetBuf]; | |
| } | |
| // Remember "final" texture & size from the last executed pass | |
| if (isLastPass) { | |
| finalTexName = passTex; | |
| finalTexWidth = passW; | |
| finalTexHeight = passH; | |
| } | |
| } | |
| // ---------- WRAP FINAL TEXTURE AS QC IMAGE ---------- | |
| if (finalTexName == 0) { | |
| // fall back to main output texture if something weird happened | |
| finalTexName = _colorTex; | |
| finalTexWidth = w; | |
| finalTexHeight = h; | |
| } | |
| id<QCPlugInOutputImageProvider> provider = nil; | |
| @try { | |
| provider = | |
| [context outputImageProviderFromTextureWithPixelFormat:QCPlugInPixelFormatBGRA8 | |
| pixelsWide:finalTexWidth | |
| pixelsHigh:finalTexHeight | |
| name:finalTexName | |
| flipped:NO | |
| releaseCallback:ISFTextureReleaseCallback | |
| releaseContext:NULL | |
| colorSpace:[context colorSpace] | |
| shouldColorMatch:YES]; | |
| } | |
| @catch (NSException *ex) { | |
| NSLog(@"[ISFRenderer] Exception while creating output image provider: %@", ex); | |
| provider = nil; | |
| } | |
| self.outputImage = provider; | |
| // Restore GL state | |
| ISFRestoreGLState(_cglContext, prevFBO, prevViewport, prevProgram, prevArrayBuffer, prevDepthTest, prevBlend); | |
| return YES; | |
| } | |
| @end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ISFRendererPlugInViewController.h | |
| // Simple Settings-tab UI for ISFRendererPlugIn: | |
| // - Label "ISF Path:" | |
| // - Text field for the path | |
| // - "Choose…" button to pick a file | |
| #import <Quartz/Quartz.h> | |
| #import <AppKit/AppKit.h> | |
| @interface ISFRendererPlugInViewController : QCPlugInViewController | |
| @property(nonatomic, strong) NSTextField *pathField; | |
| @property(nonatomic, strong) NSButton *browseButton; | |
| @end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ISFRendererPlugInViewController.m | |
| #import "ISFRendererPlugInViewController.h" | |
| @implementation ISFRendererPlugInViewController | |
| @synthesize pathField = _pathField; | |
| @synthesize browseButton = _browseButton; | |
| - (id)initWithPlugIn:(QCPlugIn *)plugIn viewNibName:(NSString *)nibName | |
| { | |
| self = [super initWithPlugIn:plugIn viewNibName:nibName]; | |
| if (self) { | |
| // nothing special | |
| } | |
| return self; | |
| } | |
| - (void)loadView | |
| { | |
| // Root view for the Settings tab | |
| NSView *root = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 360, 60)]; | |
| root.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; | |
| CGFloat padding = 8.0; | |
| CGFloat gap = 6.0; | |
| CGFloat fieldH = 22.0; | |
| CGFloat buttonW = 80.0; | |
| // LABEL: "ISF Path:" | |
| NSTextField *label = [[NSTextField alloc] initWithFrame:NSZeroRect]; | |
| label.stringValue = @"ISF Path:"; | |
| label.bezeled = NO; | |
| label.drawsBackground = NO; | |
| label.editable = NO; | |
| label.selectable = NO; | |
| [label sizeToFit]; | |
| NSRect bounds = root.bounds; | |
| NSRect labelFrame = label.frame; | |
| labelFrame.origin.x = padding; | |
| labelFrame.origin.y = NSHeight(bounds) - padding - NSHeight(labelFrame); | |
| label.frame = labelFrame; | |
| label.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin; | |
| // TEXT FIELD: shows / edits the path | |
| CGFloat fieldX = NSMaxX(labelFrame) + gap; | |
| CGFloat fieldRight = NSWidth(bounds) - padding - buttonW - gap; | |
| if (fieldRight < fieldX + 60.0) { | |
| fieldRight = fieldX + 60.0; | |
| } | |
| NSRect fieldFrame = NSMakeRect(fieldX, | |
| NSMinY(labelFrame) - 2.0, | |
| fieldRight - fieldX, | |
| fieldH); | |
| NSTextField *field = [[NSTextField alloc] initWithFrame:fieldFrame]; | |
| field.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; | |
| [field setTarget:self]; | |
| [field setAction:@selector(_pathFieldChanged:)]; | |
| // BUTTON: "Choose…" to open NSOpenPanel | |
| NSRect buttonFrame = NSMakeRect(NSMaxX(fieldFrame) + gap, | |
| NSMinY(fieldFrame), | |
| buttonW, | |
| fieldH + 2.0); | |
| NSButton *button = [[NSButton alloc] initWithFrame:buttonFrame]; | |
| [button setButtonType:NSMomentaryPushInButton]; | |
| [button setBezelStyle:NSBezelStyleRounded]; | |
| button.title = @"Choose…"; | |
| button.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; | |
| [button setTarget:self]; | |
| [button setAction:@selector(_chooseButtonClicked:)]; | |
| [root addSubview:label]; | |
| [root addSubview:field]; | |
| [root addSubview:button]; | |
| self.pathField = field; | |
| self.browseButton = button; | |
| self.view = root; | |
| } | |
| - (void)viewWillAppear | |
| { | |
| [super viewWillAppear]; | |
| // Initialize UI from plug-in's isfPath | |
| id plugin = self.plugIn; | |
| if (plugin) { | |
| @try { | |
| id v = [plugin valueForKey:@"isfPath"]; | |
| if (v && v != [NSNull null]) { | |
| self.pathField.stringValue = [v description]; | |
| } else { | |
| self.pathField.stringValue = @""; | |
| } | |
| } @catch (__unused id ex) { | |
| self.pathField.stringValue = @""; | |
| } | |
| } | |
| } | |
| #pragma mark - Actions | |
| - (void)_pathFieldChanged:(id)sender | |
| { | |
| (void)sender; | |
| NSString *path = [self.pathField.stringValue | |
| stringByTrimmingCharactersInSet: | |
| [NSCharacterSet whitespaceAndNewlineCharacterSet]]; | |
| id plugin = self.plugIn; | |
| if (plugin) { | |
| @try { | |
| [plugin setValue:path forKey:@"isfPath"]; // KVC onto ISFRendererPlugIn | |
| } @catch (__unused id ex) { | |
| } | |
| } | |
| } | |
| - (void)_chooseButtonClicked:(id)sender | |
| { | |
| (void)sender; | |
| NSOpenPanel *panel = [NSOpenPanel openPanel]; | |
| panel.canChooseFiles = YES; | |
| panel.canChooseDirectories = NO; | |
| panel.allowsMultipleSelection = NO; | |
| panel.allowedFileTypes = @[@"fs", @"frag", @"glsl"]; | |
| panel.prompt = @"Choose"; | |
| if ([panel runModal] == NSModalResponseOK) { | |
| NSURL *url = panel.URL; | |
| if (!url) return; | |
| NSString *path = url.path; | |
| if (!path) path = @""; | |
| self.pathField.stringValue = path; | |
| id plugin = self.plugIn; | |
| if (plugin) { | |
| @try { | |
| [plugin setValue:path forKey:@"isfPath"]; | |
| } @catch (__unused id ex) { | |
| } | |
| } | |
| } | |
| } | |
| @end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment