Scaffold EVE fullscreen OpenGL app

This commit is contained in:
Meta Vens 2026-05-25 12:59:37 +01:00
commit c6c73d60c0
15 changed files with 494 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.theos/
packages/
obj/
*.deb
*.ipa
*.dSYM/

22
Makefile Normal file
View file

@ -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"

77
README.md Normal file
View file

@ -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.

42
Resources/Info.plist Normal file
View file

@ -0,0 +1,42 @@
<?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>en</string>
<key>CFBundleDisplayName</key>
<string>EveCanvas</string>
<key>CFBundleExecutable</key>
<string>EveCanvas</string>
<key>CFBundleIdentifier</key>
<string>org.gamecult.evecanvas</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>EveCanvas</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

7
Sources/EVEAppDelegate.h Normal file
View file

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
@interface EVEAppDelegate : UIResponder <UIApplicationDelegate>
@property(nonatomic, strong) UIWindow *window;
@end

18
Sources/EVEAppDelegate.m Normal file
View file

@ -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

8
Sources/EVEGLView.h Normal file
View file

@ -0,0 +1,8 @@
#import <UIKit/UIKit.h>
@interface EVEGLView : UIView
- (void)resizeDrawableIfNeeded;
- (void)renderAtTime:(NSTimeInterval)time;
@end

95
Sources/EVEGLView.mm Normal file
View file

@ -0,0 +1,95 @@
#import "EVEGLView.h"
#import <math.h>
#import <OpenGLES/ES3/gl.h>
#import <QuartzCore/CAEAGLLayer.h>
@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

View file

@ -0,0 +1,5 @@
#import <UIKit/UIKit.h>
@interface EVEViewController : UIViewController
@end

View file

@ -0,0 +1,148 @@
#import "EVEViewController.h"
#import <CoreMotion/CoreMotion.h>
#import <QuartzCore/QuartzCore.h>
#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<UITouch *> *)touches withEvent:(UIEvent *)event {
(void)touches;
self.touchCount = event.allTouches.count;
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
(void)touches;
self.touchCount = event.allTouches.count;
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
(void)touches;
self.touchCount = event.allTouches.count;
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
(void)touches;
self.touchCount = event.allTouches.count;
}
@end

9
Sources/main.m Normal file
View file

@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
#import "EVEAppDelegate.h"
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([EVEAppDelegate class]));
}
}

8
control Normal file
View file

@ -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 <meta@gamecult.org>
Author: GameCult
Section: Applications

11
entitlements.plist Normal file
View file

@ -0,0 +1,11 @@
<?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>platform-application</key>
<true/>
<key>com.apple.private.security.no-container</key>
<true/>
</dict>
</plist>

View file

@ -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

20
scripts/stage-to-eve.ps1 Normal file
View file

@ -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"