Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

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