Last active
November 27, 2025 05:01
-
-
Save g-l-i-t-c-h-o-r-s-e/537a42a1cd298c885829583b55f64a55 to your computer and use it in GitHub Desktop.
Revised ISF Fragment Shader Plugin for 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 | |
| // Internally cached path to the ISF file currently in use. | |
| // Driven by the inputISFPath QC port and/or the Settings tab. | |
| NSString *_isfPath; | |
| // Last value seen on the input path port (for edge-triggered reload scheduling) | |
| NSString *_lastInputISFPath; | |
| } | |
| // QC ports | |
| @property (assign) id<QCPlugInOutputImageProvider> outputImage; | |
| // Output size ports | |
| @property double inputWidth; | |
| @property double inputHeight; | |
| // Enable / disable rendering | |
| @property (assign) BOOL inputEnabled; | |
| // Persistent QC input image port mapped to ISF image INPUTs | |
| @property (assign) id<QCPlugInInputImageSource> inputImage; | |
| // Filesystem path to the ISF fragment shader (QC input port) | |
| @property (copy) NSString *inputISFPath; | |
| // Internal setting used by the loader (fed from inputISFPath or Settings) | |
| @property (copy) NSString *isfPath; | |
| // Settings toggle – when YES, use static port names (P1..P17) instead of hashed names. | |
| @property (assign, nonatomic) BOOL useStaticInputPortNames; | |
| // Settings toggle – when YES, remove all numeric input ports and expose a single | |
| // structure input port that the renderer reads ISF INPUT values from. | |
| // When this is ON, it overrides useStaticInputPortNames. | |
| @property (assign, nonatomic) BOOL useStructureInput; | |
| // Settings toggle – when YES, the first rendered frame does not fall back to | |
| // ISF JSON DEFAULT values; only QC input/structure values are used. | |
| @property (assign, nonatomic) BOOL ignoreDefaultsOnFirstFrame; | |
| @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 <Quartz/Quartz.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]; | |
| } | |
| } | |
| // Static "generic" port key, used when useStaticInputPortNames == YES. | |
| // P1 -> "P1", P2 -> "P2", etc. We intentionally keep the QC key identical | |
| // to the visible label so that when Quartz Composer reloads a composition | |
| // and falls back to using the key as the display name, ports still appear | |
| // as "P1", "P2", "P3", ... | |
| static NSString *ISFStaticPortKeyForIndex(NSUInteger idx) | |
| { | |
| return [NSString stringWithFormat:@"P%lu", (unsigned long)(idx + 1)]; | |
| } | |
| // Number of boolean slots we expose at the top of the static port layout. | |
| static const NSUInteger kISFStaticMaxBoolPorts = 3; | |
| // Total number of static ports in static mode: 3 bool + 14 others = 17. | |
| static const NSUInteger kISFStaticTotalStaticPorts = 17; | |
| static const NSUInteger kISFStaticMaxNonBoolPorts = | |
| (kISFStaticTotalStaticPorts - kISFStaticMaxBoolPorts); | |
| // Name of the structure input port used when useStructureInput == YES. | |
| static NSString * const kISFStructureInputPortKey = @"inputParams"; | |
| #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; | |
| // Tracks whether the input port is currently "driving" the ISF path | |
| @property (nonatomic, assign) BOOL portPathIsDriving; | |
| // Tracks whether we are rendering the first frame of this execution | |
| @property (nonatomic, assign) BOOL isRenderingFirstFrame; | |
| // Snapshot of last structure-input params | |
| @property (nonatomic, strong) NSDictionary *lastParamsStructSnapshot; | |
| // Freeze structure-input params for this frame (when path changes) | |
| @property (nonatomic, assign) BOOL freezeStructureInputThisFrame; | |
| @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; | |
| @dynamic inputISFPath; | |
| @dynamic inputImage; // persistent image input port | |
| @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; | |
| @synthesize useStaticInputPortNames = _useStaticInputPortNames; | |
| @synthesize useStructureInput = _useStructureInput; | |
| @synthesize ignoreDefaultsOnFirstFrame = _ignoreDefaultsOnFirstFrame; | |
| // ------------------------------------------------------------------ | |
| // Plug-in metadata | |
| // ------------------------------------------------------------------ | |
| + (NSDictionary *)attributes | |
| { | |
| return @{ | |
| QCPlugInAttributeNameKey : kQCPlugIn_Name, | |
| QCPlugInAttributeDescriptionKey : kQCPlugIn_Description | |
| }; | |
| } | |
| // Attributes for input/output ports | |
| + (NSDictionary *)attributesForPropertyPortWithKey:(NSString *)key | |
| { | |
| 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" | |
| }; | |
| } | |
| else if ([key isEqualToString:@"inputISFPath"]) { | |
| return @{ | |
| QCPortAttributeNameKey : @"ISF Path", | |
| QCPortAttributeTypeKey : QCPortTypeString, | |
| QCPortAttributeDefaultValueKey : @"" | |
| }; | |
| } | |
| else if ([key isEqualToString:@"inputImage"]) { | |
| // NEW persistent image input port | |
| return @{ | |
| QCPortAttributeNameKey : @"Source Image", | |
| QCPortAttributeTypeKey : QCPortTypeImage | |
| }; | |
| } | |
| 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 | |
| { | |
| // Settings: "isfPath", "useStaticInputPortNames", "useStructureInput", "ignoreDefaultsOnFirstFrame" | |
| return @[ @"isfPath", | |
| @"useStaticInputPortNames", | |
| @"useStructureInput", | |
| @"ignoreDefaultsOnFirstFrame" ]; | |
| } | |
| // 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" | |
| }; | |
| } | |
| else if ([key isEqualToString:@"useStaticInputPortNames"]) { | |
| return @{ | |
| QCPlugInAttributeNameKey : @"Static input names", | |
| QCPlugInAttributeDescriptionKey : @"When ON, dynamic input ports use stable keys (no per-file hash) so QC port connections persist when changing shaders." | |
| }; | |
| } | |
| else if ([key isEqualToString:@"useStructureInput"]) { | |
| return @{ | |
| QCPlugInAttributeNameKey : @"Structure input", | |
| QCPlugInAttributeDescriptionKey : @"When ON, ISF INPUTs are driven from a single structure port (Params) instead of individual QC ports." | |
| }; | |
| } | |
| else if ([key isEqualToString:@"ignoreDefaultsOnFirstFrame"]) { | |
| return @{ | |
| QCPlugInAttributeNameKey : @"Ignore ISF DEFAULTs on first frame", | |
| QCPlugInAttributeDescriptionKey : @"When ON, the first rendered frame does not fall back to ISF JSON DEFAULT values; only QC input/structure values are used." | |
| }; | |
| } | |
| 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 | |
| - (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/port default | |
| _lastInputISFPath = nil; | |
| _isfPasses = nil; | |
| _passTargets = [[NSMutableDictionary alloc] init]; | |
| _frameIndex = 0; | |
| _lastTime = 0.0; | |
| _flipTex = 0; | |
| _flipFBO = 0; | |
| _flipWidth = 0; | |
| _flipHeight = 0; | |
| self.portPathIsDriving = NO; // default to "Settings tab drives the path" | |
| self.useStaticInputPortNames = NO; // default: hashed names per ISF file | |
| _useStructureInput = NO; // default: per-INPUT ports, no structure port | |
| _ignoreDefaultsOnFirstFrame = NO; // default: normal ISF DEFAULT behavior | |
| self.isRenderingFirstFrame = NO; | |
| self.lastParamsStructSnapshot = nil; | |
| self.freezeStructureInputThisFrame = NO; | |
| } | |
| return self; | |
| } | |
| - (void)dealloc | |
| { | |
| // GL cleanup happens in -stopExecution: | |
| } | |
| #pragma mark - Dynamic ports helpers | |
| // ====================================================================== | |
| // Plug-in Settings: ISF path & dynamic QC ports | |
| // ====================================================================== | |
| // Ensure the single structure input port exists (used when useStructureInput == YES). | |
| - (void)_ensureStructureInputPort | |
| { | |
| NSDictionary *attrs = @{ | |
| QCPortAttributeNameKey : @"Params" | |
| }; | |
| @try { | |
| [self addInputPortWithType:QCPortTypeStructure | |
| forKey:kISFStructureInputPortKey | |
| withAttributes:attrs]; | |
| } | |
| @catch (NSException *ex) { | |
| // Usually means the port already exists; ignore. | |
| } | |
| } | |
| // Remove the structure input port if it exists (used when useStructureInput == NO). | |
| - (void)_destroyStructureInputPort | |
| { | |
| @try { | |
| [self removeInputPortForKey:kISFStructureInputPortKey]; | |
| } | |
| @catch (NSException *ex) { | |
| // Missing / protected port ? ignore. | |
| } | |
| } | |
| // Hard reset: remove all dynamic ports we know about and any leftover | |
| // static P# ports that Quartz Composer may have resurrected from a | |
| // previously-saved composition. | |
| - (void)_removeDynamicInputPorts | |
| { | |
| // First pass: anything we've explicitly tracked. | |
| if (_dynamicInputKeys) { | |
| for (NSString *key in _dynamicInputKeys) { | |
| @try { | |
| [self removeInputPortForKey:key]; | |
| } | |
| @catch (NSException *ex) { | |
| // If QC thinks the port doesn't exist any more, ignore it. | |
| } | |
| } | |
| [_dynamicInputKeys removeAllObjects]; | |
| } | |
| // Second pass: belt-and-suspenders cleanup for orphaned static ports. | |
| // Older builds (and QC itself) may have created ports that aren't in | |
| // _dynamicInputKeys anymore. We know our static ports are P1..Pn or | |
| // the legacy isfP1..isfPn, so try removing a reasonable range. | |
| static const NSUInteger kMaxStaticPorts = 32; | |
| for (NSUInteger i = 0; i < kMaxStaticPorts; ++i) { | |
| NSString *pKey = [NSString stringWithFormat:@"P%lu", (unsigned long)(i + 1)]; | |
| NSString *oldKey = [NSString stringWithFormat:@"isfP%lu", (unsigned long)(i + 1)]; | |
| @try { [self removeInputPortForKey:pKey]; } @catch (NSException *ex) {} | |
| @try { [self removeInputPortForKey:oldKey]; } @catch (NSException *ex) {} | |
| } | |
| } | |
| // Build dynamic ports from _isfInputs (parsed JSON header). | |
| // - Structure mode (useStructureInput == YES): | |
| // * No per-INPUT QC ports are created. | |
| // * A single structure port "Params" (inputParams) is used instead. | |
| // - Static mode (useStaticInputPortNames == YES & !useStructureInput): | |
| // * Always exposes exactly 17 ports: P1-P3 bool, P4-P17 number. | |
| // * First 3 ISF bools -> P1-P3, first 14 numeric (float/event/int/long) | |
| // -> P4-P17. Unused slots stay as number ports. | |
| // - Hashed mode (useStaticInputPortNames == NO & !useStructureInput): | |
| // * Per-ISF dynamic ports with hashed keys, as before. | |
| - (void)_rebuildDynamicInputPortsFromCurrentISFInputs | |
| { | |
| // Structure-input mode: no per-INPUT QC ports. | |
| // All values come from the single "Params" structure input. | |
| if (self.useStructureInput) { | |
| [self _ensureStructureInputPort]; | |
| return; | |
| } | |
| // Ensure tracking array exists | |
| if (!_dynamicInputKeys) { | |
| _dynamicInputKeys = [[NSMutableArray alloc] init]; | |
| } | |
| // ------------------------------------------------------------------ | |
| // STATIC MODE: fixed ports P1..P17, independent of INPUT count | |
| // ------------------------------------------------------------------ | |
| if (self.useStaticInputPortNames) { | |
| // Collect up to 3 bool defaults and 14 numeric defaults | |
| NSMutableArray<NSNumber *> *boolDefaults = | |
| [NSMutableArray arrayWithCapacity:kISFStaticMaxBoolPorts]; | |
| NSMutableArray<NSNumber *> *numDefaults = | |
| [NSMutableArray arrayWithCapacity:kISFStaticMaxNonBoolPorts]; | |
| for (NSDictionary *input in _isfInputs) { | |
| NSString *type = input[@"TYPE"]; | |
| id defVal = input[@"DEFAULT"]; | |
| if (![type isKindOfClass:[NSString class]]) { | |
| continue; | |
| } | |
| if ([type isEqualToString:@"bool"]) { | |
| if (boolDefaults.count < kISFStaticMaxBoolPorts) { | |
| BOOL b = defVal ? [defVal boolValue] : NO; | |
| [boolDefaults addObject:@(b)]; | |
| } | |
| } | |
| else if ([type isEqualToString:@"float"] || | |
| [type isEqualToString:@"event"] || | |
| [type isEqualToString:@"long"] || | |
| [type isEqualToString:@"int"]) { | |
| if (numDefaults.count < kISFStaticMaxNonBoolPorts) { | |
| double v = 0.0; | |
| if (defVal && [defVal respondsToSelector:@selector(doubleValue)]) { | |
| v = [defVal doubleValue]; | |
| } | |
| [numDefaults addObject:@(v)]; | |
| } | |
| } | |
| // color / point2D / image / audio / audioFFT are not mapped to P-ports. | |
| } | |
| // --- Ensure P1..P3 exist as boolean ports ---------------------- | |
| for (NSUInteger i = 0; i < kISFStaticMaxBoolPorts; ++i) { | |
| NSString *boolKey = ISFStaticPortKeyForIndex(i); // "P1", "P2", "P3" | |
| NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; | |
| attrs[QCPortAttributeNameKey] = boolKey; | |
| BOOL b = NO; | |
| if (i < boolDefaults.count) { | |
| b = [boolDefaults[i] boolValue]; | |
| } | |
| attrs[QCPortAttributeDefaultValueKey] = @(b ? 1.0 : 0.0); | |
| @try { | |
| [self addInputPortWithType:QCPortTypeBoolean | |
| forKey:boolKey | |
| withAttributes:attrs]; | |
| } | |
| @catch (NSException *ex) { | |
| // Already exists; keep it to preserve connections. | |
| } | |
| if (![_dynamicInputKeys containsObject:boolKey]) { | |
| [_dynamicInputKeys addObject:boolKey]; | |
| } | |
| } | |
| // --- Ensure P4..P17 exist as number ports ---------------------- | |
| for (NSUInteger slot = 0; slot < kISFStaticMaxNonBoolPorts; ++slot) { | |
| NSUInteger staticIdx = kISFStaticMaxBoolPorts + slot; // 3..16 -> P4..P17 | |
| NSString *numKey = ISFStaticPortKeyForIndex(staticIdx); | |
| NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; | |
| attrs[QCPortAttributeNameKey] = numKey; | |
| double v = 0.0; | |
| if (slot < numDefaults.count) { | |
| v = [numDefaults[slot] doubleValue]; | |
| } | |
| attrs[QCPortAttributeDefaultValueKey] = @(v); | |
| @try { | |
| [self addInputPortWithType:QCPortTypeNumber | |
| forKey:numKey | |
| withAttributes:attrs]; | |
| } | |
| @catch (NSException *ex) { | |
| // Already exists; keep it to preserve connections. | |
| } | |
| if (![_dynamicInputKeys containsObject:numKey]) { | |
| [_dynamicInputKeys addObject:numKey]; | |
| } | |
| } | |
| // IMPORTANT: | |
| // - We do NOT remove P1..P17 here. | |
| // - The persistent "Source Image" port is a property port and is | |
| // not touched here at all. | |
| return; | |
| } | |
| // ------------------------------------------------------------------ | |
| // HASHED MODE: dynamic per-INPUT ports (no dynamic image ports) | |
| // ------------------------------------------------------------------ | |
| if (!_isfInputs || [_isfInputs count] == 0) { | |
| [self _removeDynamicInputPorts]; | |
| return; | |
| } | |
| NSString *path = self.isfPath ?: @""; | |
| NSString *signature = ISFPortSignatureForPath(path); | |
| NSMutableSet *desiredKeys = [NSMutableSet set]; | |
| 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; | |
| } | |
| NSString *portType = nil; | |
| id qcDefault = nil; | |
| 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"]) { | |
| 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"]) { | |
| 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:@"audio"] || | |
| [type isEqualToString:@"audioFFT"]) { | |
| portType = QCPortTypeImage; | |
| } | |
| else if ([type isEqualToString:@"image"]) { | |
| // IMPORTANT: DO NOT create dynamic image ports anymore. | |
| // All ISF image inputs are driven from the persistent 'Source Image' port. | |
| portType = nil; | |
| } | |
| if (!portType) { | |
| continue; | |
| } | |
| NSString *portKey = ISFPortKeyForInputName(name, signature); | |
| NSString *displayName = name; | |
| [desiredKeys addObject:portKey]; | |
| NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; | |
| attrs[QCPortAttributeNameKey] = displayName; | |
| if (qcDefault) { | |
| attrs[QCPortAttributeDefaultValueKey] = qcDefault; | |
| } | |
| BOOL alreadyExists = [_dynamicInputKeys containsObject:portKey]; | |
| if (!alreadyExists) { | |
| @try { | |
| [self addInputPortWithType:portType | |
| forKey:portKey | |
| withAttributes:attrs]; | |
| } | |
| @catch (NSException *ex) { | |
| // If QC complains, just skip this port. | |
| } | |
| if (![_dynamicInputKeys containsObject:portKey]) { | |
| [_dynamicInputKeys addObject:portKey]; | |
| } | |
| } | |
| } | |
| // Remove stale hashed ports that no longer correspond to an ISF INPUT | |
| NSArray *existing = [_dynamicInputKeys copy]; | |
| for (NSString *key in existing) { | |
| if (![desiredKeys containsObject:key]) { | |
| @try { | |
| [self removeInputPortForKey:key]; | |
| } | |
| @catch (NSException *ex) { | |
| // ignore | |
| } | |
| [_dynamicInputKeys removeObject:key]; | |
| } | |
| } | |
| } | |
| // Helper: read ISF file, parse JSON header, rebuild dynamic ports. | |
| // This is called whenever the Settings "ISF Path" changes. | |
| - (void)_reloadISFInputsAndDynamicPortsFromPath:(NSString *)path | |
| { | |
| // IMPORTANT: | |
| // - In NON-static (hashed) mode, the per-file signature means the set of | |
| // ports really belongs to that specific ISF. When the path changes, we | |
| // invalidate that whole set and rebuild from scratch. | |
| // | |
| // - In STATIC mode, P1/P2/P3... are *intentionally* independent of the | |
| // ISF file. We MUST NOT destroy them here, otherwise Quartz Composer | |
| // loses any existing connections to those ports. Instead, we just | |
| // update our internal ISF metadata and keep using the same ports. | |
| if (!self.useStaticInputPortNames && !self.useStructureInput) { | |
| [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; | |
| // IMPORTANT: | |
| // - When Quartz Composer is *not* executing this plug-in yet (no GL context), | |
| // we can safely parse the ISF JSON and rebuild the QC ports immediately. | |
| // | |
| // - When a GL context exists (we're actively executing), we *must not* | |
| // rewrite _isfInputs and the port layout before the GL program is | |
| // actually reloaded, or we get a one-frame mismatch where the old shader | |
| // runs with the new INPUT metadata. | |
| // | |
| // In the executing case, we simply mark _needsReload above and | |
| // -loadISFInContext: will both reload the program *and* rebuild ports | |
| // right before the next frame renders. | |
| if (_cglContext == NULL) { | |
| [self _reloadISFInputsAndDynamicPortsFromPath:_isfPath]; | |
| } | |
| } | |
| - (NSString *)isfPath | |
| { | |
| return _isfPath; | |
| } | |
| // Custom setter for settings toggle (static vs hashed names) | |
| - (void)setUseStaticInputPortNames:(BOOL)flag | |
| { | |
| if (_useStaticInputPortNames == flag) | |
| return; | |
| _useStaticInputPortNames = flag; | |
| NSLog(@"[ISFRenderer] useStaticInputPortNames changed to %d", (int)flag); | |
| // In structure-input mode, static vs hashed naming is ignored. | |
| // Do not touch ports; the single structure port remains. | |
| if (self.useStructureInput) { | |
| return; | |
| } | |
| // When switching modes, always clear dynamic ports and rebuild from current path. | |
| [self _removeDynamicInputPorts]; | |
| __weak typeof(self) weakSelf = self; | |
| dispatch_async(dispatch_get_main_queue(), ^{ | |
| __strong typeof(self) strongSelf = weakSelf; | |
| if (!strongSelf) return; | |
| [strongSelf _reloadISFInputsAndDynamicPortsFromPath:strongSelf.isfPath]; | |
| }); | |
| } | |
| // Custom setter for structure-input mode | |
| - (void)setUseStructureInput:(BOOL)flag | |
| { | |
| if (_useStructureInput == flag) | |
| return; | |
| _useStructureInput = flag; | |
| NSLog(@"[ISFRenderer] useStructureInput changed to %d", (int)flag); | |
| __weak typeof(self) weakSelf = self; | |
| dispatch_async(dispatch_get_main_queue(), ^{ | |
| __strong typeof(self) strongSelf = weakSelf; | |
| if (!strongSelf) return; | |
| if (strongSelf.useStructureInput) { | |
| // Turn ON structure mode: | |
| // - remove all per-INPUT QC ports (hashed/static) | |
| // - ensure the structure port exists | |
| [strongSelf _removeDynamicInputPorts]; | |
| [strongSelf _ensureStructureInputPort]; | |
| // Reset snapshot/freezer when entering structure mode | |
| strongSelf.lastParamsStructSnapshot = nil; | |
| strongSelf.freezeStructureInputThisFrame = NO; | |
| } else { | |
| // Turn OFF structure mode: | |
| // - remove the structure port | |
| // - rebuild dynamic ports for the current path using static/hashed names | |
| [strongSelf _destroyStructureInputPort]; | |
| [strongSelf _reloadISFInputsAndDynamicPortsFromPath:strongSelf.isfPath]; | |
| // Not using structure input anymore | |
| strongSelf.lastParamsStructSnapshot = nil; | |
| strongSelf.freezeStructureInputThisFrame = NO; | |
| } | |
| }); | |
| } | |
| #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. | |
| - (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; | |
| 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 | |
| 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 and cache INPUTS/PASSES. | |
| - (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"]; | |
| [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"]; | |
| _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; | |
| 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"]) { | |
| glslType = @"sampler2DRect"; | |
| } | |
| NSMutableDictionary *stored = [NSMutableDictionary dictionary]; | |
| stored[@"NAME"] = name; | |
| stored[@"TYPE"] = type; | |
| id defVal = inputDict[@"DEFAULT"]; | |
| if (defVal) { | |
| stored[@"DEFAULT"] = defVal; | |
| } | |
| [parsedInputs addObject:stored]; | |
| if (glslType) { | |
| 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]; | |
| } | |
| } | |
| } | |
| if ([parsedInputs count] > 0) { | |
| _isfInputs = [parsedInputs copy]; | |
| } | |
| } | |
| // PASSES | |
| 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) { | |
| 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); | |
| } | |
| NSString *result = [NSString stringWithFormat:@"%@\n%@", preamble, raw]; | |
| return result; | |
| } | |
| // Decorate raw ISF vertex source with uniforms and helper macros. | |
| - (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"]; | |
| @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. | |
| [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; | |
| } | |
| // Should we ignore ISF DEFAULTs on this frame? | |
| BOOL ignoreDefaultsThisFrame = (self.ignoreDefaultsOnFirstFrame && self.isRenderingFirstFrame); | |
| @try { | |
| NSMutableString *mutableExpr = [expr mutableCopy]; | |
| // Replace $WIDTH/$HEIGHT with WIDTH/HEIGHT variables | |
| [mutableExpr replaceOccurrencesOfString:@"$WIDTH" | |
| withString:@"WIDTH" | |
| options:0 | |
| range:NSMakeRange(0, mutableExpr.length)]; | |
| [mutableExpr replaceOccurrencesOfString:@"$HEIGHT" | |
| withString:@"HEIGHT" | |
| options:0 | |
| range:NSMakeRange(0, mutableExpr.length)]; | |
| NSString *path = self.isfPath ?: @""; | |
| NSString *signature = self.useStaticInputPortNames ? nil : ISFPortSignatureForPath(path); | |
| NSMutableDictionary *vars = [@{ | |
| @"WIDTH" : @(outWidth), | |
| @"HEIGHT" : @(outHeight) | |
| } mutableCopy]; | |
| // ---------------- structure-input freeze snapshot logic ---------------- | |
| NSDictionary *paramsStructLive = nil; | |
| if (self.useStructureInput) { | |
| id raw = nil; | |
| @try { | |
| raw = [self valueForInputKey:kISFStructureInputPortKey]; | |
| } | |
| @catch (NSException *ex) { | |
| raw = nil; | |
| } | |
| if ([raw isKindOfClass:[NSDictionary class]]) { | |
| paramsStructLive = (NSDictionary *)raw; | |
| } | |
| } | |
| NSDictionary *paramsStruct = paramsStructLive; | |
| if (self.useStructureInput && | |
| self.freezeStructureInputThisFrame && | |
| self.lastParamsStructSnapshot) { | |
| paramsStruct = self.lastParamsStructSnapshot; | |
| } | |
| // ----------------------------------------------------------------------- | |
| NSUInteger boolSlotsUsed = 0; | |
| NSUInteger nonBoolStaticIndex = kISFStaticMaxBoolPorts; | |
| 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"]; | |
| NSString *portKey = nil; | |
| if (!self.useStructureInput) { | |
| if (self.useStaticInputPortNames) { | |
| if ([type isEqualToString:@"bool"]) { | |
| if (boolSlotsUsed < kISFStaticMaxBoolPorts) { | |
| portKey = ISFStaticPortKeyForIndex(boolSlotsUsed); | |
| } | |
| boolSlotsUsed++; | |
| } else { | |
| if (nonBoolStaticIndex < kISFStaticTotalStaticPorts) { | |
| portKey = ISFStaticPortKeyForIndex(nonBoolStaticIndex); | |
| nonBoolStaticIndex++; | |
| } else { | |
| // Out of static non-bool slots; this INPUT can't be driven by a QC port. | |
| portKey = nil; | |
| } | |
| } | |
| } else { | |
| portKey = ISFPortKeyForInputName(name, signature); | |
| } | |
| } | |
| NSString *marker = [@"$" stringByAppendingString:name]; | |
| [mutableExpr replaceOccurrencesOfString:marker | |
| withString:name | |
| options:0 | |
| range:NSMakeRange(0, mutableExpr.length)]; | |
| if (!numeric) { | |
| continue; | |
| } | |
| id portValue = nil; | |
| if (self.useStructureInput) { | |
| if (paramsStruct) { | |
| portValue = paramsStruct[name]; | |
| } | |
| } else if (portKey) { | |
| @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 (!ignoreDefaultsThisFrame && input[@"DEFAULT"]) { | |
| v = [input[@"DEFAULT"] boolValue] ? 1.0 : 0.0; | |
| } | |
| } else { | |
| id def = input[@"DEFAULT"]; | |
| if (portValue && [portValue respondsToSelector:@selector(doubleValue)]) { | |
| v = [portValue doubleValue]; | |
| } else if (!ignoreDefaultsThisFrame && | |
| def && [def respondsToSelector:@selector(doubleValue)]) { | |
| v = [def doubleValue]; | |
| } | |
| } | |
| vars[name] = @(v); | |
| } | |
| // Update snapshot after evaluation when not frozen | |
| if (self.useStructureInput && | |
| !self.freezeStructureInputThisFrame && | |
| paramsStructLive) { | |
| self.lastParamsStructSnapshot = paramsStructLive; | |
| } | |
| 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 = self.useStaticInputPortNames ? nil : ISFPortSignatureForPath(path); | |
| // Are we ignoring JSON DEFAULTs on this frame? | |
| BOOL ignoreDefaultsThisFrame = (self.ignoreDefaultsOnFirstFrame && self.isRenderingFirstFrame); | |
| GLint initialActiveTex = 0; | |
| glGetIntegerv(GL_ACTIVE_TEXTURE, &initialActiveTex); | |
| 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; | |
| // ------------------------------------------------------------------ | |
| // Structure-input mode: decide which structure we will actually use | |
| // for this frame (live vs snapshot). | |
| // ------------------------------------------------------------------ | |
| NSDictionary *paramsStructLive = nil; | |
| if (self.useStructureInput) { | |
| id raw = nil; | |
| @try { | |
| raw = [self valueForInputKey:kISFStructureInputPortKey]; | |
| } | |
| @catch (NSException *ex) { | |
| raw = nil; | |
| } | |
| if ([raw isKindOfClass:[NSDictionary class]]) { | |
| paramsStructLive = (NSDictionary *)raw; | |
| } | |
| } | |
| // By default we use the live structure; but if the ISF Path input port | |
| // changed on this frame, and we have a previous snapshot, we "freeze" | |
| // to the snapshot so the last frame of the old shader doesn't see the | |
| // first frame of the new params. | |
| NSDictionary *paramsStruct = paramsStructLive; | |
| if (self.useStructureInput && | |
| self.freezeStructureInputThisFrame && | |
| self.lastParamsStructSnapshot) { | |
| paramsStruct = self.lastParamsStructSnapshot; | |
| } | |
| NSUInteger boolSlotsUsed = 0; | |
| NSUInteger nonBoolStaticIndex = kISFStaticMaxBoolPorts; | |
| double (^numVal)(id,id) = ^double(id p, id d) { | |
| if ([p respondsToSelector:@selector(doubleValue)]) return [p doubleValue]; | |
| if (!ignoreDefaultsThisFrame && [d respondsToSelector:@selector(doubleValue)]) return [d doubleValue]; | |
| return 0.0; | |
| }; | |
| NSDictionary* (^structVal)(id) = ^NSDictionary* (id v) { | |
| return [v isKindOfClass:[NSDictionary class]] ? (NSDictionary *)v : nil; | |
| }; | |
| 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; | |
| } | |
| // Figure out which QC port (if any) backs this input (except images). | |
| NSString *portKey = nil; | |
| if (!self.useStructureInput) { | |
| if (self.useStaticInputPortNames) { | |
| if ([type isEqualToString:@"bool"]) { | |
| if (boolSlotsUsed < kISFStaticMaxBoolPorts) { | |
| portKey = ISFStaticPortKeyForIndex(boolSlotsUsed); // P1..P3 | |
| } | |
| boolSlotsUsed++; | |
| } else if (![type isEqualToString:@"image"]) { | |
| if (nonBoolStaticIndex < kISFStaticTotalStaticPorts) { | |
| portKey = ISFStaticPortKeyForIndex(nonBoolStaticIndex); // P4..P17 | |
| nonBoolStaticIndex++; | |
| } | |
| } | |
| } else { | |
| if (![type isEqualToString:@"image"]) { | |
| portKey = ISFPortKeyForInputName(name, signature); | |
| } | |
| } | |
| } | |
| id portValue = nil; | |
| if (self.useStructureInput) { | |
| if (paramsStruct) { | |
| portValue = paramsStruct[name]; | |
| } | |
| } else if (portKey) { | |
| @try { | |
| portValue = [self valueForInputKey:portKey]; | |
| } | |
| @catch (NSException *ex) { | |
| portValue = nil; | |
| } | |
| } | |
| if ([type isEqualToString:@"float"] || | |
| [type isEqualToString:@"event"]) { | |
| GLfloat v = (GLfloat)numVal(portValue, defaultVal); | |
| glUniform1f(loc, v); | |
| } | |
| else if ([type isEqualToString:@"bool"]) { | |
| BOOL b = NO; | |
| if (portValue) { | |
| b = [portValue boolValue]; | |
| } else if (defaultVal && !ignoreDefaultsThisFrame) { | |
| b = [defaultVal boolValue]; | |
| } | |
| glUniform1i(loc, b ? 1 : 0); | |
| } | |
| else if ([type isEqualToString:@"long"] || | |
| [type isEqualToString:@"int"]) { | |
| GLint v = 0; | |
| if (portValue) { | |
| v = (GLint)[portValue intValue]; | |
| } else if (defaultVal && !ignoreDefaultsThisFrame) { | |
| v = (GLint)[defaultVal intValue]; | |
| } | |
| glUniform1i(loc, v); | |
| } | |
| 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 (!ignoreDefaultsThisFrame && | |
| [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); | |
| } | |
| 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 (!ignoreDefaultsThisFrame && | |
| [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); | |
| } | |
| else if ([type isEqualToString:@"image"]) { | |
| // ALWAYS use the persistent Source Image port for all ISF image inputs. | |
| id img = self.inputImage; | |
| 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]; | |
| BOOL srcIsFlipped = NO; | |
| if ([img respondsToSelector:@selector(textureFlipped)]) { | |
| srcIsFlipped = [img textureFlipped]; | |
| } | |
| 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); | |
| glBegin(GL_QUADS); | |
| if (srcIsFlipped) { | |
| 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 { | |
| 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); | |
| GLint texUnit = currentTexUnit++; | |
| glActiveTexture(GL_TEXTURE0 + texUnit); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, self.flipTex); | |
| glUniform1i(loc, texUnit); | |
| 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 directly | |
| GLint texUnit = 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) { | |
| glUniform2f(sizeLoc, (GLfloat)srcW, (GLfloat)srcH); | |
| } | |
| } | |
| [img unlockTextureRepresentation]; | |
| } | |
| } | |
| } | |
| // audio/audioFFT etc could be handled similarly if needed | |
| } | |
| // After we've decided what structure to use for this frame, update the | |
| // snapshot ONLY on non-freeze frames and only when we have a live struct. | |
| if (self.useStructureInput && | |
| !self.freezeStructureInputThisFrame && | |
| paramsStructLive) { | |
| self.lastParamsStructSnapshot = paramsStructLive; | |
| } | |
| // PASS TARGET textures (unchanged) | |
| 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++; | |
| glActiveTexture(GL_TEXTURE0 + texUnit); | |
| glBindTexture(GL_TEXTURE_RECTANGLE_EXT, buf.readTex); | |
| glUniform1i(loc, texUnit); | |
| NSString *sizeName = [targetName stringByAppendingString:@"Size"]; | |
| GLint sizeLoc = glGetUniformLocation(_program, [sizeName UTF8String]); | |
| if (sizeLoc >= 0) { | |
| glUniform2f(sizeLoc, | |
| (GLfloat)buf.width, | |
| (GLfloat)buf.height); | |
| } | |
| } | |
| } | |
| 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; | |
| if (!path || [path length] == 0) | |
| return NO; | |
| 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 nothing changed and we already have a linked program, keep using it. | |
| if (!_needsReload && !pathChanged && !dateChanged && _program != 0) | |
| return YES; | |
| NSLog(@"[ISFRenderer] Loading / reloading ISF from '%@' (pathChanged=%d, dateChanged=%d, needsReload=%d)", | |
| path, (int)pathChanged, (int)dateChanged, (int)_needsReload); | |
| // Drop multi-pass buffers; they'll be recreated on demand for the new ISF. | |
| [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; | |
| } | |
| // decoratedFragmentSourceFromRaw: parses the JSON header and populates | |
| // _isfInputs and _isfPasses for this ISF as a side-effect. | |
| 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]); | |
| // Just for logging: how many image inputs are declared? | |
| 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: .vs if present, otherwise default | |
| NSString *vertexSourceString = nil; | |
| NSString *vsPath = [[path stringByDeletingPathExtension] stringByAppendingPathExtension:@"vs"]; | |
| 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) { | |
| NSString *defaultVSRaw = @"void main() { isf_vertShaderInit(); }\n"; | |
| vertexSourceString = [self decoratedVertexSourceFromRaw:defaultVSRaw]; | |
| } | |
| const GLchar *vertSrc = (const GLchar *)[vertexSourceString UTF8String]; | |
| CGLSetCurrentContext(_cglContext); | |
| // Throw away the old program + shaders and build a new one. | |
| 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; | |
| } | |
| 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); | |
| // *** IMPORTANT *** | |
| // Keep port layout and shader metadata in sync atomically. | |
| [self _rebuildDynamicInputPortsFromCurrentISFInputs]; | |
| 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; | |
| // Reset port-path tracking for this execution run. | |
| _lastInputISFPath = nil; | |
| self.portPathIsDriving = NO; | |
| // First real render after this will be considered "first frame" | |
| self.isRenderingFirstFrame = YES; | |
| // Reset structure-input snapshot state for this run. | |
| self.lastParamsStructSnapshot = nil; | |
| self.freezeStructureInputThisFrame = NO; | |
| NSLog(@"[ISFRenderer] startExecution, context=%p, isfPath='%@'", context, self.isfPath); | |
| // Important: we do not touch self.inputISFPath here. | |
| 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; | |
| // Track inputISFPath changes and optionally let it drive isfPath. | |
| BOOL portPathChangedThisFrame = NO; | |
| NSString *portPath = nil; | |
| @try { | |
| portPath = self.inputISFPath; | |
| } | |
| @catch (NSException *ex) { | |
| portPath = @""; | |
| } | |
| if (!portPath) { | |
| portPath = @""; | |
| } | |
| NSString *storedPath = self.isfPath ?: @""; | |
| if (!self.portPathIsDriving) { | |
| if (portPath.length > 0 && ![portPath isEqualToString:storedPath]) { | |
| // First time the ISF Path input port diverges from the settings path: | |
| // the port now "drives" the path. | |
| self.portPathIsDriving = YES; | |
| _lastInputISFPath = [portPath copy]; | |
| portPathChangedThisFrame = YES; | |
| __weak typeof(self) weakSelf = self; | |
| NSString *pathCopy = [portPath copy]; | |
| dispatch_async(dispatch_get_main_queue(), ^{ | |
| __strong typeof(self) strongSelf = weakSelf; | |
| if (!strongSelf) return; | |
| [strongSelf setIsfPath:pathCopy]; | |
| }); | |
| } else { | |
| _lastInputISFPath = [portPath copy]; | |
| } | |
| } else { | |
| if (!_lastInputISFPath || ![_lastInputISFPath isEqualToString:portPath]) { | |
| // Port was already driving, and changed this frame. | |
| _lastInputISFPath = [portPath copy]; | |
| portPathChangedThisFrame = YES; | |
| __weak typeof(self) weakSelf = self; | |
| NSString *pathCopy = [portPath copy]; | |
| dispatch_async(dispatch_get_main_queue(), ^{ | |
| __strong typeof(self) strongSelf = weakSelf; | |
| if (!strongSelf) return; | |
| [strongSelf setIsfPath:pathCopy]; | |
| }); | |
| } | |
| } | |
| // In structure-input mode, if the ISF Path input changed this frame, we want | |
| // to "freeze" the Params structure to the previous frame's snapshot so that | |
| // the last frame of the old shader doesn't see the new params. | |
| self.freezeStructureInputThisFrame = (self.useStructureInput && portPathChangedThisFrame); | |
| // Enable flag: 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); | |
| 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); | |
| if (_startTime == 0.0) | |
| _startTime = time; | |
| GLfloat deltaTime = 0.0f; | |
| if (self.lastTime > 0.0) { | |
| deltaTime = (GLfloat)(time - self.lastTime); | |
| } | |
| self.lastTime = time; | |
| if (![self loadISFInContext:context]) { | |
| self.outputImage = nil; | |
| ISFRestoreGLState(_cglContext, prevFBO, prevViewport, prevProgram, prevArrayBuffer, prevDepthTest, prevBlend); | |
| return YES; | |
| } | |
| 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); | |
| GLfloat tVal = (GLfloat)(time - _startTime); | |
| self.frameIndex += 1; | |
| 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]); | |
| 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) { | |
| 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); | |
| 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) { | |
| 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); | |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| } | |
| else { | |
| 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); | |
| GLint timeLoc = glGetUniformLocation(_program, "TIME"); | |
| if (timeLoc >= 0) | |
| glUniform1f(timeLoc, tVal); | |
| GLint deltaLoc = glGetUniformLocation(_program, "TIMEDELTA"); | |
| if (deltaLoc >= 0) | |
| glUniform1f(deltaLoc, deltaTime); | |
| GLint rsLoc = glGetUniformLocation(_program, "RENDERSIZE"); | |
| if (rsLoc >= 0) | |
| glUniform2f(rsLoc, (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); | |
| [self applyISFInputDefaultsWithContext:context]; | |
| 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 (targetBuf && targetBuf.isPersistent) { | |
| [self swapPersistentBuffer:targetBuf]; | |
| } | |
| if (isLastPass) { | |
| finalTexName = passTex; | |
| finalTexWidth = passW; | |
| finalTexHeight = passH; | |
| } | |
| } | |
| if (finalTexName == 0) { | |
| 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; | |
| ISFRestoreGLState(_cglContext, prevFBO, prevViewport, prevProgram, prevArrayBuffer, prevDepthTest, prevBlend); | |
| // We've now rendered at least one frame successfully | |
| self.isRenderingFirstFrame = NO; | |
| 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 | |
| // Quartz Composer ISF Renderer | |
| // | |
| #import <Quartz/Quartz.h> | |
| #import <AppKit/AppKit.h> | |
| @interface ISFRendererPlugInViewController : QCPlugInViewController | |
| @property(nonatomic, strong) NSTextField *pathField; | |
| @property(nonatomic, strong) NSButton *browseButton; | |
| @property(nonatomic, strong) NSButton *staticNamesCheckbox; | |
| @property(nonatomic, strong) NSButton *structureInputCheckbox; | |
| @property(nonatomic, strong) NSButton *ignoreDefaultsCheckbox; | |
| @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 | |
| // Quartz Composer ISF Renderer | |
| // | |
| #import "ISFRendererPlugInViewController.h" | |
| @implementation ISFRendererPlugInViewController | |
| @synthesize pathField = _pathField; | |
| @synthesize browseButton = _browseButton; | |
| @synthesize staticNamesCheckbox = _staticNamesCheckbox; | |
| @synthesize structureInputCheckbox = _structureInputCheckbox; | |
| @synthesize ignoreDefaultsCheckbox = _ignoreDefaultsCheckbox; | |
| - (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 | |
| // Bump height to make room for three checkboxes | |
| NSView *root = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 360, 144)]; | |
| 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:)]; | |
| // CHECKBOX 1: "Static input names" | |
| NSRect staticCheckboxFrame = NSMakeRect(padding, | |
| NSMinY(fieldFrame) - fieldH - 4.0, | |
| NSWidth(bounds) - 2 * padding, | |
| fieldH); | |
| NSButton *staticCheckbox = [[NSButton alloc] initWithFrame:staticCheckboxFrame]; | |
| [staticCheckbox setButtonType:NSSwitchButton]; | |
| staticCheckbox.title = @"Static input names (no per-shader key hash)"; | |
| staticCheckbox.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; | |
| [staticCheckbox setTarget:self]; | |
| [staticCheckbox setAction:@selector(_staticNamesCheckboxChanged:)]; | |
| // CHECKBOX 2: "Structure input" | |
| NSRect structCheckboxFrame = NSMakeRect(padding, | |
| NSMinY(staticCheckboxFrame) - fieldH - 4.0, | |
| NSWidth(bounds) - 2 * padding, | |
| fieldH); | |
| NSButton *structCheckbox = [[NSButton alloc] initWithFrame:structCheckboxFrame]; | |
| [structCheckbox setButtonType:NSSwitchButton]; | |
| structCheckbox.title = @"Use structure input (no fixed ports)"; | |
| structCheckbox.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; | |
| [structCheckbox setTarget:self]; | |
| [structCheckbox setAction:@selector(_structureInputCheckboxChanged:)]; | |
| // CHECKBOX 3: "Ignore ISF DEFAULTs on first frame" | |
| NSRect ignoreCheckboxFrame = NSMakeRect(padding, | |
| NSMinY(structCheckboxFrame) - fieldH - 4.0, | |
| NSWidth(bounds) - 2 * padding, | |
| fieldH); | |
| NSButton *ignoreCheckbox = [[NSButton alloc] initWithFrame:ignoreCheckboxFrame]; | |
| [ignoreCheckbox setButtonType:NSSwitchButton]; | |
| ignoreCheckbox.title = @"Ignore ISF DEFAULTs on first frame"; | |
| ignoreCheckbox.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; | |
| [ignoreCheckbox setTarget:self]; | |
| [ignoreCheckbox setAction:@selector(_ignoreDefaultsCheckboxChanged:)]; | |
| [root addSubview:label]; | |
| [root addSubview:field]; | |
| [root addSubview:button]; | |
| [root addSubview:staticCheckbox]; | |
| [root addSubview:structCheckbox]; | |
| [root addSubview:ignoreCheckbox]; | |
| self.pathField = field; | |
| self.browseButton = button; | |
| self.staticNamesCheckbox = staticCheckbox; | |
| self.structureInputCheckbox = structCheckbox; | |
| self.ignoreDefaultsCheckbox = ignoreCheckbox; | |
| self.view = root; | |
| } | |
| - (void)viewWillAppear | |
| { | |
| [super viewWillAppear]; | |
| id plugin = self.plugIn; | |
| if (!plugin) | |
| return; | |
| @try { | |
| // Initialize path from plug-in's isfPath | |
| 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 = @""; | |
| } | |
| BOOL staticOn = NO; | |
| BOOL structOn = NO; | |
| BOOL ignoreDefaultsOn = NO; | |
| @try { | |
| // Initialize static-names checkbox from plug-in | |
| id s = [plugin valueForKey:@"useStaticInputPortNames"]; | |
| staticOn = s ? [s boolValue] : NO; | |
| self.staticNamesCheckbox.state = staticOn ? NSControlStateValueOn : NSControlStateValueOff; | |
| } @catch (__unused id ex) { | |
| self.staticNamesCheckbox.state = NSControlStateValueOff; | |
| } | |
| @try { | |
| // Initialize structure-input checkbox from plug-in | |
| id sv = [plugin valueForKey:@"useStructureInput"]; | |
| structOn = sv ? [sv boolValue] : NO; | |
| self.structureInputCheckbox.state = structOn ? NSControlStateValueOn : NSControlStateValueOff; | |
| } @catch (__unused id ex) { | |
| self.structureInputCheckbox.state = NSControlStateValueOff; | |
| } | |
| @try { | |
| // Initialize ignoreDefaultsOnFirstFrame checkbox from plug-in | |
| id iv = [plugin valueForKey:@"ignoreDefaultsOnFirstFrame"]; | |
| ignoreDefaultsOn = iv ? [iv boolValue] : NO; | |
| self.ignoreDefaultsCheckbox.state = ignoreDefaultsOn ? NSControlStateValueOn : NSControlStateValueOff; | |
| } @catch (__unused id ex) { | |
| self.ignoreDefaultsCheckbox.state = NSControlStateValueOff; | |
| } | |
| // When structure input is ON, static naming is effectively overridden. | |
| // Optionally, reflect that in the UI by disabling the static checkbox. | |
| self.staticNamesCheckbox.enabled = !structOn; | |
| } | |
| #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) { | |
| } | |
| } | |
| } | |
| } | |
| - (void)_staticNamesCheckboxChanged:(id)sender | |
| { | |
| (void)sender; | |
| BOOL flag = (self.staticNamesCheckbox.state == NSControlStateValueOn); | |
| id plugin = self.plugIn; | |
| if (plugin) { | |
| @try { | |
| [plugin setValue:@(flag) forKey:@"useStaticInputPortNames"]; | |
| } @catch (__unused id ex) { | |
| } | |
| } | |
| } | |
| - (void)_structureInputCheckboxChanged:(id)sender | |
| { | |
| (void)sender; | |
| BOOL on = (self.structureInputCheckbox.state == NSControlStateValueOn); | |
| id plugin = self.plugIn; | |
| if (plugin) { | |
| @try { | |
| [plugin setValue:@(on) forKey:@"useStructureInput"]; | |
| } @catch (__unused id ex) { | |
| } | |
| } | |
| // Reflect override in UI: when structure mode is on, static mode is ignored. | |
| self.staticNamesCheckbox.enabled = !on; | |
| } | |
| - (void)_ignoreDefaultsCheckboxChanged:(id)sender | |
| { | |
| (void)sender; | |
| BOOL on = (self.ignoreDefaultsCheckbox.state == NSControlStateValueOn); | |
| id plugin = self.plugIn; | |
| if (plugin) { | |
| @try { | |
| [plugin setValue:@(on) forKey:@"ignoreDefaultsOnFirstFrame"]; | |
| } @catch (__unused id ex) { | |
| } | |
| } | |
| } | |
| @end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment