From c6c73d60c0a1edc34239734ef275ec15fbb01a25 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 25 May 2026 12:59:37 +0100 Subject: [PATCH] Scaffold EVE fullscreen OpenGL app --- .gitignore | 6 ++ Makefile | 22 +++++ README.md | 77 +++++++++++++++++ Resources/Info.plist | 42 +++++++++ Sources/EVEAppDelegate.h | 7 ++ Sources/EVEAppDelegate.m | 18 ++++ Sources/EVEGLView.h | 8 ++ Sources/EVEGLView.mm | 95 ++++++++++++++++++++ Sources/EVEViewController.h | 5 ++ Sources/EVEViewController.mm | 148 ++++++++++++++++++++++++++++++++ Sources/main.m | 9 ++ control | 8 ++ entitlements.plist | 11 +++ scripts/check-eve-toolchain.ps1 | 18 ++++ scripts/stage-to-eve.ps1 | 20 +++++ 15 files changed, 494 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 Resources/Info.plist create mode 100644 Sources/EVEAppDelegate.h create mode 100644 Sources/EVEAppDelegate.m create mode 100644 Sources/EVEGLView.h create mode 100644 Sources/EVEGLView.mm create mode 100644 Sources/EVEViewController.h create mode 100644 Sources/EVEViewController.mm create mode 100644 Sources/main.m create mode 100644 control create mode 100644 entitlements.plist create mode 100644 scripts/check-eve-toolchain.ps1 create mode 100644 scripts/stage-to-eve.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a41443 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.theos/ +packages/ +obj/ +*.deb +*.ipa +*.dSYM/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bf1ab4d --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +ARCHS = arm64 +TARGET = iphone:clang:latest:14.0 + +include $(THEOS)/makefiles/common.mk + +APPLICATION_NAME = EveCanvas + +EveCanvas_FILES = \ + Sources/main.m \ + Sources/EVEAppDelegate.m \ + Sources/EVEViewController.mm \ + Sources/EVEGLView.mm + +EveCanvas_FRAMEWORKS = UIKit QuartzCore OpenGLES CoreMotion +EveCanvas_CFLAGS = -fobjc-arc -Wall -Wextra +EveCanvas_CODESIGN_FLAGS = -Sentitlements.plist +EveCanvas_INSTALL_PATH = /Applications + +include $(THEOS_MAKE_PATH)/application.mk + +after-install:: + install.exec "uicache -p /Applications/EveCanvas.app || uicache" diff --git a/README.md b/README.md new file mode 100644 index 0000000..73e3d16 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# EveCanvas + +Fullscreen native render shell for EVE, the jailbroken iPad 8th generation test +device. + +This is deliberately not a web app. UIKit owns the app/window lifecycle and the +text overlay. OpenGL ES 3.0 owns the render surface. + +## Objective + +Put a real fullscreen native rendering surface on EVE without requiring macOS as +the development machine. + +## Current Mechanism + +- `EVEAppDelegate` creates one fullscreen `UIWindow`. +- `EVEViewController` installs: + - `EVEGLView`, a `CAEAGLLayer` backed OpenGL ES 3.0 render target. + - a native UIKit `UILabel` overlay for crisp Retina text. + - `CADisplayLink` for frame ticking. + - `CMMotionManager` for accelerometer and gyro telemetry. +- `EVEGLView` clears the full drawable with a slow color pulse and presents the + renderbuffer every frame. + +## Invariants + +- Browser/PWA layout is not part of this path. +- UIKit is the text owner; OpenGL ES is the pixel owner. +- The status bar stays hidden. +- Sensor reads are display-only telemetry until a later input model owns them. +- CultMesh networking is not smuggled into the render shell yet. + +## Build Shape + +The project is a Theos-style iOS application: + +```sh +make package +make install +``` + +EVE currently has SSH and jailbreak package management, but the native build +toolchain still needs to be installed. In particular, a Theos install and an +iPhoneOS SDK are required before `make package` can work. + +Expected EVE target: + +```text +ssh alias: eve +device: iPad11,6 / J171aAP +iPadOS: 14.4 +install path: /Applications/EveCanvas.app +``` + +## First Deployment Plan + +1. Install or stage Theos on EVE. +2. Stage a compatible iPhoneOS SDK under `$THEOS/sdks`. +3. Copy this project to EVE, for example `/var/mobile/Projects/Eve`. +4. Build on EVE: + +```sh +cd /var/mobile/Projects/Eve +export THEOS=/var/theos +make package +make install +uicache -p /Applications/EveCanvas.app +``` + +5. Launch `EveCanvas` from SpringBoard. + +## Next Cut + +- Confirm actual fullscreen drawable size on EVE. +- Add a simple triangle/quad shader so the renderer proves more than clear. +- Add touch/Pencil visual markers. +- Add a local telemetry bridge once the shell is stable. diff --git a/Resources/Info.plist b/Resources/Info.plist new file mode 100644 index 0000000..3a3f12e --- /dev/null +++ b/Resources/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + EveCanvas + CFBundleExecutable + EveCanvas + CFBundleIdentifier + org.gamecult.evecanvas + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + EveCanvas + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UIRequiresFullScreen + + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Sources/EVEAppDelegate.h b/Sources/EVEAppDelegate.h new file mode 100644 index 0000000..bedb492 --- /dev/null +++ b/Sources/EVEAppDelegate.h @@ -0,0 +1,7 @@ +#import + +@interface EVEAppDelegate : UIResponder + +@property(nonatomic, strong) UIWindow *window; + +@end diff --git a/Sources/EVEAppDelegate.m b/Sources/EVEAppDelegate.m new file mode 100644 index 0000000..818ed93 --- /dev/null +++ b/Sources/EVEAppDelegate.m @@ -0,0 +1,18 @@ +#import "EVEAppDelegate.h" + +#import "EVEViewController.h" + +@implementation EVEAppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + (void)application; + (void)launchOptions; + + self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + self.window.rootViewController = [[EVEViewController alloc] init]; + self.window.backgroundColor = UIColor.blackColor; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/Sources/EVEGLView.h b/Sources/EVEGLView.h new file mode 100644 index 0000000..2abea89 --- /dev/null +++ b/Sources/EVEGLView.h @@ -0,0 +1,8 @@ +#import + +@interface EVEGLView : UIView + +- (void)resizeDrawableIfNeeded; +- (void)renderAtTime:(NSTimeInterval)time; + +@end diff --git a/Sources/EVEGLView.mm b/Sources/EVEGLView.mm new file mode 100644 index 0000000..369f086 --- /dev/null +++ b/Sources/EVEGLView.mm @@ -0,0 +1,95 @@ +#import "EVEGLView.h" + +#import +#import +#import + +@interface EVEGLView () + +@property(nonatomic, strong) EAGLContext *context; +@property(nonatomic, assign) GLuint framebuffer; +@property(nonatomic, assign) GLuint colorRenderbuffer; +@property(nonatomic, assign) GLint drawableWidth; +@property(nonatomic, assign) GLint drawableHeight; + +@end + +@implementation EVEGLView + ++ (Class)layerClass { + return CAEAGLLayer.class; +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) { + return nil; + } + + self.contentScaleFactor = UIScreen.mainScreen.scale; + self.opaque = YES; + self.multipleTouchEnabled = YES; + + CAEAGLLayer *layer = (CAEAGLLayer *)self.layer; + layer.opaque = YES; + layer.drawableProperties = @{ + kEAGLDrawablePropertyRetainedBacking: @NO, + kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8, + }; + + self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + NSAssert(self.context != nil, @"EVE requires OpenGL ES 3.0."); + [EAGLContext setCurrentContext:self.context]; + + glGenFramebuffers(1, &_framebuffer); + glGenRenderbuffers(1, &_colorRenderbuffer); + glBindFramebuffer(GL_FRAMEBUFFER, self.framebuffer); + glBindRenderbuffer(GL_RENDERBUFFER, self.colorRenderbuffer); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.colorRenderbuffer); + + [self resizeDrawableIfNeeded]; + return self; +} + +- (void)dealloc { + [EAGLContext setCurrentContext:self.context]; + if (_colorRenderbuffer) { + glDeleteRenderbuffers(1, &_colorRenderbuffer); + } + if (_framebuffer) { + glDeleteFramebuffers(1, &_framebuffer); + } + [EAGLContext setCurrentContext:nil]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + [self resizeDrawableIfNeeded]; +} + +- (void)resizeDrawableIfNeeded { + [EAGLContext setCurrentContext:self.context]; + glBindRenderbuffer(GL_RENDERBUFFER, self.colorRenderbuffer); + [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer]; + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_drawableWidth); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_drawableHeight); + glViewport(0, 0, self.drawableWidth, self.drawableHeight); +} + +- (void)renderAtTime:(NSTimeInterval)time { + [EAGLContext setCurrentContext:self.context]; + glBindFramebuffer(GL_FRAMEBUFFER, self.framebuffer); + glViewport(0, 0, self.drawableWidth, self.drawableHeight); + + float pulse = (float)((sin(time * 0.8) + 1.0) * 0.5); + float green = 0.08f + pulse * 0.12f; + float blue = 0.13f + pulse * 0.18f; + + glClearColor(0.02f, green, blue, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + glBindRenderbuffer(GL_RENDERBUFFER, self.colorRenderbuffer); + [self.context presentRenderbuffer:GL_RENDERBUFFER]; +} + +@end diff --git a/Sources/EVEViewController.h b/Sources/EVEViewController.h new file mode 100644 index 0000000..396c10a --- /dev/null +++ b/Sources/EVEViewController.h @@ -0,0 +1,5 @@ +#import + +@interface EVEViewController : UIViewController + +@end diff --git a/Sources/EVEViewController.mm b/Sources/EVEViewController.mm new file mode 100644 index 0000000..d5a64f2 --- /dev/null +++ b/Sources/EVEViewController.mm @@ -0,0 +1,148 @@ +#import "EVEViewController.h" + +#import +#import + +#import "EVEGLView.h" + +@interface EVEViewController () + +@property(nonatomic, strong) EVEGLView *glView; +@property(nonatomic, strong) UILabel *overlayLabel; +@property(nonatomic, strong) CADisplayLink *displayLink; +@property(nonatomic, strong) CMMotionManager *motionManager; +@property(nonatomic, assign) NSTimeInterval previousTimestamp; +@property(nonatomic, assign) double filteredFPS; +@property(nonatomic, assign) NSUInteger touchCount; + +@end + +@implementation EVEViewController + +- (void)loadView { + UIView *root = [[UIView alloc] initWithFrame:UIScreen.mainScreen.bounds]; + root.backgroundColor = UIColor.blackColor; + root.multipleTouchEnabled = YES; + + self.glView = [[EVEGLView alloc] initWithFrame:root.bounds]; + self.glView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [root addSubview:self.glView]; + + self.overlayLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.overlayLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.overlayLabel.numberOfLines = 0; + self.overlayLabel.textColor = UIColor.whiteColor; + self.overlayLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.45]; + self.overlayLabel.font = [UIFont monospacedSystemFontOfSize:13.0 weight:UIFontWeightRegular]; + self.overlayLabel.layer.cornerRadius = 6.0; + self.overlayLabel.layer.masksToBounds = YES; + [root addSubview:self.overlayLabel]; + + UILayoutGuide *safe = root.safeAreaLayoutGuide; + [NSLayoutConstraint activateConstraints:@[ + [self.overlayLabel.leadingAnchor constraintEqualToAnchor:safe.leadingAnchor constant:12.0], + [self.overlayLabel.topAnchor constraintEqualToAnchor:safe.topAnchor constant:12.0], + [self.overlayLabel.widthAnchor constraintLessThanOrEqualToAnchor:safe.widthAnchor multiplier:0.92], + ]]; + + self.view = root; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.motionManager = [[CMMotionManager alloc] init]; + self.motionManager.accelerometerUpdateInterval = 1.0 / 30.0; + self.motionManager.gyroUpdateInterval = 1.0 / 30.0; + if (self.motionManager.accelerometerAvailable) { + [self.motionManager startAccelerometerUpdates]; + } + if (self.motionManager.gyroAvailable) { + [self.motionManager startGyroUpdates]; + } + + self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(frameTick:)]; + [self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; +} + +- (void)dealloc { + [self.displayLink invalidate]; + [self.motionManager stopAccelerometerUpdates]; + [self.motionManager stopGyroUpdates]; +} + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { + return UIRectEdgeAll; +} + +- (BOOL)prefersHomeIndicatorAutoHidden { + return YES; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + [self.glView resizeDrawableIfNeeded]; +} + +- (void)frameTick:(CADisplayLink *)link { + if (self.previousTimestamp > 0) { + NSTimeInterval delta = link.timestamp - self.previousTimestamp; + if (delta > 0) { + double instantFPS = 1.0 / delta; + self.filteredFPS = self.filteredFPS == 0 ? instantFPS : (self.filteredFPS * 0.9 + instantFPS * 0.1); + } + } + self.previousTimestamp = link.timestamp; + + [self.glView renderAtTime:link.timestamp]; + [self updateOverlay]; +} + +- (void)updateOverlay { + UIScreen *screen = UIScreen.mainScreen; + CGSize points = screen.bounds.size; + CGFloat scale = screen.scale; + CGSize pixels = CGSizeMake(points.width * scale, points.height * scale); + + CMAcceleration acceleration = self.motionManager.accelerometerData.acceleration; + CMRotationRate rotation = self.motionManager.gyroData.rotationRate; + + self.overlayLabel.text = [NSString stringWithFormat: + @"EVE Canvas\n" + "OpenGL ES 3.0 + UIKit text\n" + "points %.0fx%.0f pixels %.0fx%.0f @ %.1fx\n" + "fps %.1f touches %lu\n" + "accel %+0.2f %+0.2f %+0.2f\n" + "gyro %+0.2f %+0.2f %+0.2f", + points.width, points.height, pixels.width, pixels.height, scale, + self.filteredFPS, (unsigned long)self.touchCount, + acceleration.x, acceleration.y, acceleration.z, + rotation.x, rotation.y, rotation.z]; + [self.overlayLabel sizeToFit]; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + (void)touches; + self.touchCount = event.allTouches.count; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + (void)touches; + self.touchCount = event.allTouches.count; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + (void)touches; + self.touchCount = event.allTouches.count; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + (void)touches; + self.touchCount = event.allTouches.count; +} + +@end diff --git a/Sources/main.m b/Sources/main.m new file mode 100644 index 0000000..4e8c962 --- /dev/null +++ b/Sources/main.m @@ -0,0 +1,9 @@ +#import + +#import "EVEAppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([EVEAppDelegate class])); + } +} diff --git a/control b/control new file mode 100644 index 0000000..bb557e4 --- /dev/null +++ b/control @@ -0,0 +1,8 @@ +Package: org.gamecult.evecanvas +Name: EveCanvas +Version: 0.1.0 +Architecture: iphoneos-arm +Description: Fullscreen OpenGL ES 3.0 render shell for EVE. +Maintainer: GameCult +Author: GameCult +Section: Applications diff --git a/entitlements.plist b/entitlements.plist new file mode 100644 index 0000000..1b38132 --- /dev/null +++ b/entitlements.plist @@ -0,0 +1,11 @@ + + + + + platform-application + + com.apple.private.security.no-container + + + diff --git a/scripts/check-eve-toolchain.ps1 b/scripts/check-eve-toolchain.ps1 new file mode 100644 index 0000000..89235bd --- /dev/null +++ b/scripts/check-eve-toolchain.ps1 @@ -0,0 +1,18 @@ +param( + [string] $Target = "eve" +) + +$ErrorActionPreference = "Stop" + +$remoteScript = "echo '== identity =='; " + + "uname -a; " + + "sysctl -n hw.machine kern.osversion 2>/dev/null || true; " + + "echo '== command availability =='; " + + "for c in make clang ldid git uicache dpkg apt; do " + + "if command -v `$c >/dev/null 2>&1; then printf '%-8s %s\n' `$c `$(command -v `$c); else printf '%-8s missing\n' `$c; fi; " + + "done; " + + "echo '== theos =='; " + + "printf 'THEOS=%s\n' `"`${THEOS:-}`"; " + + "for d in /var/theos /opt/theos `"`$HOME/theos`"; do test -d `"`$d`" && find `"`$d`" -maxdepth 2 -type d | head -20; done" + +ssh $Target $remoteScript diff --git a/scripts/stage-to-eve.ps1 b/scripts/stage-to-eve.ps1 new file mode 100644 index 0000000..ed981f6 --- /dev/null +++ b/scripts/stage-to-eve.ps1 @@ -0,0 +1,20 @@ +param( + [string] $Target = "eve", + [string] $RemotePath = "/var/mobile/Projects/Eve" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$archive = Join-Path $env:TEMP "eve-canvas-source.tar" + +if (Test-Path $archive) { + Remove-Item -LiteralPath $archive -Force +} + +tar -cf $archive -C $projectRoot . +ssh $Target "mkdir -p '$RemotePath'" +scp $archive "${Target}:/tmp/eve-canvas-source.tar" +ssh $Target "tar -xf /tmp/eve-canvas-source.tar -C '$RemotePath' && rm /tmp/eve-canvas-source.tar && find '$RemotePath' -maxdepth 2 -type f | sort" + +Write-Host "Staged EveCanvas to ${Target}:$RemotePath"