diff --git a/Ports/iOSPort/nativeSources/CN1ES2compat.h b/Ports/iOSPort/nativeSources/CN1ES2compat.h index af662f79c5..fddfa91eb5 100644 --- a/Ports/iOSPort/nativeSources/CN1ES2compat.h +++ b/Ports/iOSPort/nativeSources/CN1ES2compat.h @@ -20,13 +20,56 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ +//#define CN1_USE_METAL + +#ifndef CN1_USE_METAL #define USE_ES2 1 +#endif + enum CN1GLenum { CN1_GL_ALPHA_TEXTURE, CN1_GL_VERTEX_COLORS }; -#ifdef USE_ES2 +#ifdef CN1_USE_METAL +// Metal compatibility layer +// These macros are no-ops for Metal since it uses a different rendering model +// Transform operations are handled by CN1METALTransform instead +#import "CN1METALTransform.h" + +#define _glMatrixMode(foo) ((void)0) +#define _glLoadIdentity() CN1_Metal_LoadIdentity() +#define _glOrthof(p1,p2,p3,p4,p5,p6) ((void)0) +#define _glDisable(foo) ((void)0) +#define _glEnable(foo) ((void)0) +#define _glScalef(xScale,yScale,zScale) CN1_Metal_Scale(xScale,yScale,zScale) +#define _glTranslatef(x,y,z) CN1_Metal_Translate(x,y,z) +#define _glColor4f(r,g,b,a) ((void)0) +#define _glEnableClientState(s) ((void)0) +#define _glDisableClientState(s) ((void)0) +#define _glTexCoordPointer(size,type,stride,pointer) ((void)0) +#define _glVertexPointer(size,type,stride,pointer) ((void)0) +#define _glDrawArrays(mode,first,count) ((void)0) +#define _glRotatef(angle,x,y,z) CN1_Metal_Rotate(angle,x,y,z) +#define _glEnableCN1State(state) ((void)0) +#define _glDisableCN1State(state) ((void)0) +#define _glAlphaMaskTexCoordPointer(size,type,stride,pointer) ((void)0) + +// Define GL constants as no-ops for Metal +#define GL_TEXTURE_2D 0 +#define GL_BLEND 0 +#define GL_ONE_MINUS_SRC_ALPHA 0 +#define GL_SRC_ALPHA 0 +#define GL_MODELVIEW 0 +#define GL_PROJECTION 0 +#define GL_VERTEX_ARRAY 0 +#define GL_TEXTURE_COORD_ARRAY 0 +#define GL_TRIANGLES 0 +#define GL_TRIANGLE_FAN 0 +#define GL_TRIANGLE_STRIP 0 +#define GL_NO_ERROR 0 + +#elif USE_ES2 #import #import #import "ExecutableOp.h" diff --git a/Ports/iOSPort/nativeSources/CN1METALTransform.h b/Ports/iOSPort/nativeSources/CN1METALTransform.h new file mode 100644 index 0000000000..a4f6d09605 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1METALTransform.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +#ifndef CN1METALTransform_h +#define CN1METALTransform_h + +#ifdef CN1_USE_METAL +#import +@import simd; + +// Global matrix state for Metal rendering +extern simd_float4x4 CN1_Metal_ProjectionMatrix; +extern simd_float4x4 CN1_Metal_ModelViewMatrix; +extern simd_float4x4 CN1_Metal_TransformMatrix; + +// Version tracking for matrix changes (optimization) +extern int CN1_Metal_ProjectionMatrixVersion; +extern int CN1_Metal_ModelViewMatrixVersion; +extern int CN1_Metal_TransformMatrixVersion; + +// Initialize matrices for 2D rendering +void CN1_Metal_InitMatrices(int framebufferWidth, int framebufferHeight); + +// Set matrices +void CN1_Metal_SetProjectionMatrix(simd_float4x4 matrix); +void CN1_Metal_SetModelViewMatrix(simd_float4x4 matrix); +void CN1_Metal_SetTransformMatrix(simd_float4x4 matrix); + +// Get combined MVP matrix (Projection * ModelView * Transform) +simd_float4x4 CN1_Metal_GetMVPMatrix(void); + +// Transform operations +void CN1_Metal_Translate(float x, float y, float z); +void CN1_Metal_Scale(float x, float y, float z); +void CN1_Metal_Rotate(float angle, float x, float y, float z); + +// Matrix stack operations (for nested transforms) +void CN1_Metal_PushMatrix(void); +void CN1_Metal_PopMatrix(void); +void CN1_Metal_LoadIdentity(void); + +// Helper functions +simd_float4x4 CN1_Metal_MakeOrtho(float left, float right, float bottom, float top, float near, float far); +simd_float4x4 CN1_Metal_MakeTranslation(float x, float y, float z); +simd_float4x4 CN1_Metal_MakeScale(float x, float y, float z); +simd_float4x4 CN1_Metal_MakeRotation(float angle, float x, float y, float z); + +#endif // CN1_USE_METAL +#endif // CN1METALTransform_h diff --git a/Ports/iOSPort/nativeSources/CN1METALTransform.m b/Ports/iOSPort/nativeSources/CN1METALTransform.m new file mode 100644 index 0000000000..fed1dd3b18 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1METALTransform.m @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +#ifdef CN1_USE_METAL +#import "CN1METALTransform.h" +@import simd; + +// Global matrix state +simd_float4x4 CN1_Metal_ProjectionMatrix; +simd_float4x4 CN1_Metal_ModelViewMatrix; +simd_float4x4 CN1_Metal_TransformMatrix; + +int CN1_Metal_ProjectionMatrixVersion = 0; +int CN1_Metal_ModelViewMatrixVersion = 0; +int CN1_Metal_TransformMatrixVersion = 0; + +// Matrix stack for nested transforms (max depth 32) +#define MAX_MATRIX_STACK_DEPTH 32 +static simd_float4x4 matrixStack[MAX_MATRIX_STACK_DEPTH]; +static int matrixStackTop = -1; + +void CN1_Metal_InitMatrices(int framebufferWidth, int framebufferHeight) { + // NSLog(@"CN1_Metal_InitMatrices called with width=%d, height=%d", framebufferWidth, framebufferHeight); + + // Create orthographic projection for 2D UI rendering + // Origin at top-left (0,0), Y increases downward (UIKit convention) + CN1_Metal_ProjectionMatrix = CN1_Metal_MakeOrtho( + 0, framebufferWidth, + framebufferHeight, 0, // Flipped Y for UIKit coordinates + -1, 1 + ); + CN1_Metal_ProjectionMatrixVersion++; + + // NSLog(@"Projection matrix after init: [%.6f %.6f %.6f %.6f] [%.6f %.6f %.6f %.6f] [%.6f %.6f %.6f %.6f] [%.6f %.6f %.6f %.6f]", + // CN1_Metal_ProjectionMatrix.columns[0][0], CN1_Metal_ProjectionMatrix.columns[0][1], CN1_Metal_ProjectionMatrix.columns[0][2], CN1_Metal_ProjectionMatrix.columns[0][3], + // CN1_Metal_ProjectionMatrix.columns[1][0], CN1_Metal_ProjectionMatrix.columns[1][1], CN1_Metal_ProjectionMatrix.columns[1][2], CN1_Metal_ProjectionMatrix.columns[1][3], + // CN1_Metal_ProjectionMatrix.columns[2][0], CN1_Metal_ProjectionMatrix.columns[2][1], CN1_Metal_ProjectionMatrix.columns[2][2], CN1_Metal_ProjectionMatrix.columns[2][3], + // CN1_Metal_ProjectionMatrix.columns[3][0], CN1_Metal_ProjectionMatrix.columns[3][1], CN1_Metal_ProjectionMatrix.columns[3][2], CN1_Metal_ProjectionMatrix.columns[3][3]); + + // Initialize model-view and transform to identity + CN1_Metal_ModelViewMatrix = matrix_identity_float4x4; + CN1_Metal_ModelViewMatrixVersion++; + + CN1_Metal_TransformMatrix = matrix_identity_float4x4; + CN1_Metal_TransformMatrixVersion++; + + // Reset matrix stack + matrixStackTop = -1; +} + +void CN1_Metal_SetProjectionMatrix(simd_float4x4 matrix) { + CN1_Metal_ProjectionMatrix = matrix; + CN1_Metal_ProjectionMatrixVersion++; +} + +void CN1_Metal_SetModelViewMatrix(simd_float4x4 matrix) { + CN1_Metal_ModelViewMatrix = matrix; + CN1_Metal_ModelViewMatrixVersion++; +} + +void CN1_Metal_SetTransformMatrix(simd_float4x4 matrix) { + CN1_Metal_TransformMatrix = matrix; + CN1_Metal_TransformMatrixVersion++; +} + +simd_float4x4 CN1_Metal_GetMVPMatrix(void) { + // Combine matrices: Projection * ModelView * Transform + return simd_mul(CN1_Metal_ProjectionMatrix, + simd_mul(CN1_Metal_ModelViewMatrix, + CN1_Metal_TransformMatrix)); +} + +void CN1_Metal_Translate(float x, float y, float z) { + simd_float4x4 translation = CN1_Metal_MakeTranslation(x, y, z); + CN1_Metal_TransformMatrix = simd_mul(CN1_Metal_TransformMatrix, translation); + CN1_Metal_TransformMatrixVersion++; +} + +void CN1_Metal_Scale(float x, float y, float z) { + simd_float4x4 scale = CN1_Metal_MakeScale(x, y, z); + CN1_Metal_TransformMatrix = simd_mul(CN1_Metal_TransformMatrix, scale); + CN1_Metal_TransformMatrixVersion++; +} + +void CN1_Metal_Rotate(float angle, float x, float y, float z) { + simd_float4x4 rotation = CN1_Metal_MakeRotation(angle, x, y, z); + CN1_Metal_TransformMatrix = simd_mul(CN1_Metal_TransformMatrix, rotation); + CN1_Metal_TransformMatrixVersion++; +} + +void CN1_Metal_PushMatrix(void) { + if (matrixStackTop < MAX_MATRIX_STACK_DEPTH - 1) { + matrixStackTop++; + matrixStack[matrixStackTop] = CN1_Metal_TransformMatrix; + } else { + NSLog(@"CN1METALTransform: Matrix stack overflow!"); + } +} + +void CN1_Metal_PopMatrix(void) { + if (matrixStackTop >= 0) { + CN1_Metal_TransformMatrix = matrixStack[matrixStackTop]; + matrixStackTop--; + CN1_Metal_TransformMatrixVersion++; + } else { + NSLog(@"CN1METALTransform: Matrix stack underflow!"); + } +} + +void CN1_Metal_LoadIdentity(void) { + CN1_Metal_TransformMatrix = matrix_identity_float4x4; + CN1_Metal_TransformMatrixVersion++; +} + +// Helper: Create orthographic projection matrix +simd_float4x4 CN1_Metal_MakeOrtho(float left, float right, float bottom, float top, float near, float far) { + simd_float4x4 m = matrix_identity_float4x4; + m.columns[0][0] = 2.0f / (right - left); + m.columns[1][1] = 2.0f / (top - bottom); + m.columns[2][2] = -2.0f / (far - near); + m.columns[3][0] = -(right + left) / (right - left); + m.columns[3][1] = -(top + bottom) / (top - bottom); + m.columns[3][2] = -(far + near) / (far - near); + m.columns[3][3] = 1.0f; + return m; +} + +// Helper: Create translation matrix +simd_float4x4 CN1_Metal_MakeTranslation(float x, float y, float z) { + simd_float4x4 m = matrix_identity_float4x4; + m.columns[3][0] = x; + m.columns[3][1] = y; + m.columns[3][2] = z; + return m; +} + +// Helper: Create scale matrix +simd_float4x4 CN1_Metal_MakeScale(float x, float y, float z) { + simd_float4x4 m = matrix_identity_float4x4; + m.columns[0][0] = x; + m.columns[1][1] = y; + m.columns[2][2] = z; + return m; +} + +// Helper: Create rotation matrix around arbitrary axis +simd_float4x4 CN1_Metal_MakeRotation(float angleRadians, float x, float y, float z) { + // Normalize axis + float length = sqrtf(x * x + y * y + z * z); + if (length == 0) { + return matrix_identity_float4x4; + } + x /= length; + y /= length; + z /= length; + + float c = cosf(angleRadians); + float s = sinf(angleRadians); + float t = 1.0f - c; + + simd_float4x4 m = matrix_identity_float4x4; + m.columns[0][0] = t * x * x + c; + m.columns[0][1] = t * x * y + s * z; + m.columns[0][2] = t * x * z - s * y; + + m.columns[1][0] = t * x * y - s * z; + m.columns[1][1] = t * y * y + c; + m.columns[1][2] = t * y * z + s * x; + + m.columns[2][0] = t * x * z + s * y; + m.columns[2][1] = t * y * z - s * x; + m.columns[2][2] = t * z * z + c; + + return m; +} + +#endif // CN1_USE_METAL diff --git a/Ports/iOSPort/nativeSources/ClipRect.m b/Ports/iOSPort/nativeSources/ClipRect.m index beb2c6cb12..6603fbe5b5 100644 --- a/Ports/iOSPort/nativeSources/ClipRect.m +++ b/Ports/iOSPort/nativeSources/ClipRect.m @@ -23,6 +23,9 @@ #import "ClipRect.h" #import "CodenameOne_GLViewController.h" #import "FillRect.h" +#ifdef CN1_USE_METAL +#import +#endif #ifdef USE_ES2 #import "DrawTextureAlphaMask.h" #import "FillPolygon.h" @@ -92,7 +95,94 @@ -(void)executeWithLog { } -(void)execute { -#ifdef USE_ES2 +#ifdef CN1_USE_METAL + // Metal implementation using scissor rectangle + // For polygon/texture clipping, we'd need stencil buffer - not implemented yet + if (texture != 0 || numPoints > 0) { + NSLog(@"ClipRect: Polygon/texture clipping not yet implemented in Metal"); + return; + } + + // In Metal, don't clamp clips to drawingRect - allow full screen clipping + // The Java layer sends proper background fills, and clamping causes issues + // with partial repaints and buffer synchronization + clipIsTexture = NO; + + if(width > 0 && height > 0) { + [super clipBlock:NO]; + int scale = scaleValue; + int displayHeight = [CodenameOne_GLViewController instance].view.bounds.size.height * scale; + + // Check if this is full screen - if so, disable scissor + if(width == [CodenameOne_GLViewController instance].view.bounds.size.width * scale && height == displayHeight) { + MTLScissorRect fullScreenRect = {0, 0, 0, 0}; + [[CodenameOne_GLViewController instance] setScissorRect:fullScreenRect enabled:NO]; + return; + } + + // Set scissor rectangle for Metal + clipX = x; + clipW = width; + if (clipX < 0) { + clipX = 0; + clipW = width; + } + + clipY = y; + clipH = height; + if (clipY < 0) { + clipY = 0; + clipH = height; + } + + [ClipRect updateClipToScale]; + + // Note: Input coordinates appear to already be in physical pixels (pre-scaled) + // Just clamp to drawable bounds for safety (Metal requires this) + CGSize drawableSize = [[[CodenameOne_GLViewController instance] metalView] drawableSize]; + int drawableW = (int)drawableSize.width; + int drawableH = (int)drawableSize.height; + + int finalX = clipX; + int finalY = clipY; + int finalW = clipW; + int finalH = clipH; + + if (finalX < 0) { + finalW += finalX; + finalX = 0; + } + if (finalY < 0) { + finalH += finalY; + finalY = 0; + } + if (finalX + finalW > drawableW) { + finalW = drawableW - finalX; + } + if (finalY + finalH > drawableH) { + finalH = drawableH - finalY; + } + if (finalW < 0) finalW = 0; + if (finalH < 0) finalH = 0; + if (finalX >= drawableW || finalY >= drawableH || finalW == 0 || finalH == 0) { + // Scissor is completely outside drawable - block drawing + [super clipBlock:YES]; + return; + } + + // Apply scissor to Metal encoder + MTLScissorRect scissor = {finalX, finalY, finalW, finalH}; + + // Match ES2 behavior: only apply scissor when no transform is active + if (currentScaleX == 1 && currentScaleY == 1) { + [[CodenameOne_GLViewController instance] setScissorRect:scissor enabled:YES]; + } + + clipApplied = YES; + } else { + [super clipBlock:YES]; + } +#elif defined(USE_ES2) if ( texture != 0 || numPoints > 0 ){ clipX = x; clipY=y; clipW=width; clipH=height; glClearStencil(0x0); @@ -131,8 +221,9 @@ -(void)execute { return; } - + #endif +#ifndef CN1_USE_METAL clipIsTexture = NO; int x2 = x + width; int y2 = y + height; @@ -210,11 +301,13 @@ -(void)execute { #endif clipApplied = NO; } +#endif // #ifndef CN1_USE_METAL } +(void)updateClipToScale { +#ifndef CN1_USE_METAL if ( clipIsTexture ){ return; } @@ -224,7 +317,7 @@ +(void)updateClipToScale { //CN1Log(@"Updating clip to scale"); glScissor(clipX, displayHeight - clipY - clipH, clipW, clipH); } - +#endif } #ifndef CN1_USE_ARC diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m index 0c15173d1f..1a8045e47c 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m @@ -339,7 +339,11 @@ - (void)applicationWillResignActive:(UIApplication *)application - (void)applicationDidEnterBackground:(UIApplication *)application { #ifdef CN1_BLOCK_SCREENSHOTS_ON_ENTER_BACKGROUND +#ifdef CN1_USE_METAL + [[CodenameOne_GLViewController instance] metalView].hidden = YES; +#else [[CodenameOne_GLViewController instance] eaglView].hidden = YES; +#endif cn1IsHiddenInBackground = YES; #endif if(editingComponent != nil) { @@ -365,7 +369,11 @@ - (void)applicationDidEnterBackground:(UIApplication *)application - (void)applicationWillEnterForeground:(UIApplication *)application { if (cn1IsHiddenInBackground) { +#ifdef CN1_USE_METAL + [[CodenameOne_GLViewController instance] metalView].hidden = NO; +#else [[CodenameOne_GLViewController instance] eaglView].hidden = NO; +#endif } /* diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h index cbd4a710c8..df185f39e9 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h @@ -23,12 +23,17 @@ #import "CN1ES2compat.h" #import +#ifdef CN1_USE_METAL +#import "METALView.h" +#else #import #import "EAGLView.h" #import #import #import #import +#endif + #import "ExecutableOp.h" #import "PaintOp.h" #import "GLUIImage.h" @@ -159,7 +164,11 @@ #define CN1_CAP_ROUND 1 #define CN1_CAP_SQUARE 2 +#ifdef CN1_USE_METAL +#define METALVIEW [[CodenameOne_GLViewController instance] metalView] +#else #define EAGLVIEW [[CodenameOne_GLViewController instance] eaglView] +#endif //ADD_INCLUDE @@ -191,8 +200,10 @@ MFMessageComposeViewControllerDelegate, CLLocationManagerDelegate, AVAudioRecord #endif > { @private +#ifndef CN1_USE_METAL EAGLContext *context; GLuint program; +#endif BOOL animating; NSInteger animationFrameInterval; @@ -227,7 +238,13 @@ MFMessageComposeViewControllerDelegate, CLLocationManagerDelegate, AVAudioRecord - (void)signIn:(GIDSignIn *)signIn didDisconnectWithUser:(GIDGoogleUser *)user withError:(NSError *)error; #endif +#ifdef CN1_USE_METAL +-(METALView*)metalView; +-(void)setScissorRect:(MTLScissorRect)rect enabled:(BOOL)enabled; +#else -(EAGLView*)eaglView; +#endif + -(void)startAnimation; -(void)stopAnimation; +(BOOL)isDrawTextureSupported; diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index e8b8be6a08..68812f590e 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -425,7 +425,11 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { utf.font = (BRIDGE_CAST UIFont*)font; } utf.text = [NSString stringWithUTF8String:str]; +#ifdef CN1_USE_METAL + utf.delegate = [[CodenameOne_GLViewController instance] metalView]; +#else utf.delegate = [[CodenameOne_GLViewController instance] eaglView]; +#endif [utf setBackgroundColor:[UIColor clearColor]]; #ifndef NEW_CODENAME_ONE_VM @@ -548,8 +552,12 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { utv.font = (BRIDGE_CAST UIFont*)font; } utv.text = [NSString stringWithUTF8String:str]; +#ifdef CN1_USE_METAL + utv.delegate = [[CodenameOne_GLViewController instance] metalView]; +#else utv.delegate = [[CodenameOne_GLViewController instance] eaglView]; - +#endif + // Apply constraints for multiline text view // INITIAL_CAPS_WORD if((constraint & 0x100000) == 0x100000) { @@ -1994,7 +2002,16 @@ -(void)updateCanvas:(BOOL)animated { // the display width/height each time to match the view, without performing other resizing // details, so it is possible that the size change event still needs to be sent // even if the display width already matches the value we're given here. +#ifdef CN1_USE_METAL + // Metal updates drawable size automatically via CAMetalLayer + int framebufferWidth = (int)([[self metalView] drawableSize].width); + int framebufferHeight = (int)([[self metalView] drawableSize].height); + if (framebufferWidth > 0 && framebufferHeight > 0) { + CN1_Metal_InitMatrices(framebufferWidth, framebufferHeight); + } +#else [[self eaglView] updateFrameBufferSize:(int)self.view.bounds.size.width h:(int)self.view.bounds.size.height]; +#endif displayWidth = currentWidth; displayHeight = (int)self.view.bounds.size.height * scaleValue; screenSizeChanged(displayWidth, displayHeight); @@ -2118,6 +2135,63 @@ -(UIImage*)createSplashImage { return img; } +#ifdef CN1_USE_METAL +METALView* lastFoundMetalView; +/** + * By default the view of the CodenameOne_GLViewController is a METALView object. But + * if there are peer components, and they are to be painted behind, then the view hierarchy + * is re-rooted with a parent. This method is a convenience method in cases where + * we need to obtain the METALView. + */ +-(METALView*) metalView { + if ([self.view class] == [METALView class]) { + lastFoundMetalView = (METALView*)self.view; + return (METALView*)self.view; + } + for (UIView* child in self.view.subviews) { + + if ([child class] == [METALView class]) { + lastFoundMetalView = (METALView*)child; + return (METALView*)child; + } + } + if (lastFoundMetalView != nil && lastFoundMetalView.peerComponentsLayer != nil) { + // This is an edge case that occurs if we add a peer component for the first time while + // the app is in transition. In this case, the new root would be added + // to the UITransitionView, and when the transition is complete, the + // AutoLayoutView has the original METAL view added to it, but our view controller + // would lose the reference to the metal view. + // We need to re-do the re-rooting of the METAL view and peer components layer in this case. + UIView* parent = [lastFoundMetalView superview]; + UIView* newRoot = [lastFoundMetalView.peerComponentsLayer superview]; + [lastFoundMetalView removeFromSuperview]; + [newRoot addSubview:lastFoundMetalView]; + [parent addSubview:newRoot]; + self.view = newRoot; + return lastFoundMetalView; + } + NSLog(@"METALView not found. This is not good!!"); + return nil; +} + +-(void)setScissorRect:(MTLScissorRect)rect enabled:(BOOL)enabled { + METALView* view = [self metalView]; + view.scissorRect = rect; + view.scissorEnabled = enabled; + + // If there's a current encoder, apply scissor immediately + if (view.currentEncoder != nil) { + if (enabled) { + [view.currentEncoder setScissorRect:rect]; + } else { + // Disable scissor by setting it to full drawable size + CGSize drawableSize = [view drawableSize]; + MTLScissorRect fullRect = {0, 0, (NSUInteger)drawableSize.width, (NSUInteger)drawableSize.height}; + [view.currentEncoder setScissorRect:fullRect]; + } + } +} +#else EAGLView* lastFoundEaglView; /** * By default the view of the CodenameOne_GLViewController is an EAGLView object. But @@ -2131,7 +2205,7 @@ -(EAGLView*) eaglView { return (EAGLView*)self.view; } for (UIView* child in self.view.subviews) { - + if ([child class] == [EAGLView class]) { lastFoundEaglView = (EAGLView*)child; return (EAGLView*)child; @@ -2140,7 +2214,7 @@ -(EAGLView*) eaglView { if (lastFoundEaglView != nil && lastFoundEaglView.peerComponentsLayer != nil) { // This is an edge case that occurs if we add a peer component for the first time while // the app is in transition. In this case, the new root would be added - // to the UITransitionView, and when the transition is complete, the + // to the UITransitionView, and when the transition is complete, the // AutoLayoutView has the original EAGL view added to it, but our view controller // would lose the reference to the eagl view. // We need to re-do the re-rooting of the EAGL view and peer components layer in this case. @@ -2155,9 +2229,30 @@ -(EAGLView*) eaglView { NSLog(@"EAGLView not found. This is not good!!"); return nil; } +#endif - (void)awakeFromNib { +#ifdef CN1_USE_METAL + // Metal initialization + retinaBug = isRetinaBug(); + if(retinaBug) { + scaleValue = 1; + } else { + scaleValue = [UIScreen mainScreen].scale; + } + sharedSingleton = self; + [self initVars]; + + // Initialize Metal matrices + METALView *metalView = [self metalView]; + CAMetalLayer *layer = (CAMetalLayer *)metalView.layer; + int framebufferWidth = (int)layer.drawableSize.width; + int framebufferHeight = (int)layer.drawableSize.height; + if (framebufferWidth > 0 && framebufferHeight > 0) { + CN1_Metal_InitMatrices(framebufferWidth, framebufferHeight); + } +#else #ifdef USE_ES2 if (!cn1CompareMatrices(GLKMatrix4Identity, CN1transformMatrix)) { CN1transformMatrix = GLKMatrix4Identity; @@ -2176,7 +2271,7 @@ - (void)awakeFromNib EAGLContext *aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; #else EAGLContext *aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1]; - + if (!aContext) { aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1]; } @@ -2185,14 +2280,15 @@ - (void)awakeFromNib CN1Log(@"Failed to create ES context"); else if (![EAGLContext setCurrentContext:aContext]) CN1Log(@"Failed to set ES context current"); - + self.context = aContext; #ifndef CN1_USE_ARC [aContext release]; #endif - + [[self eaglView] setContext:context]; [[self eaglView] setFramebuffer]; +#endif //self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; //self.view.autoresizesSubviews = YES; @@ -2203,10 +2299,15 @@ - (void)awakeFromNib animating = FALSE; animationFrameInterval = 1; self.displayLink = nil; - + +#ifndef CN1_USE_METAL const char* extensions = (const char*)glGetString(GL_EXTENSIONS); drawTextureSupported = extensions == 0 || strstr(extensions, "OES_draw_texture") != 0; //CN1Log(@"Draw texture extension %i", (int)drawTextureSupported); +#else + // Metal doesn't need texture extension checks + drawTextureSupported = YES; +#endif // register for keyboard notifications [[NSNotificationCenter defaultCenter] addObserver:self @@ -2251,7 +2352,9 @@ - (void)awakeFromNib gl = [[GLUIImage alloc] initWithImage:img]; dr = [[DrawImage alloc] initWithArgs:255 xpos:0 ypos:0 i:gl w:img.size.width h:img.size.height]; +#ifndef CN1_USE_METAL [[self eaglView] setFramebuffer]; +#endif } else { //add statusbar fix 20 pix only if not an iPad a iPad Launch images height is without statusbar @@ -2275,31 +2378,41 @@ - (void)awakeFromNib } CN1Log(@"Drew image on %i, %i for display %i, %i", imgHeight, imgWidth, wi, he); +#ifndef CN1_USE_METAL [[self eaglView] setFramebuffer]; +#endif } - +#ifndef CN1_USE_METAL GLErrorLog; - + _glScalef(xScale, -1, 1); GLErrorLog; _glTranslatef(0, -he, 0); GLErrorLog; - + [dr execute]; #ifndef CN1_USE_ARC [gl release]; [dr release]; #endif - + _glTranslatef(0, he, 0); GLErrorLog; - + _glScalef(xScale, -1, 1); GLErrorLog; - + [[self eaglView] presentFramebuffer]; GLErrorLog; +#else + // TODO: Implement Metal splash screen rendering + // For now, skip splash screen in Metal mode +#ifndef CN1_USE_ARC + [gl release]; + [dr release]; +#endif +#endif } #ifdef CN1_USE_STOREKIT [[SKPaymentQueue defaultQueue] addTransactionObserver:[CodenameOne_GLViewController instance]]; @@ -2467,18 +2580,20 @@ - (void)keyboardWillShow:(NSNotification *)n - (void)dealloc { +#ifndef CN1_USE_METAL if (program) { glDeleteProgram(program); program = 0; } - + // Tear down context. if ([EAGLContext currentContext] == context) [EAGLContext setCurrentContext:nil]; - + #ifndef CN1_USE_ARC [context release]; #endif +#endif #ifdef INCLUDE_MOPUB self.adView = nil; @@ -2520,16 +2635,18 @@ - (void)viewWillDisappear:(BOOL)animated - (void)viewDidUnload { [super viewDidUnload]; - + +#ifndef CN1_USE_METAL if (program) { glDeleteProgram(program); program = 0; } - + // Tear down context. if ([EAGLContext currentContext] == context) [EAGLContext setCurrentContext:nil]; self.context = nil; +#endif } - (NSInteger)animationFrameInterval @@ -2710,9 +2827,18 @@ -(void) viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coor } // simply create a property of 'BOOL' type +#ifdef CN1_USE_METAL + // Metal updates drawable size automatically + int framebufferWidth = (int)([[self metalView] drawableSize].width); + int framebufferHeight = (int)([[self metalView] drawableSize].height); + if (framebufferWidth > 0 && framebufferHeight > 0) { + CN1_Metal_InitMatrices(framebufferWidth, framebufferHeight); + } +#else [[self eaglView] updateFrameBufferSize:(int)size.width h:(int)size.height]; [[self eaglView] deleteFramebuffer]; - +#endif + displayWidth = (int)size.width * scaleValue; displayHeight = (int)size.height * scaleValue; @@ -2749,9 +2875,18 @@ - (void)viewSafeAreaInsetsDidChange { } -(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { - + +#ifdef CN1_USE_METAL + // Metal updates drawable size automatically + int framebufferWidth = (int)([[self metalView] drawableSize].width); + int framebufferHeight = (int)([[self metalView] drawableSize].height); + if (framebufferWidth > 0 && framebufferHeight > 0) { + CN1_Metal_InitMatrices(framebufferWidth, framebufferHeight); + } +#else [[self eaglView] updateFrameBufferSize:(int)self.view.bounds.size.width h:(int)self.view.bounds.size.height]; [[self eaglView] deleteFramebuffer]; +#endif displayWidth = (int)self.view.bounds.size.width * scaleValue; displayHeight = (int)self.view.bounds.size.height * scaleValue; @@ -2789,6 +2924,33 @@ - (void)drawFrame:(CGRect)rect if([UIApplication sharedApplication].applicationState != UIApplicationStateActive) { return; } +#ifdef CN1_USE_METAL + // Metal rendering path - use autorelease pool to ensure cleanup each frame + METALView *metalView = [self metalView]; + @autoreleasepool { + [metalView beginFrame]; + + if(currentTarget != nil) { + if([currentTarget count] > 0) { + [ClipRect setDrawRect:rect]; + + NSMutableArray* cp = nil; + @synchronized([CodenameOne_GLViewController instance]) { + cp = [currentTarget copy]; + [currentTarget removeAllObjects]; + } + + for(ExecutableOp* ex in cp) { + [ex executeWithClipping]; + //[ex executeWithLog]; + } + +#ifndef CN1_USE_ARC + [cp release]; +#endif + } // end @autoreleasepool for Metal +#else + // OpenGL ES rendering path [[self eaglView] setFramebuffer]; GLErrorLog; if(currentTarget != nil) { @@ -2799,14 +2961,14 @@ - (void)drawFrame:(CGRect)rect GLErrorLog; _glTranslatef(0, -displayHeight, 0); GLErrorLog; - + /*if(((int)rect.size.width) != displayWidth || ((int)rect.size.height) != displayHeight) { glScissor(rect.origin.x, displayHeight - rect.origin.y - rect.size.height, rect.size.width, rect.size.height); glEnable(GL_SCISSOR_TEST); glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT); }*/ - + //CN1Log(@"self.view.bounds.size.height %i displayHeight %i", (int)self.view.bounds.size.height, displayHeight); NSMutableArray* cp = nil; @synchronized([CodenameOne_GLViewController instance]) { @@ -2827,6 +2989,7 @@ - (void)drawFrame:(CGRect)rect GLErrorLog; _glScalef(1, -1, 1); GLErrorLog; +#endif [DrawGradientTextureCache flushDeleted]; [DrawStringTextureCache flushDeleted]; @@ -2886,10 +3049,16 @@ - (void)drawFrame:(CGRect)rect } } } +#ifdef CN1_USE_METAL + @autoreleasepool { + [metalView presentFramebuffer]; + } +#else GLErrorLog; - + [[self eaglView] presentFramebuffer]; GLErrorLog; +#endif } @@ -2986,6 +3155,7 @@ - (BOOL)validateProgram:(GLuint)prog return TRUE; } +#ifndef CN1_USE_METAL - (BOOL)loadShaders { GLuint vertShader, fragShader; @@ -3053,9 +3223,10 @@ - (BOOL)loadShaders glDeleteShader(vertShader); if (fragShader) glDeleteShader(fragShader); - + return TRUE; } +#endif // CN1_USE_METAL -(BOOL)isPaintFinished { diff --git a/Ports/iOSPort/nativeSources/CodenameOne_METALViewController.xib b/Ports/iOSPort/nativeSources/CodenameOne_METALViewController.xib new file mode 100644 index 0000000000..9c77bb07f7 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CodenameOne_METALViewController.xib @@ -0,0 +1,164 @@ + + + + 1024 + 10J567 + 1305 + 1038.35 + 462.00 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 300 + + + YES + IBProxyObject + IBUIView + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + + + YES + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + + 274 + {320, 460} + + + + + 3 + MQA + + 2 + + + NO + IBCocoaTouchFramework + + + + + YES + + + view + + + + 3 + + + + + YES + + 0 + + + + + + -1 + + + File's Owner + + + -2 + + + + + 2 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 2.CustomClassName + 2.IBEditorWindowLastContentRect + 2.IBPluginDependency + + + YES + CodenameOne_GLViewController + UIResponder + METALView + {{401, 662}, {320, 460}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + + + + + YES + + + + + 4 + + + + YES + + METALView + UIView + + IBProjectSource + ./Classes/METALView.h + + + + CodenameOne_GLViewController + UIViewController + + IBProjectSource + ./Classes/CodenameOne_GLViewController.h + + + + + 0 + IBCocoaTouchFramework + + com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS + + + + com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 + + + YES + 3 + 300 + + diff --git a/Ports/iOSPort/nativeSources/DrawImage.m b/Ports/iOSPort/nativeSources/DrawImage.m index 8927b3c743..a1eea6b132 100644 --- a/Ports/iOSPort/nativeSources/DrawImage.m +++ b/Ports/iOSPort/nativeSources/DrawImage.m @@ -2,6 +2,12 @@ #import "CodenameOne_GLViewController.h" #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import +#import +#import "CN1METALTransform.h" +#endif + #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; extern GLKMatrix4 CN1projectionMatrix; @@ -102,7 +108,143 @@ -(id)initWithArgs:(int)a xpos:(int)xpos ypos:(int)ypos i:(GLUIImage*)i w:(int)w #endif return self; } -#ifdef USE_ES2 + +#ifdef CN1_USE_METAL +-(void)execute { + // Metal rendering path + id encoder = [self makeRenderCommandEncoder]; + if (!encoder) { + return; + } + + // Get the Metal device and create pipeline state if needed + static id pipelineState = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + id device = [self device]; + id library = [device newDefaultLibrary]; + + MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"textured_vertex"]; + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"textured_fragment"]; + pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; + + // Configure vertex descriptor for textured shader + MTLVertexDescriptor *vertexDescriptor = [[MTLVertexDescriptor alloc] init]; + // Position attribute (float2) at attribute 0 + vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[0].offset = 0; + vertexDescriptor.attributes[0].bufferIndex = 0; + // TexCoord attribute (float2) at attribute 1 + vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[1].offset = sizeof(float) * 2; + vertexDescriptor.attributes[1].bufferIndex = 0; + // Layout for buffer 0 (interleaved position + texCoord) + vertexDescriptor.layouts[0].stride = sizeof(float) * 4; + vertexDescriptor.layouts[0].stepRate = 1; + vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; + pipelineDescriptor.vertexDescriptor = vertexDescriptor; + + // Enable blending for alpha + pipelineDescriptor.colorAttachments[0].blendingEnabled = YES; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + + NSError *error = nil; + pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; + if (error) { + NSLog(@"Error creating DrawImage pipeline state: %@", error); + } +#ifndef CN1_USE_ARC + [pipelineDescriptor release]; +#endif + }); + + [encoder setRenderPipelineState:pipelineState]; + + // Get Metal texture from GLUIImage + id texture = [img getMetalTexture]; + if (!texture) { + return; + } + + // Calculate texture coordinates + GLfloat actualImageWidth = [img getImage].size.width; + GLfloat actualImageHeight = [img getImage].size.height; + GLfloat actualImageWidthP2 = nextPowerOf2((int)actualImageWidth); + GLfloat actualImageHeightP2 = nextPowerOf2((int)actualImageHeight); + + // Get actual texture dimensions + int textW = [img getTextureWidth]; + int textH = [img getTextureHeight]; + + // Textures are flipped during creation to match OpenGL convention (V=0 at bottom, V=1 at top) + // Texture coordinates must match ES2 exactly + GLfloat nY = 1.0; // Top (V=1 in OpenGL) + GLfloat wX = 0; + GLfloat sY = 1.0 - actualImageHeight / actualImageHeightP2; // Bottom + GLfloat eX = actualImageWidth / actualImageWidthP2; + + // Create vertex data (position + texCoord interleaved) + typedef struct { + float position[2]; + float texCoord[2]; + } Vertex; + + Vertex vertices[] = { + {{(float)x, (float)y}, {wX, nY}}, // Top-left + {{(float)(x+width), (float)y}, {eX, nY}}, // Top-right + {{(float)x, (float)(y+height)}, {wX, sY}}, // Bottom-left + {{(float)(x+width), (float)(y+height)}, {eX, sY}} // Bottom-right + }; + + // Set vertex buffer + [encoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0]; + + // Set uniforms (MVP matrix + color) + typedef struct { + simd_float4x4 mvpMatrix; + simd_float4 color; + } Uniforms; + + Uniforms uniforms; + uniforms.mvpMatrix = [self getMVPMatrix]; + + // Preserve texture color, only apply alpha transparency + float alphaFloat = ((float)alpha) / 255.0f; + uniforms.color = simd_make_float4(1.0f, 1.0f, 1.0f, alphaFloat); + + [encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:1]; + + // Set texture + [encoder setFragmentTexture:texture atIndex:0]; + + // Create sampler state + static id samplerState = nil; + static dispatch_once_t samplerOnce; + dispatch_once(&samplerOnce, ^{ + MTLSamplerDescriptor *samplerDescriptor = [[MTLSamplerDescriptor alloc] init]; + samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear; + samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear; + samplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge; + samplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge; + samplerState = [[self device] newSamplerStateWithDescriptor:samplerDescriptor]; +#ifndef CN1_USE_ARC + [samplerDescriptor release]; +#endif + }); + + [encoder setFragmentSamplerState:samplerState atIndex:0]; + + // Draw rectangle as triangle strip + [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; +} + +#elif USE_ES2 -(void)execute { glUseProgram(getOGLProgram()); GLKVector4 color = GLKVector4Make(((float)alpha) / 255.0f, ((float)alpha) / 255.0f, ((float)alpha) / 255.0f, ((float)alpha) / 255.0f); diff --git a/Ports/iOSPort/nativeSources/DrawRect.m b/Ports/iOSPort/nativeSources/DrawRect.m index 91ef7cb3c1..2f4525db33 100644 --- a/Ports/iOSPort/nativeSources/DrawRect.m +++ b/Ports/iOSPort/nativeSources/DrawRect.m @@ -103,7 +103,92 @@ -(id)initWithArgs:(int)c a:(int)a xpos:(int)xpos ypos:(int)ypos w:(int)w h:(int) height = h; return self; } -#ifdef USE_ES2 + +#ifdef CN1_USE_METAL +-(void)execute { + // Metal rendering path - draws rectangle outline + id encoder = [self makeRenderCommandEncoder]; + if (!encoder) { + NSLog(@"DrawRect: No encoder available!"); + return; + } + + // Get or create pipeline state (same as FillRect - uses solid color shader) + static id pipelineState = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + id device = [self device]; + id library = [device newDefaultLibrary]; + + MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"solidColor_vertex"]; + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"solidColor_fragment"]; + pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; + + // Configure vertex descriptor + MTLVertexDescriptor *vertexDescriptor = [[MTLVertexDescriptor alloc] init]; + vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[0].offset = 0; + vertexDescriptor.attributes[0].bufferIndex = 0; + vertexDescriptor.layouts[0].stride = sizeof(float) * 2; + vertexDescriptor.layouts[0].stepRate = 1; + vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; + pipelineDescriptor.vertexDescriptor = vertexDescriptor; + + // Enable blending for alpha + pipelineDescriptor.colorAttachments[0].blendingEnabled = YES; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + + NSError *error = nil; + pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; + if (error) { + NSLog(@"Error creating DrawRect pipeline state: %@", error); + } +#ifndef CN1_USE_ARC + [pipelineDescriptor release]; +#endif + }); + + [encoder setRenderPipelineState:pipelineState]; + + // Create vertex data - 4 corners with 0.5 pixel offset for proper line alignment + float vertices[] = { + (float)x + 0.5f, (float)y + 0.5f, // Top-left + (float)(x + width) + 0.5f, (float)y + 0.5f, // Top-right + (float)(x + width) + 0.5f, (float)(y + height) + 0.5f, // Bottom-right + (float)x + 0.5f, (float)(y + height) + 0.5f // Bottom-left + }; + + [encoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0]; + + // Set uniforms (MVP matrix + color) + typedef struct { + simd_float4x4 mvpMatrix; + simd_float4 color; + } Uniforms; + + Uniforms uniforms; + uniforms.mvpMatrix = [self getMVPMatrix]; + uniforms.color = [self colorToFloat4:color alpha:alpha]; + + [encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:1]; + + // Draw rectangle outline as line strip (4 lines forming a closed loop) + // Metal doesn't have LINE_LOOP, so we need to draw lines manually + // Draw 4 line segments: 0->1, 1->2, 2->3, 3->0 + uint16_t indices[] = {0, 1, 1, 2, 2, 3, 3, 0}; + [encoder drawIndexedPrimitives:MTLPrimitiveTypeLine + indexCount:8 + indexType:MTLIndexTypeUInt16 + indexBuffer:[encoder.device newBufferWithBytes:indices length:sizeof(indices) options:MTLResourceStorageModeShared] + indexBufferOffset:0]; +} +#elif USE_ES2 -(void)execute { glUseProgram(getOGLProgram()); diff --git a/Ports/iOSPort/nativeSources/DrawString.m b/Ports/iOSPort/nativeSources/DrawString.m index b11675eb95..a232fdda9f 100644 --- a/Ports/iOSPort/nativeSources/DrawString.m +++ b/Ports/iOSPort/nativeSources/DrawString.m @@ -25,6 +25,13 @@ #import "DrawStringTextureCache.h" #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import +#import +#import "CN1METALTransform.h" +#import "DrawStringMetalTextureCache.h" +#endif + extern float scaleValue; #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; @@ -127,7 +134,187 @@ -(id)initWithArgs:(int)c a:(int)a xpos:(int)xpos ypos:(int)ypos s:(NSString*)s f return self; } -#ifdef USE_ES2 +#ifdef CN1_USE_METAL +-(void)execute { + // Metal rendering path + id encoder = [self makeRenderCommandEncoder]; + if (!encoder) { + NSLog(@"DrawString: No encoder available!"); + return; + } + + // Get pipeline state (same as DrawImage - uses textured shader) + static id pipelineState = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + id device = [self device]; + id library = [device newDefaultLibrary]; + + MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"textured_vertex"]; + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"textured_fragment"]; + pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; + + // Configure vertex descriptor for textured shader + MTLVertexDescriptor *vertexDescriptor = [[MTLVertexDescriptor alloc] init]; + // Position attribute (float2) at attribute 0 + vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[0].offset = 0; + vertexDescriptor.attributes[0].bufferIndex = 0; + // TexCoord attribute (float2) at attribute 1 + vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[1].offset = sizeof(float) * 2; + vertexDescriptor.attributes[1].bufferIndex = 0; + // Layout for buffer 0 (interleaved position + texCoord) + vertexDescriptor.layouts[0].stride = sizeof(float) * 4; + vertexDescriptor.layouts[0].stepRate = 1; + vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; + pipelineDescriptor.vertexDescriptor = vertexDescriptor; + + // Enable blending for alpha + pipelineDescriptor.colorAttachments[0].blendingEnabled = YES; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + + NSError *error = nil; + pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; + if (error) { + NSLog(@"Error creating DrawString pipeline state: %@", error); + } +#ifndef CN1_USE_ARC + [pipelineDescriptor release]; +#endif + }); + + if (!pipelineState) { + NSLog(@"DrawString: Pipeline state is nil!"); + return; + } + + [encoder setRenderPipelineState:pipelineState]; + + // Check cache first + DrawStringMetalTextureCache *cachedTex = [DrawStringMetalTextureCache checkCache:str f:font c:color a:255]; + id texture = nil; + int w = -1; + + if (cachedTex != nil) { + texture = [cachedTex texture]; + w = [cachedTex stringWidth]; + } else { + // Calculate text dimensions + w = (int)[str sizeWithAttributes:@{NSFontAttributeName: font}].width; + int h = (int)ceil([font lineHeight] + 1.0 * scaleValue); + int p2w = nextPowerOf2(w); + int p2h = nextPowerOf2(h); + + // Create text texture - same as ES2 version + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + void* imageData = malloc(p2h * p2w * 4); + CGContextRef context = CGBitmapContextCreate(imageData, p2w, p2h, 8, 4 * p2w, colorSpace, kCGImageAlphaPremultipliedLast); + // Note: ES2 version has flip commented out, so we don't flip either + CGColorSpaceRelease(colorSpace); + CGContextClearRect(context, CGRectMake(0, 0, p2w, p2h)); + + UIGraphicsPushContext(context); + UIColor *textColor = UIColorFromRGB(color, 255); + [str drawAtPoint:CGPointZero withAttributes:@{ + NSFontAttributeName: font, + NSForegroundColorAttributeName: textColor + }]; + UIGraphicsPopContext(); + + // Create Metal texture + MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm + width:p2w + height:p2h + mipmapped:NO]; + textureDescriptor.usage = MTLTextureUsageShaderRead; + + texture = [[self device] newTextureWithDescriptor:textureDescriptor]; + MTLRegion region = MTLRegionMake2D(0, 0, p2w, p2h); + [texture replaceRegion:region mipmapLevel:0 withBytes:imageData bytesPerRow:4 * p2w]; + + CGContextRelease(context); + free(imageData); + + // Cache the texture for future use + [DrawStringMetalTextureCache cache:str f:font t:texture c:color a:255]; + } + + // Calculate height for vertex positioning + int h = (int)ceil([font lineHeight] + 1.0 * scaleValue); + int p2w = nextPowerOf2(w); + int p2h = nextPowerOf2(h); + + // Create vertex data (position + texCoord interleaved) + typedef struct { + float position[2]; + float texCoord[2]; + } Vertex; + + // Match ES2 texture coordinates exactly + GLfloat nY = 1.0; + GLfloat wX = 0; + GLfloat sY = 1.0 - (GLfloat)h / (GLfloat)p2h; + GLfloat eX = (GLfloat)w / (GLfloat)p2w; + + Vertex vertices[] = { + {{(float)x, (float)y}, {wX, nY}}, // Top-left + {{(float)(x+w), (float)y}, {eX, nY}}, // Top-right + {{(float)x, (float)(y+h)}, {wX, sY}}, // Bottom-left + {{(float)(x+w), (float)(y+h)}, {eX, sY}} // Bottom-right + }; + + // Set vertex buffer + [encoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0]; + + // Set uniforms (MVP matrix + color) + typedef struct { + simd_float4x4 mvpMatrix; + simd_float4 color; + } Uniforms; + + Uniforms uniforms; + uniforms.mvpMatrix = [self getMVPMatrix]; + + // The text color is already in the texture, we just need alpha modulation + // Use white (1,1,1) to preserve texture color, with alpha for transparency + float alphaFloat = ((float)alpha) / 255.0f; + uniforms.color = simd_make_float4(1.0f, 1.0f, 1.0f, alphaFloat); + + [encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:1]; + + // Set texture + [encoder setFragmentTexture:texture atIndex:0]; + + // Create sampler state + static id samplerState = nil; + static dispatch_once_t samplerOnce; + dispatch_once(&samplerOnce, ^{ + MTLSamplerDescriptor *samplerDescriptor = [[MTLSamplerDescriptor alloc] init]; + samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear; + samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear; + samplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge; + samplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge; + samplerState = [[self device] newSamplerStateWithDescriptor:samplerDescriptor]; +#ifndef CN1_USE_ARC + [samplerDescriptor release]; +#endif + }); + + [encoder setFragmentSamplerState:samplerState atIndex:0]; + + // Draw rectangle as triangle strip + [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; + // NSLog(@"DrawString draw command issued for '%@'", str); +} + +#elif USE_ES2 -(void)execute { glUseProgram(getOGLProgram()); GLuint textureName = 0; diff --git a/Ports/iOSPort/nativeSources/DrawStringMetalTextureCache.h b/Ports/iOSPort/nativeSources/DrawStringMetalTextureCache.h new file mode 100644 index 0000000000..e9a7c28d31 --- /dev/null +++ b/Ports/iOSPort/nativeSources/DrawStringMetalTextureCache.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +#import +#import + +#ifdef CN1_USE_METAL +#import + +@interface DrawStringMetalTextureCache : NSObject { + NSDate *lastAccess; + id texture; + NSString *str; + UIFont *font; + int color; + int alpha; + int stringWidth; +} + +-(BOOL)isEqual:(id)object; +-(id)initWithString:(NSString*)s f:(UIFont*)f t:(id)t c:(int)c a:(int)a; ++(void)cache:(NSString*)s f:(UIFont*)f t:(id)t c:(int)c a:(int)a; ++(DrawStringMetalTextureCache*)checkCache:(NSString*)s f:(UIFont*)f c:(int)c a:(int)a; ++(void)flushDeleted; +-(int)stringWidth; +-(id)texture; + +@end + +#endif diff --git a/Ports/iOSPort/nativeSources/DrawStringMetalTextureCache.m b/Ports/iOSPort/nativeSources/DrawStringMetalTextureCache.m new file mode 100644 index 0000000000..fae31920a7 --- /dev/null +++ b/Ports/iOSPort/nativeSources/DrawStringMetalTextureCache.m @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +#import "DrawStringMetalTextureCache.h" + +#ifdef CN1_USE_METAL + +#import "ExecutableOp.h" +#include "xmlvm.h" + +@implementation DrawStringMetalTextureCache + +static int MAX_CACHE_SIZE = 100; + +-(id)initWithString:(NSString*)s f:(UIFont*)f t:(id)t c:(int)c a:(int)a { + stringWidth = -1; + str = s; + font = f; +#ifndef CN1_USE_ARC + [str retain]; + [font retain]; + lastAccess = [[NSDate date] retain]; + [t retain]; +#else + lastAccess = [NSDate date]; +#endif + texture = t; + color = c; + alpha = a; + if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + MAX_CACHE_SIZE = 200; + } + return self; +} + +static NSMutableArray* cachedStrings = nil; +static NSMutableArray* pendingDeleteStrings = nil; + +-(int)stringWidth { + if (stringWidth == -1) { + stringWidth = [str sizeWithAttributes:@{NSFontAttributeName: font}].width; + } + return stringWidth; +} + +-(id)texture { + return texture; +} + ++(void)cache:(NSString*)s f:(UIFont*)f t:(id)t c:(int)c a:(int)a { + DrawStringMetalTextureCache* d = [[DrawStringMetalTextureCache alloc] initWithString:s f:f t:t c:c a:a]; + if(cachedStrings == nil) { + cachedStrings = [[NSMutableArray alloc] init]; + [cachedStrings addObject:d]; + pendingDeleteStrings = [[NSMutableArray alloc] init]; +#ifndef CN1_USE_ARC + [d release]; +#endif + return; + } + if([cachedStrings count] < MAX_CACHE_SIZE) { + [cachedStrings addObject:d]; +#ifndef CN1_USE_ARC + [d release]; +#endif + } else { + // need to pick an element in the array to remove + DrawStringMetalTextureCache* oldest = d; + for(DrawStringMetalTextureCache* entry in cachedStrings) { + if([oldest->lastAccess compare:entry->lastAccess] == NSOrderedDescending) { + oldest = entry; + } + } + [pendingDeleteStrings addObject:oldest]; + [cachedStrings removeObject:oldest]; + [cachedStrings addObject:d]; +#ifndef CN1_USE_ARC + [d release]; +#endif + } +} + ++(void)flushDeleted { + [pendingDeleteStrings removeAllObjects]; +} + +-(BOOL)isEqual:(id)object { + if(object == self) { + return YES; + } + DrawStringMetalTextureCache* o = (DrawStringMetalTextureCache*)object; + return color == o->color && + alpha == o->alpha && [str isEqualToString:o->str] && + [font isEqual:o->font]; +} + ++(DrawStringMetalTextureCache*)checkCache:(NSString*)s f:(UIFont*)f c:(int)c a:(int)a { + DrawStringMetalTextureCache* tmp = [[DrawStringMetalTextureCache alloc] initWithString:s f:f t:nil c:c a:a]; + for(DrawStringMetalTextureCache* d in cachedStrings) { + if([tmp isEqual:d]) { +#ifndef CN1_USE_ARC + [d->lastAccess release]; +#endif + d->lastAccess = [NSDate date]; +#ifndef CN1_USE_ARC + [d->lastAccess retain]; + [tmp release]; +#endif + return d; + } + } +#ifndef CN1_USE_ARC + [tmp release]; +#endif + return nil; +} + +#ifndef CN1_USE_ARC +-(void)dealloc { + [str release]; + [font release]; + [lastAccess release]; + [texture release]; + [super dealloc]; +} +#endif + +@end + +#endif diff --git a/Ports/iOSPort/nativeSources/ExecutableOp.h b/Ports/iOSPort/nativeSources/ExecutableOp.h index 722083b8cd..fbb515f1e0 100644 --- a/Ports/iOSPort/nativeSources/ExecutableOp.h +++ b/Ports/iOSPort/nativeSources/ExecutableOp.h @@ -48,6 +48,11 @@ green:((float)((rgbValue >> 8) & 0xff))/255.0 blue:((float)(rgbValue & 0xff))/25 #endif +#ifdef CN1_USE_METAL +@import Metal; +@import simd; +#endif + @interface ExecutableOp : NSObject { } @@ -58,4 +63,14 @@ green:((float)((rgbValue >> 8) & 0xff))/255.0 blue:((float)(rgbValue & 0xff))/25 -(void)execute; -(void)executeWithLog; -(NSString*)getName; + +#ifdef CN1_USE_METAL +// Metal helper methods (accessible to all ExecutableOp subclasses) +-(id)device; +-(id)makeRenderCommandEncoder; +-(void)applyClip:(id)encoder; +-(simd_float4x4)getMVPMatrix; +-(simd_float4)colorToFloat4:(int)color alpha:(int)alpha; +#endif + @end diff --git a/Ports/iOSPort/nativeSources/ExecutableOp.m b/Ports/iOSPort/nativeSources/ExecutableOp.m index 64eebf3157..a5cc2dac41 100644 --- a/Ports/iOSPort/nativeSources/ExecutableOp.m +++ b/Ports/iOSPort/nativeSources/ExecutableOp.m @@ -27,6 +27,7 @@ #import +#ifndef CN1_USE_METAL extern void logGlErrorAt(const char *f, int l) { GLenum err = glGetError(); if(err != GL_NO_ERROR) { @@ -46,6 +47,11 @@ extern void logGlErrorAt(const char *f, int l) { } } } +#else +extern void logGlErrorAt(const char *f, int l) { + // No-op for Metal builds - use Metal validation layers instead +} +#endif @implementation ExecutableOp static BOOL blockDrawing = NO; @@ -120,4 +126,42 @@ -(NSString*)getName { return nil; } +#ifdef CN1_USE_METAL +// Metal helper method implementations +#import "METALView.h" +#import "CN1METALTransform.h" + +-(id)device { + METALView *metalView = [[CodenameOne_GLViewController instance] metalView]; + return metalView.device; +} + +-(id)makeRenderCommandEncoder { + METALView *metalView = [[CodenameOne_GLViewController instance] metalView]; + return [metalView makeRenderCommandEncoder]; +} + +-(void)applyClip:(id)encoder { + // TODO: Implement clipping via scissor rectangle or stencil buffer + // For now, no clipping is applied + // This will be implemented when ClipRect ExecutableOp is done +} + +-(simd_float4x4)getMVPMatrix { + simd_float4x4 mvp = CN1_Metal_GetMVPMatrix(); + return mvp; +} + +-(simd_float4)colorToFloat4:(int)color alpha:(int)alpha { + float alph = ((float)alpha) / 255.0; + return simd_make_float4( + ((float)((color >> 16) & 0xff)) / 255.0 * alph, + ((float)((color >> 8) & 0xff)) / 255.0 * alph, + ((float)(color & 0xff)) / 255.0 * alph, + alph + ); +} + +#endif + @end diff --git a/Ports/iOSPort/nativeSources/FillPolygon.m b/Ports/iOSPort/nativeSources/FillPolygon.m index 6b1aa43502..e82ebb5f97 100644 --- a/Ports/iOSPort/nativeSources/FillPolygon.m +++ b/Ports/iOSPort/nativeSources/FillPolygon.m @@ -122,6 +122,11 @@ -(id)initWithArgs:(JAVA_FLOAT*)xCoords y:(JAVA_FLOAT*)yCoords num:(int)num color } -(void)execute { +#ifdef CN1_USE_METAL + // TODO: Implement Metal polygon rendering + // For now, just skip rendering to allow build to succeed + NSLog(@"FillPolygon: Metal implementation not yet available"); +#else glUseProgram(getOGLProgram()); float alph = ((float)alpha)/255.0; @@ -183,13 +188,14 @@ -(void)execute glDisableVertexAttribArray(vertexCoordAtt); GLErrorLog; - + //glUseProgram(CN1activeProgram); //GLErrorLog; - - + + // ---------- end - +#endif // CN1_USE_METAL + } diff --git a/Ports/iOSPort/nativeSources/FillRect.m b/Ports/iOSPort/nativeSources/FillRect.m index a6e18120a5..674147e16f 100644 --- a/Ports/iOSPort/nativeSources/FillRect.m +++ b/Ports/iOSPort/nativeSources/FillRect.m @@ -25,6 +25,12 @@ #include "xmlvm.h" #include "TargetConditionals.h" +#ifdef CN1_USE_METAL +#import +#import +#import "CN1METALTransform.h" +#endif + #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; extern GLKMatrix4 CN1projectionMatrix; @@ -105,7 +111,90 @@ -(id)initWithArgs:(int)c a:(int)a xpos:(int)xpos ypos:(int)ypos w:(int)w h:(int) height = h; return self; } -#ifdef USE_ES2 + +#ifdef CN1_USE_METAL +-(void)execute { + // Metal rendering path + id encoder = [self makeRenderCommandEncoder]; + if (!encoder) { + NSLog(@"FillRect: No encoder available!"); + return; + } + + // Get the Metal device and create pipeline state if needed + static id pipelineState = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + id device = [self device]; + id library = [device newDefaultLibrary]; + + MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"solidColor_vertex"]; + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"solidColor_fragment"]; + pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; + + // Configure vertex descriptor + MTLVertexDescriptor *vertexDescriptor = [[MTLVertexDescriptor alloc] init]; + // Position attribute (float2) + vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[0].offset = 0; + vertexDescriptor.attributes[0].bufferIndex = 0; + // Layout for buffer 0 + vertexDescriptor.layouts[0].stride = sizeof(float) * 2; + vertexDescriptor.layouts[0].stepRate = 1; + vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; + pipelineDescriptor.vertexDescriptor = vertexDescriptor; + + // Enable blending for alpha + pipelineDescriptor.colorAttachments[0].blendingEnabled = YES; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + + NSError *error = nil; + pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; + if (error) { + NSLog(@"Error creating FillRect pipeline state: %@", error); + } +#ifndef CN1_USE_ARC + [pipelineDescriptor release]; +#endif + }); + + [encoder setRenderPipelineState:pipelineState]; + + // Create vertex data (2 triangles forming a rectangle) + float vertices[] = { + (float)x, (float)y, // Top-left + (float)(x+width), (float)y, // Top-right + (float)x, (float)(y+height), // Bottom-left + (float)(x+width), (float)(y+height) // Bottom-right + }; + + // Set vertex buffer + [encoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0]; + + // Set uniforms (MVP matrix + color) + typedef struct { + simd_float4x4 mvpMatrix; + simd_float4 color; + } Uniforms; + + Uniforms uniforms; + uniforms.mvpMatrix = [self getMVPMatrix]; + uniforms.color = [self colorToFloat4:color alpha:alpha]; + + [encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:1]; + + // Draw rectangle as triangle strip + [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; + // NSLog(@"FillRect draw command issued"); +} + +#elif USE_ES2 -(void)execute { //[UIColorFromRGB(color, alpha) set]; //CGContextFillRect(context, CGRectMake(x, y, width, height)); diff --git a/Ports/iOSPort/nativeSources/GLUIImage.h b/Ports/iOSPort/nativeSources/GLUIImage.h index 81869c2f30..e028badd41 100644 --- a/Ports/iOSPort/nativeSources/GLUIImage.h +++ b/Ports/iOSPort/nativeSources/GLUIImage.h @@ -21,24 +21,38 @@ * need additional information or have any questions. */ #import -#import +#ifndef CN1_USE_METAL +#import #import #import #import #import +#else +#import +#endif + #import @interface GLUIImage : NSObject { UIImage* img; +#ifndef CN1_USE_METAL GLuint textureName; +#else + id metalTexture; +#endif NSString* name; int textureWidth; int textureHeight; } -(id)initWithImage:(UIImage*)i; -(UIImage*)getImage; +#ifndef CN1_USE_METAL -(GLuint)getTexture:(int)texWidth texHeight:(int)texHeight; +#else +-(id)getMetalTexture; +-(id)getMetalTextureWithDevice:(id)device; +#endif -(void)setName:(NSString*)s; -(void)setImage:(UIImage*)i; -(int)getTextureWidth; diff --git a/Ports/iOSPort/nativeSources/GLUIImage.m b/Ports/iOSPort/nativeSources/GLUIImage.m index cfce49800d..dac9d815f8 100644 --- a/Ports/iOSPort/nativeSources/GLUIImage.m +++ b/Ports/iOSPort/nativeSources/GLUIImage.m @@ -34,7 +34,11 @@ -(id)initWithImage:(UIImage*)i { #ifndef CN1_USE_ARC [img retain]; #endif +#ifndef CN1_USE_METAL textureName = 0; +#else + metalTexture = nil; +#endif textureWidth = -1; textureHeight = -1; return self; @@ -52,6 +56,55 @@ -(int)getTextureHeight { return textureHeight; } +#ifdef CN1_USE_METAL +-(id)getMetalTexture { + // Delegate to getMetalTextureWithDevice with the system default device + // This is deprecated - callers should use getMetalTextureWithDevice: instead + return [self getMetalTextureWithDevice:MTLCreateSystemDefaultDevice()]; +} + +-(id)getMetalTextureWithDevice:(id)device { + if (metalTexture == nil) { + + // Calculate texture dimensions (power of 2) + int w = (int)img.size.width; + int h = (int)img.size.height; + int p2w = nextPowerOf2(w); + int p2h = nextPowerOf2(h); + + textureWidth = w; + textureHeight = h; + + // Create image data + // Flip the context to match OpenGL's bottom-left origin convention + // This allows us to use the same texture coordinates as OpenGL + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + void* imageData = malloc(p2h * p2w * 4); + CGContextRef context = CGBitmapContextCreate(imageData, p2w, p2h, 8, 4 * p2w, colorSpace, kCGImageAlphaPremultipliedLast); + CGContextTranslateCTM(context, 0, p2h); + CGContextScaleCTM(context, 1, -1); + CGColorSpaceRelease(colorSpace); + CGContextClearRect(context, CGRectMake(0, 0, p2w, p2h)); + CGContextDrawImage(context, CGRectMake(0, p2h - h, w, h), img.CGImage); + + // Create Metal texture + MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm + width:p2w + height:p2h + mipmapped:NO]; + textureDescriptor.usage = MTLTextureUsageShaderRead; + + metalTexture = [device newTextureWithDescriptor:textureDescriptor]; + + MTLRegion region = MTLRegionMake2D(0, 0, p2w, p2h); + [metalTexture replaceRegion:region mipmapLevel:0 withBytes:imageData bytesPerRow:4 * p2w]; + + CGContextRelease(context); + free(imageData); + } + return metalTexture; +} +#else -(GLuint)getTexture:(int)texWidth texHeight:(int)texHeight { if(textureName == 0) { textureWidth = texWidth; @@ -121,6 +174,7 @@ -(GLuint)getTexture:(int)texWidth texHeight:(int)texHeight { } return textureName; } +#endif -(void)setImage:(UIImage*)i { if(img != nil) { @@ -132,6 +186,14 @@ -(void)setImage:(UIImage*)i { #ifndef CN1_USE_ARC [img retain]; #endif +#ifdef CN1_USE_METAL + if(metalTexture != nil) { +#ifndef CN1_USE_ARC + [metalTexture release]; +#endif + metalTexture = nil; + } +#else if(textureName != 0) { int tname = textureName; textureName = 0; @@ -147,6 +209,7 @@ -(void)setImage:(UIImage*)i { }); } } +#endif } -(void)setName:(NSString*)s { @@ -158,11 +221,19 @@ -(void)setName:(NSString*)s { -(void)dealloc { if(name != nil) { - //CN1Log(@"Deleting image name %@", name); + //CN1Log(@"Deleting image name %@", name); #ifndef CN1_USE_ARC [name release]; #endif } +#ifdef CN1_USE_METAL + if(metalTexture != nil) { +#ifndef CN1_USE_ARC + [metalTexture release]; +#endif + metalTexture = nil; + } +#else if(textureName != 0) { int tname = textureName; textureName = 0; @@ -174,10 +245,11 @@ -(void)dealloc { //int fm = [ExecutableOp get_free_memory]; glDeleteTextures(1, &tname); GLErrorLog; - //CN1Log(@"Texture deletion freed up: %i", [ExecutableOp get_free_memory] - fm); + //CN1Log(@"Texture deletion freed up: %i", [ExecutableOp get_free_memory] - fm); }); } } +#endif #ifndef CN1_USE_ARC [img release]; [super dealloc]; diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index d92bb9cf18..a521bed227 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -5309,12 +5309,20 @@ static BOOL cn1_renderViewIntoContext(UIView *renderView, UIView *rootView, CGCo static void cn1_renderPeerComponents(UIView *rootView, CGContextRef ctx) { CodenameOne_GLViewController *controller = [CodenameOne_GLViewController instance]; - EAGLView *glView = [controller eaglView]; +#ifdef CN1_USE_METAL + UIView *glView = [controller metalView]; +#else + UIView *glView = [controller eaglView]; +#endif if (glView == nil || rootView == nil || ctx == NULL) { return; } - UIView *peerLayer = glView.peerComponentsLayer; +#ifdef CN1_USE_METAL + UIView *peerLayer = nil; // TODO: Add peerComponentsLayer support to METALView +#else + UIView *peerLayer = ((EAGLView*)glView).peerComponentsLayer; +#endif NSArray *peerCandidates = nil; if (peerLayer != nil) { [peerLayer layoutIfNeeded]; @@ -5423,7 +5431,11 @@ static void cn1_renderPeerComponents(UIView *rootView, CGContextRef ctx) { cn1_renderViewIntoContext(view, rootView, ctx); CodenameOne_GLViewController *controller = [CodenameOne_GLViewController instance]; - EAGLView *glView = [controller eaglView]; +#ifdef CN1_USE_METAL + UIView *glView = [controller metalView]; +#else + UIView *glView = [controller eaglView]; +#endif if (glView != nil && glView != view) { cn1_renderViewIntoContext(glView, rootView, ctx); } @@ -8432,6 +8444,7 @@ JAVA_VOID com_codename1_impl_ios_Matrix_MatrixUtil_transformPoints___float_1ARRA JAVA_ARRAY_FLOAT* inData = (JAVA_ARRAY_FLOAT*) ((JAVA_ARRAY)in)->data; JAVA_ARRAY_FLOAT* outData = (JAVA_ARRAY_FLOAT*) ((JAVA_ARRAY)out)->data; #endif +#ifndef CN1_USE_METAL GLKMatrix4 mMat = GLKMatrix4MakeWithArray(mData); JAVA_INT len = numPoints * pointSize; for (JAVA_INT i=0; i device; +@property (nonatomic, strong) id commandQueue; +@property (nonatomic, strong) id commandBuffer; +@property (nonatomic, strong) MTLRenderPassDescriptor* renderPassDescriptor; +@property (nonatomic, strong) id drawable; +@property (nonatomic, strong) UIView* peerComponentsLayer; +@property (nonatomic, strong) id currentEncoder; +@property (nonatomic, readonly) CGSize drawableSize; +@property (nonatomic, assign) MTLScissorRect scissorRect; +@property (nonatomic, assign) BOOL scissorEnabled; +@property (nonatomic, strong) id persistentTexture; +@property (nonatomic, assign) BOOL persistentTextureNeedsClear; -(void)textViewDidChange:(UITextView *)textView; -(void)deleteFramebuffer; +-(void)beginFrame; - (void)setFramebuffer; - (BOOL)presentFramebuffer; -(void)updateFrameBufferSize:(int)w h:(int)h; @@ -57,5 +64,6 @@ -(void) keyboardNextClicked; -(void) addPeerComponent:(UIView*) view; -(void) removePeerComponent:(UIView*) view; +-(id)makeRenderCommandEncoder; @end #endif \ No newline at end of file diff --git a/Ports/iOSPort/nativeSources/METALView.m b/Ports/iOSPort/nativeSources/METALView.m index ea036c3eda..d2a1869a07 100644 --- a/Ports/iOSPort/nativeSources/METALView.m +++ b/Ports/iOSPort/nativeSources/METALView.m @@ -26,6 +26,7 @@ #import "METALView.h" #import "ExecutableOp.h" #import "CodenameOne_GLViewController.h" +#import "CN1METALTransform.h" #include "com_codename1_impl_ios_IOSImplementation.h" #include "xmlvm.h" #include "com_codename1_impl_ios_TextEditUtil.h" @@ -44,11 +45,19 @@ - (void)deleteFramebuffer; @implementation METALView +@synthesize device; @synthesize commandQueue; @synthesize commandBuffer; @synthesize renderPassDescriptor; -@synthesize renderCommandEncoder; +@synthesize drawable; @synthesize peerComponentsLayer; +@synthesize currentEncoder; +@synthesize persistentTexture; + +- (CGSize)drawableSize { + CAMetalLayer *layer = (CAMetalLayer *)self.layer; + return layer.drawableSize; +} // You must implement this method + (Class)layerClass @@ -116,11 +125,14 @@ - (id)initWithCoder:(NSCoder*)coder } } CAMetalLayer *metalLayer = (CAMetalLayer *)self.layer; - metalLayer.device = MTLCreateSystemDefaultDevice(); + self.device = MTLCreateSystemDefaultDevice(); + metalLayer.device = self.device; metalLayer.opaque = TRUE; metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; - metalLayer.framebufferOnly = YES; - self.commandQueue = [metalLayer.device makeCommandQueue]; + metalLayer.framebufferOnly = NO; // Allow blit operations on drawable + // Use triple buffering for better CPU/GPU pipelining + metalLayer.maximumDrawableCount = 3; + self.commandQueue = [self.device newCommandQueue]; } @@ -130,6 +142,8 @@ - (id)initWithCoder:(NSCoder*)coder - (void)dealloc { #ifndef CN1_USE_ARC + // Property setter handles release when set to nil + self.persistentTexture = nil; [super dealloc]; #endif } @@ -148,50 +162,152 @@ -(void)updateFrameBufferSize:(int)w h:(int)h { } --(void)createRenderPassDescriptor { - if (self.renderPassDescriptor != nil) { +-(void)ensurePersistentTexture:(CGSize)size { + // Validate size - can't create 0x0 textures + if (size.width <= 0 || size.height <= 0) { return; } + + // Create or resize persistent texture to match drawable size + if (self.persistentTexture == nil || + self.persistentTexture.width != (NSUInteger)size.width || + self.persistentTexture.height != (NSUInteger)size.height) { + + MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:(NSUInteger)size.width + height:(NSUInteger)size.height + mipmapped:NO]; + desc.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget; + desc.storageMode = MTLStorageModePrivate; + + // newTextureWithDescriptor returns a retained object (+1) + // Property setter will release old and retain new, so we need to release our +1 + id newTexture = [self.device newTextureWithDescriptor:desc]; + self.persistentTexture = newTexture; +#ifndef CN1_USE_ARC + [newTexture release]; // Balance the +1 from newTextureWithDescriptor +#endif + // Mark that this new texture needs to be cleared before first use + self.persistentTextureNeedsClear = YES; + } +} + +-(void)createRenderPassDescriptor { CAMetalLayer *layer = (CAMetalLayer*)self.layer; - self.renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + + // Get drawable first - its texture has the correct size even if layer.drawableSize is 0 self.drawable = [layer nextDrawable]; - MTLRenderPipelineColorAttachmentDescriptor* colorAttachment = self.renderPassDescriptor.colorAttachments[0]; - colorAttachment.texture = self.drawable.texture; - colorAttachment.loadAction = MTLLoadActionClear; - colorAttachment.isBlendingEnabled = YES; - colorAttachment.sourceRGBBlendFactor = MTLBlendFactorOne; - colorAttachment.destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; - colorAttachment.sourceAlphaBlendFactor = MTLBlendFactorOne; - colorAttachment.destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + if (self.drawable == nil) { + return; + } + + // Get size from drawable texture (more reliable than layer.drawableSize) + CGSize size = CGSizeMake(self.drawable.texture.width, self.drawable.texture.height); + + // Skip if size is invalid (during initialization) + if (size.width <= 0 || size.height <= 0) { + return; + } + + // Ensure persistent texture exists - this is our actual render target + [self ensurePersistentTexture:size]; + if (self.persistentTexture == nil) { + return; + } + + self.renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + MTLRenderPassColorAttachmentDescriptor* colorAttachment = self.renderPassDescriptor.colorAttachments[0]; + // Render to persistent texture (not drawable) - content is naturally preserved + colorAttachment.texture = self.persistentTexture; + + // Clear on first use, then load to preserve content + if (self.persistentTextureNeedsClear) { + colorAttachment.loadAction = MTLLoadActionClear; + colorAttachment.clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0); // White background + self.persistentTextureNeedsClear = NO; + } else { + colorAttachment.loadAction = MTLLoadActionLoad; + } + colorAttachment.storeAction = MTLStoreActionStore; + + // Note: Blending is configured on MTLRenderPipelineDescriptor, not here + // Each ExecutableOp configures blending on its pipeline state +} + +- (void)beginFrame +{ + [self setFramebuffer]; + + // Create render command encoder for this frame + // We render directly to persistentTexture, so content is naturally preserved + if (self.renderPassDescriptor != nil && self.commandBuffer != nil) { + self.currentEncoder = [self.commandBuffer renderCommandEncoderWithDescriptor:self.renderPassDescriptor]; + + // Reset scissor to full texture at frame start (matches ES2 behavior) + self.scissorEnabled = NO; + if (self.currentEncoder != nil) { + CGSize drawableSize = [self drawableSize]; + MTLScissorRect fullRect = {0, 0, (NSUInteger)drawableSize.width, (NSUInteger)drawableSize.height}; + [self.currentEncoder setScissorRect:fullRect]; + } + } } - (void)setFramebuffer { - CAMetalLayer *layer = (CAMetalLayer*)self.layer; - self.commandBuffer = [self.commandQueue makeCommandBuffer]; + + // Create command buffer for this frame + self.commandBuffer = [self.commandQueue commandBuffer]; + + // Get drawable and create render pass descriptor [self createRenderPassDescriptor]; - self.renderCommandEncoder = [self.commandBuffer makeRenderCommandEncoderWithDescriptor:self.renderPassDescriptor]; - [self.renderCommandEncoder setViewport: (MTLViewport){ 0.0, 0.0, layer.drawableSize.width, layer.drawableSize.height, 0.0, 1.0 }]; - - _glMatrixMode(GL_PROJECTION); - _glLoadIdentity(); - _glOrthof(0, framebufferWidth, 0, framebufferHeight, -1, 1); - _glMatrixMode(GL_MODELVIEW); - _glLoadIdentity(); + + // Update framebuffer size + framebufferWidth = (int)layer.drawableSize.width; + framebufferHeight = (int)layer.drawableSize.height; + + // Initialize matrices for Metal rendering (only if size is valid) + if (framebufferWidth > 0 && framebufferHeight > 0) { + CN1_Metal_InitMatrices(framebufferWidth, framebufferHeight); + } } - (BOOL)presentFramebuffer { - BOOL success = FALSE; - - if (self.renderCommandEncoder) { - [self.renderCommandEncoder ] - [self.commandBuffer present:self.drawable]; + // End render encoding before blitting + if (self.currentEncoder) { + [self.currentEncoder endEncoding]; + self.currentEncoder = nil; + } + + if (self.commandBuffer && self.drawable && self.persistentTexture) { + // Single blit: copy from persistent texture to drawable for display + CGSize size = [self drawableSize]; + id blitEncoder = [self.commandBuffer blitCommandEncoder]; + [blitEncoder copyFromTexture:self.persistentTexture + sourceSlice:0 + sourceLevel:0 + sourceOrigin:MTLOriginMake(0, 0, 0) + sourceSize:MTLSizeMake(size.width, size.height, 1) + toTexture:self.drawable.texture + destinationSlice:0 + destinationLevel:0 + destinationOrigin:MTLOriginMake(0, 0, 0)]; + [blitEncoder endEncoding]; + + [self.commandBuffer presentDrawable:self.drawable]; [self.commandBuffer commit]; + + // Clear for next frame (property setters handle release for retain/strong) + self.commandBuffer = nil; + self.renderPassDescriptor = nil; + self.drawable = nil; + + return YES; } - - return success; + + return NO; } /** @@ -322,6 +438,11 @@ -(void)layoutSubviews [[CodenameOne_GLViewController instance] drawFrame:rect]; }*/ +-(id)makeRenderCommandEncoder { + // Return the current encoder created in beginFrame + // This allows all ExecutableOps in a frame to use the same encoder + return self.currentEncoder; +} @end #endif \ No newline at end of file diff --git a/Ports/iOSPort/nativeSources/MainWindowMETAL.xib b/Ports/iOSPort/nativeSources/MainWindowMETAL.xib new file mode 100644 index 0000000000..561d143bda --- /dev/null +++ b/Ports/iOSPort/nativeSources/MainWindowMETAL.xib @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ports/iOSPort/nativeSources/Shaders.metal b/Ports/iOSPort/nativeSources/Shaders.metal new file mode 100644 index 0000000000..8c2dcc622c --- /dev/null +++ b/Ports/iOSPort/nativeSources/Shaders.metal @@ -0,0 +1,231 @@ +/* + * Shaders.metal + * Codename One - Metal Shading Language Shaders + * + * Created for Codename One iOS Metal Backend 2025 + * Copyright (c) 2012-2025, Codename One and/or its affiliates. All rights reserved. + */ + +#include +using namespace metal; + +// ============================================================================ +// SHARED STRUCTURES +// ============================================================================ + +// Uniforms passed to all shaders +struct Uniforms { + float4x4 mvpMatrix; // Model-View-Projection matrix + float4 color; // Modulation color (RGBA) +}; + +// ============================================================================ +// SOLID COLOR SHADERS (for FillRect, DrawRect, etc.) +// ============================================================================ + +struct SolidColorVertexIn { + float2 position [[attribute(0)]]; +}; + +struct SolidColorVertexOut { + float4 position [[position]]; + float4 color; +}; + +vertex SolidColorVertexOut solidColor_vertex( + SolidColorVertexIn in [[stage_in]], + constant Uniforms &uniforms [[buffer(1)]] +) { + SolidColorVertexOut out; + out.position = uniforms.mvpMatrix * float4(in.position, 0.0, 1.0); + out.color = uniforms.color; + return out; +} + +fragment float4 solidColor_fragment(SolidColorVertexOut in [[stage_in]]) { + return in.color; +} + +// ============================================================================ +// TEXTURED SHADERS (for DrawImage, DrawString, etc.) +// ============================================================================ + +struct TexturedVertexIn { + float2 position [[attribute(0)]]; + float2 texCoord [[attribute(1)]]; +}; + +struct TexturedVertexOut { + float4 position [[position]]; + float2 texCoord; + float4 color; +}; + +vertex TexturedVertexOut textured_vertex( + TexturedVertexIn in [[stage_in]], + constant Uniforms &uniforms [[buffer(1)]] +) { + TexturedVertexOut out; + out.position = uniforms.mvpMatrix * float4(in.position, 0.0, 1.0); + out.texCoord = in.texCoord; + out.color = uniforms.color; + return out; +} + +fragment float4 textured_fragment( + TexturedVertexOut in [[stage_in]], + texture2d tex [[texture(0)]], + sampler texSampler [[sampler(0)]] +) { + float4 texColor = tex.sample(texSampler, in.texCoord); + return texColor * in.color; // Modulate by color (for alpha, tint) +} + +// ============================================================================ +// LINEAR GRADIENT SHADERS +// ============================================================================ + +struct GradientUniforms { + float4x4 mvpMatrix; + float4 color1; // Start color + float4 color2; // End color + float2 startPoint; // Gradient start (normalized 0-1) + float2 endPoint; // Gradient end (normalized 0-1) +}; + +struct GradientVertexIn { + float2 position [[attribute(0)]]; +}; + +struct GradientVertexOut { + float4 position [[position]]; + float2 worldPosition; // For interpolation calculation +}; + +vertex GradientVertexOut gradient_vertex( + GradientVertexIn in [[stage_in]], + constant GradientUniforms &uniforms [[buffer(1)]] +) { + GradientVertexOut out; + out.position = uniforms.mvpMatrix * float4(in.position, 0.0, 1.0); + out.worldPosition = in.position; + return out; +} + +fragment float4 gradient_fragment( + GradientVertexOut in [[stage_in]], + constant GradientUniforms &uniforms [[buffer(1)]] +) { + // Calculate interpolation factor along gradient vector + float2 gradVec = uniforms.endPoint - uniforms.startPoint; + float2 fragVec = in.worldPosition - uniforms.startPoint; + float t = dot(fragVec, gradVec) / dot(gradVec, gradVec); + t = clamp(t, 0.0, 1.0); + + // Interpolate between colors + return mix(uniforms.color1, uniforms.color2, t); +} + +// ============================================================================ +// RADIAL GRADIENT SHADERS +// ============================================================================ + +struct RadialGradientUniforms { + float4x4 mvpMatrix; + float4 color1; // Center color + float4 color2; // Outer color + float2 centerPoint; // Center of gradient + float radius; // Radius of gradient +}; + +fragment float4 radialGradient_fragment( + GradientVertexOut in [[stage_in]], + constant RadialGradientUniforms &uniforms [[buffer(1)]] +) { + // Calculate distance from center + float2 diff = in.worldPosition - uniforms.centerPoint; + float distance = length(diff); + + // Normalize by radius + float t = clamp(distance / uniforms.radius, 0.0, 1.0); + + // Interpolate between colors + return mix(uniforms.color1, uniforms.color2, t); +} + +// ============================================================================ +// LINE SHADERS (for DrawLine) +// ============================================================================ + +// Uses same vertex/fragment as solidColor shaders +// Lines are drawn as thin rectangles or using Metal line primitives + +// ============================================================================ +// ALPHA MASK SHADERS (for DrawTextureAlphaMask) +// ============================================================================ + +struct AlphaMaskVertexIn { + float2 position [[attribute(0)]]; + float2 texCoord [[attribute(1)]]; + float2 maskCoord [[attribute(2)]]; +}; + +struct AlphaMaskVertexOut { + float4 position [[position]]; + float2 texCoord; + float2 maskCoord; + float4 color; +}; + +vertex AlphaMaskVertexOut alphaMask_vertex( + AlphaMaskVertexIn in [[stage_in]], + constant Uniforms &uniforms [[buffer(1)]] +) { + AlphaMaskVertexOut out; + out.position = uniforms.mvpMatrix * float4(in.position, 0.0, 1.0); + out.texCoord = in.texCoord; + out.maskCoord = in.maskCoord; + out.color = uniforms.color; + return out; +} + +fragment float4 alphaMask_fragment( + AlphaMaskVertexOut in [[stage_in]], + texture2d tex [[texture(0)]], + texture2d mask [[texture(1)]], + sampler texSampler [[sampler(0)]] +) { + float4 texColor = tex.sample(texSampler, in.texCoord); + float maskAlpha = mask.sample(texSampler, in.maskCoord).a; + + return float4(texColor.rgb, texColor.a * maskAlpha) * in.color; +} + +// ============================================================================ +// DEBUG SHADERS (for testing) +// ============================================================================ + +// Simple vertex color shader for debugging +struct DebugVertexIn { + float2 position [[attribute(0)]]; + float4 color [[attribute(1)]]; +}; + +struct DebugVertexOut { + float4 position [[position]]; + float4 color; +}; + +vertex DebugVertexOut debug_vertex( + DebugVertexIn in [[stage_in]], + constant Uniforms &uniforms [[buffer(1)]] +) { + DebugVertexOut out; + out.position = uniforms.mvpMatrix * float4(in.position, 0.0, 1.0); + out.color = in.color; + return out; +} + +fragment float4 debug_fragment(DebugVertexOut in [[stage_in]]) { + return in.color; +} diff --git a/Ports/iOSPort/nativeSources/TileImage.m b/Ports/iOSPort/nativeSources/TileImage.m index d34a64a099..40a84cb94a 100644 --- a/Ports/iOSPort/nativeSources/TileImage.m +++ b/Ports/iOSPort/nativeSources/TileImage.m @@ -140,7 +140,204 @@ -(id)initWithArgs:(int)a xpos:(int)xpos ypos:(int)ypos i:(GLUIImage*)i w:(int)w #endif return self; } -#ifdef USE_ES2 +#ifdef CN1_USE_METAL +-(void)execute { + if (width <= 0 || height <= 0) { + return; + } + + int imageWidth = (int)[[img getImage] size].width; + int imageHeight = (int)[[img getImage] size].height; + + if (imageWidth <= 0 || imageHeight <= 0) { + return; + } + + + // Get Metal encoder + id encoder = [self makeRenderCommandEncoder]; + if (!encoder) { + NSLog(@"TileImage: No encoder available!"); + return; + } + + // Get or create pipeline state + static id pipelineState = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + id device = [self device]; + id library = [device newDefaultLibrary]; + + MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"textured_vertex"]; + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"textured_fragment"]; + pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; + + // Configure vertex descriptor + MTLVertexDescriptor *vertexDescriptor = [[MTLVertexDescriptor alloc] init]; + // Position attribute (float2) + vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[0].offset = 0; + vertexDescriptor.attributes[0].bufferIndex = 0; + // Texture coordinate attribute (float2) + vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[1].offset = sizeof(float) * 2; + vertexDescriptor.attributes[1].bufferIndex = 0; + // Layout for buffer 0 + vertexDescriptor.layouts[0].stride = sizeof(float) * 4; // 2 for position + 2 for texcoord + vertexDescriptor.layouts[0].stepRate = 1; + vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; + pipelineDescriptor.vertexDescriptor = vertexDescriptor; + + // Enable blending for alpha + pipelineDescriptor.colorAttachments[0].blendingEnabled = YES; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + + NSError *error = nil; + pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; + if (error) { + NSLog(@"Error creating TileImage pipeline state: %@", error); + } +#ifndef CN1_USE_ARC + [pipelineDescriptor release]; +#endif + }); + + [encoder setRenderPipelineState:pipelineState]; + + // Get texture from GLUIImage + id metalTexture = [img getMetalTextureWithDevice:[self device]]; + if (!metalTexture) { + NSLog(@"TileImage: Failed to get Metal texture!"); + return; + } + [encoder setFragmentTexture:metalTexture atIndex:0]; + + // Create sampler state + static id samplerState = nil; + static dispatch_once_t samplerOnce; + dispatch_once(&samplerOnce, ^{ + MTLSamplerDescriptor *samplerDescriptor = [[MTLSamplerDescriptor alloc] init]; + samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear; + samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear; + samplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge; + samplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge; + samplerState = [[self device] newSamplerStateWithDescriptor:samplerDescriptor]; +#ifndef CN1_USE_ARC + [samplerDescriptor release]; +#endif + }); + [encoder setFragmentSamplerState:samplerState atIndex:0]; + + // Calculate tiling similar to ES2 version + int p2w = nextPowerOf2(imageWidth); + int p2h = nextPowerOf2(imageHeight); + + GLfloat wRatio = (GLfloat)imageWidth / (GLfloat)p2w; + GLfloat hRatio = (GLfloat)imageHeight / (GLfloat)p2h; + + // Inset texture coordinates by 0.5 pixels to avoid sampling at exact edge + GLfloat insetX = 0.5f / (GLfloat)p2w; + GLfloat insetY = 0.5f / (GLfloat)p2h; + + // Use OpenGL-style texture coordinates (textures are flipped during creation to match) + GLfloat t0Y = 1.0 - hRatio + insetY; + GLfloat t0X = 0 + insetX; + GLfloat t1Y = 1 - insetY; + GLfloat t1X = wRatio - insetX; + + int numTiles = ceil((float)width / (float)imageWidth) * ceil((float)height / (float)imageHeight); + + // Create vertex array with position and texture coordinates interleaved + // Each tile needs 6 vertices (2 triangles) with 4 floats each (2 pos + 2 texcoord) + GLfloat* vertices = malloc(24 * numTiles * sizeof(GLfloat)); // 6 vertices * 4 floats per vertex + + int vertexOffset = 0; + for (int xPos = 0; xPos < width; xPos += imageWidth) { + for (int yPos = 0; yPos < height; yPos += imageHeight) { + // Match ES2 implementation - exact tile boundaries with no overlap + GLfloat vx0 = (GLfloat)(x + xPos); + GLfloat vy0 = (GLfloat)(y + yPos); + GLfloat vx1 = vx0 + (GLfloat)imageWidth; + GLfloat vy1 = vy0 + (GLfloat)imageHeight; + + GLfloat tx0 = t0X; + GLfloat ty0 = t0Y; + GLfloat tx1 = t1X; + GLfloat ty1 = t1Y; + + // Adjust for edges that exceed the target area + if (xPos + imageWidth > width) { + vx1 = (GLfloat)(x + width); + tx1 = (GLfloat)(width - xPos) / (GLfloat)p2w; + } + if (yPos + imageHeight > height) { + // For the last tile, use exact boundary + vy1 = (GLfloat)(y + height); + ty0 = 1.0 - (GLfloat)(height - yPos) / (GLfloat)p2h; + } + + // First triangle: top-left, top-right, bottom-left + // Using OpenGL-style texture coordinates (V=1 at top, V=0 at bottom) + vertices[vertexOffset++] = vx0; vertices[vertexOffset++] = vy0; + vertices[vertexOffset++] = tx0; vertices[vertexOffset++] = ty1; + + vertices[vertexOffset++] = vx1; vertices[vertexOffset++] = vy0; + vertices[vertexOffset++] = tx1; vertices[vertexOffset++] = ty1; + + vertices[vertexOffset++] = vx0; vertices[vertexOffset++] = vy1; + vertices[vertexOffset++] = tx0; vertices[vertexOffset++] = ty0; + + // Second triangle: bottom-left, top-right, bottom-right + vertices[vertexOffset++] = vx0; vertices[vertexOffset++] = vy1; + vertices[vertexOffset++] = tx0; vertices[vertexOffset++] = ty0; + + vertices[vertexOffset++] = vx1; vertices[vertexOffset++] = vy0; + vertices[vertexOffset++] = tx1; vertices[vertexOffset++] = ty1; + + vertices[vertexOffset++] = vx1; vertices[vertexOffset++] = vy1; + vertices[vertexOffset++] = tx1; vertices[vertexOffset++] = ty0; + } + } + + // Set vertex buffer + // Metal has a 4KB limit for setVertexBytes, so use a buffer for larger data + size_t vertexDataSize = vertexOffset * sizeof(GLfloat); + if (vertexDataSize <= 4096) { + [encoder setVertexBytes:vertices length:vertexDataSize atIndex:0]; + } else { + id vertexBuffer = [[self device] newBufferWithBytes:vertices + length:vertexDataSize + options:MTLResourceStorageModeShared]; + [encoder setVertexBuffer:vertexBuffer offset:0 atIndex:0]; + } + + // Set uniforms (MVP matrix + color modulation) + typedef struct { + simd_float4x4 mvpMatrix; + simd_float4 colorModulate; + } Uniforms; + + Uniforms uniforms; + uniforms.mvpMatrix = [self getMVPMatrix]; + float alphaNorm = ((float)alpha) / 255.0f; + // Preserve texture color, only apply alpha transparency + uniforms.colorModulate = simd_make_float4(1.0f, 1.0f, 1.0f, alphaNorm); + + [encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:1]; + [encoder setFragmentBytes:&uniforms.colorModulate length:sizeof(simd_float4) atIndex:0]; + + // Draw all tiles in one call + [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6 * numTiles]; + + free(vertices); +} +#elif USE_ES2 -(void)execute { if (width <= 0 || height <= 0) { return; diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index 973c78e39b..57037b67c9 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -660,8 +660,6 @@ public void usesClassMethod(String cls, String method) { if (useMetal) { try { - File CN1ES2compat = new File(buildinRes, "CN1ES2compat.h"); - replaceInFile(CN1ES2compat, "//#define CN1_USE_METAL", "#define CN1_USE_METAL"); copy(new File(buildinRes, "MainWindowMETAL.xib"), new File(buildinRes, "MainWindow.xib")); copy(new File(buildinRes, "CodenameOne_METALViewController.xib"), new File(buildinRes, "CodenameOne_GLViewController.xib")); } catch (Exception ex) { @@ -1624,6 +1622,7 @@ public void usesClassMethod(String cls, String method) { if (useMetal) { File pbx = new File(tmpFile, "dist/" + request.getMainClass() + ".xcodeproj/project.pbxproj"); replaceInFile(pbx, "CLANG_ENABLE_MODULES = NO;", "CLANG_ENABLE_MODULES = YES;"); + replaceInFile(pbx, "GCC_PREPROCESSOR_DEFINITIONS = (", "GCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\"CN1_USE_METAL=1\","); } } catch (Exception ex) { throw new BuildException("Failed to update infoplist file", ex); @@ -1887,6 +1886,7 @@ public void usesClassMethod(String cls, String method) { if (useMetal) { buildSettings += " config.build_settings['CLANG_ENABLE_MODULES'] = \"YES\"\n"; + buildSettings += " config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = \"$(inherited) CN1_USE_METAL=1\"\n"; } diff --git a/scripts/hellocodenameone/common/codenameone_settings.properties b/scripts/hellocodenameone/common/codenameone_settings.properties index b388059468..889ad2227e 100644 --- a/scripts/hellocodenameone/common/codenameone_settings.properties +++ b/scripts/hellocodenameone/common/codenameone_settings.properties @@ -28,3 +28,4 @@ codename1.secondaryTitle=Hello World codename1.vendor=CodenameOne codename1.version=1.0 codename1.cssTheme=true +codename1.arg.ios.metal=true