Skip to content

Instantly share code, notes, and snippets.

@g-l-i-t-c-h-o-r-s-e
Last active November 27, 2025 05:01
Show Gist options
  • Select an option

  • Save g-l-i-t-c-h-o-r-s-e/537a42a1cd298c885829583b55f64a55 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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.)."
//
// 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
//
// 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
//
// 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
//
// 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