Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/_build-ios-port.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
id: src_hash
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes \
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ios-packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
id: src_hash
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes \
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/scripts-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ name: Test Android build scripts
- '!CodenameOne/src/**/*.md'
- 'Ports/Android/**'
- '!Ports/Android/**/*.md'
- 'native-themes/android-material/**'
- '!native-themes/android-material/**/*.md'
- 'maven/**'
- '!maven/core-unittests/**'
- 'tests/**'
Expand Down Expand Up @@ -49,6 +51,8 @@ name: Test Android build scripts
- '!CodenameOne/src/**/*.md'
- 'Ports/Android/**'
- '!Ports/Android/**/*.md'
- 'native-themes/android-material/**'
- '!native-themes/android-material/**/*.md'
- 'maven/**'
- '!maven/core-unittests/**'
- 'tests/**'
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/scripts-ios-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ on:
- '!CodenameOne/src/**/*.md'
- 'Ports/iOSPort/**'
- '!Ports/iOSPort/**/*.md'
- 'native-themes/ios-modern/**'
- '!native-themes/ios-modern/**/*.md'
- 'vm/**'
- '!vm/**/*.md'
- 'tests/**'
Expand Down Expand Up @@ -47,6 +49,8 @@ on:
- '!CodenameOne/src/**/*.md'
- 'Ports/iOSPort/**'
- '!Ports/iOSPort/**/*.md'
- 'native-themes/ios-modern/**'
- '!native-themes/ios-modern/**/*.md'
- 'vm/**'
- '!vm/**/*.md'
- 'tests/**'
Expand Down Expand Up @@ -108,7 +112,7 @@ jobs:
id: src_hash
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes \
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/scripts-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ on:
- '!CodenameOne/src/**/*.md'
- 'Ports/iOSPort/**'
- '!Ports/iOSPort/**/*.md'
- 'native-themes/ios-modern/**'
- '!native-themes/ios-modern/**/*.md'
- 'vm/**'
- '!vm/**/*.md'
- 'tests/**'
Expand Down Expand Up @@ -53,6 +55,8 @@ on:
- '!CodenameOne/src/**/*.md'
- 'Ports/iOSPort/**'
- '!Ports/iOSPort/**/*.md'
- 'native-themes/ios-modern/**'
- '!native-themes/ios-modern/**/*.md'
- 'vm/**'
- '!vm/**/*.md'
- 'tests/**'
Expand Down Expand Up @@ -120,7 +124,7 @@ jobs:
id: src_hash
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes \
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down Expand Up @@ -269,7 +273,7 @@ jobs:
id: src_hash
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes \
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
95 changes: 95 additions & 0 deletions CodenameOne/src/com/codename1/ui/plaf/UIManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1802,6 +1802,8 @@ private void buildTheme(Hashtable themeProps) {
this.themeProps.put(key, themeProps.get(key));
}

applyThemeBindings();

updateLargerTextScaleSettingFromTheme();

if (!this.themeProps.containsKey("PickerButtonBar.derive")) {
Expand Down Expand Up @@ -1863,6 +1865,99 @@ private void buildTheme(Hashtable themeProps) {

}

/// Theme entries can be bound to a named theme constant via a
/// `@cn1-bind:<themeKey>=<varName>` pseudo-constant emitted by the CSS
/// compiler when it expands a `var(--name, fallback)` reference. The
/// compiler still inlines `fallback` as the baked-in default (so themes
/// load correctly with no override), but additionally records that the
/// resolved style property tracks `--name`.
///
/// At runtime, callers tune the palette by injecting an `@<varName>`
/// constant via [#addThemeProps]. This method walks the binding entries
/// and overlays the override value onto every bound style key, so a
/// single `addThemeProps({"@accent-color": "ff2d95"})` call retunes
/// every UIID whose CSS rule referenced `var(--accent-color, ...)`.
/// Bindings without a matching override are left at their baked-in
/// default (whatever was already in themeProps from the initial load).
private void applyThemeBindings() {
if (themeConstants == null || themeConstants.isEmpty() || themeProps == null) {
return;
}
final String prefix = "cn1-bind:";
for (Map.Entry<String, Object> entry : themeConstants.entrySet()) {
String constantKey = entry.getKey();
if (constantKey == null || !constantKey.startsWith(prefix)) {
continue;
}
Object varNameObj = entry.getValue();
if (!(varNameObj instanceof String)) {
continue;
}
String varName = ((String) varNameObj).trim();
if (varName.length() == 0) {
continue;
}
Object override = themeConstants.get(varName);
if (!(override instanceof String)) {
continue;
}
String themeKey = constantKey.substring(prefix.length());
if (themeKey.length() == 0) {
continue;
}
// Only retune keys that are already present in themeProps so a
// stale binding entry (left over after the bound rule was
// dropped from the source CSS) can't materialize a phantom
// style key from the user's override value.
if (!themeProps.containsKey(themeKey)) {
continue;
}
String overrideValue = (String) override;
if (themeKey.endsWith("Color")) {
overrideValue = normalizeBoundColorValue(overrideValue);
if (overrideValue == null) {
continue;
}
}
themeProps.put(themeKey, overrideValue);
}
}

/// `loadTheme` stores color theme entries as plain hex strings (no `#`,
/// lowercase). User-supplied overrides may use either form, so trim a
/// leading `#` and lowercase the value before assigning it to a bound
/// color key. Returns null when the value can't be parsed as a 3- or
/// 6-digit hex color so the binding falls through to its default.
private static String normalizeBoundColorValue(String raw) {
if (raw == null) {
return null;
}
String value = raw.trim();
if (value.length() == 0) {
return null;
}
if (value.charAt(0) == '#') {
value = value.substring(1);
}
if (value.length() == 3) {
char r = value.charAt(0);
char g = value.charAt(1);
char b = value.charAt(2);
value = "" + r + r + g + g + b + b;
}
if (value.length() != 6) {
return null;
}
for (int i = 0; i < 6; i++) {
char c = value.charAt(i);
boolean hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
if (!hex) {
return null;
}
}
return value.toLowerCase();
}

private Map<String, String> parseCache() {
if (parseCache == null) {
parseCache = new HashMap<String, String>();
Expand Down
83 changes: 71 additions & 12 deletions Ports/iOSPort/nativeSources/CN1Metalcompat.m
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,35 @@
static int currentFramebufferHeight = 0;
static CN1MetalPipelineCache *pipelineCache = nil;

// --------------- Per-encoder state cache ---------------
//
// Every drawQuad / drawSolidPrimitive used to call setRenderPipelineState
// and re-upload the matrix struct via setVertexBytes, even when the prior
// draw used the same pipeline and the matrices hadn't changed. For one-off
// fills that's fine, but UI code routinely emits a long burst of same-
// pipeline same-matrix solid-colour fills (gradients-as-scanlines, the
// hellocodenameone TextureBackdropPainter's diagonal stripes when a port
// lacks fillPolygon, RoundRectBorder's per-row interior fill, etc.). At
// burst counts of 100k+ draws, the redundant per-call setVertexBytes for
// a 192-byte matrix struct + the redundant pipeline state-set choke the
// CAMetalLayer command buffer to the point where a textured-backdrop
// dark-mode capture stalled the iOS Metal screenshot suite for ~18
// minutes (until the surrounding step's wall-clock timeout).
//
// Track the last-bound pipeline + matrix bytes per encoder; only forward
// to Metal when they actually changed. Invalidated on every `activeEncoder
// = ...` assignment because Metal command encoders don't carry state
// between encoders -- a fresh encoder needs the first call to actually
// bind the state, even if nominally it matches the previous encoder's.
static __unsafe_unretained id<MTLRenderPipelineState> lastBoundPipelineState = nil;
static CN1MetalMatrices lastBoundMatrices;
static BOOL lastBoundMatricesValid = NO;

static inline void invalidateEncoderStateCache(void) {
lastBoundPipelineState = nil;
lastBoundMatricesValid = NO;
}

#define CN1_MATRIX_STACK_DEPTH 32
static simd_float4x4 modelViewStack[CN1_MATRIX_STACK_DEPTH];
static int modelViewStackTop = 0;
Expand Down Expand Up @@ -78,6 +107,7 @@ void CN1MetalBeginFrame(id<MTLRenderCommandEncoder> encoder,
int framebufferWidth,
int framebufferHeight) {
activeEncoder = encoder;
invalidateEncoderStateCache();
currentProjection = projection;
currentFramebufferWidth = framebufferWidth;
currentFramebufferHeight = framebufferHeight;
Expand All @@ -92,6 +122,7 @@ void CN1MetalBeginFrame(id<MTLRenderCommandEncoder> encoder,

void CN1MetalEndFrame(void) {
activeEncoder = nil;
invalidateEncoderStateCache();
}

id<MTLRenderCommandEncoder> CN1MetalActiveEncoder(void) {
Expand Down Expand Up @@ -209,6 +240,36 @@ static CN1MetalMatrices currentMatrices(void) {
return m;
}

// Binds `state` on activeEncoder only when it differs from the last
// pipeline state we bound on this encoder. Saves a Metal API call per
// draw in the (very common) burst case where many consecutive draws
// reuse the same pipeline (e.g. solid-colour fillRect storms from
// gradient/scanline approximations).
static inline void bindPipelineStateIfChanged(id<MTLRenderPipelineState> state) {
if (state == lastBoundPipelineState) return;
[activeEncoder setRenderPipelineState:state];
lastBoundPipelineState = state;
}

// setVertexBytes for the matrix struct dominates the CPU cost of a
// burst of fills (it's a 192-byte copy into the encoder's argument
// scratch on every call). Skip the upload when the matrix snapshot is
// byte-identical to the last one we uploaded on this encoder. The
// matrix mutators (Set/LoadIdentity/Push/Pop/Scale/Translate/Rotate)
// don't touch this cache themselves -- they only mutate the global
// matrix state; the cache compares against the bytes we last wrote
// and naturally re-uploads on the next draw if they've drifted.
static inline void uploadMatricesIfChanged(NSUInteger atIndex) {
CN1MetalMatrices matrices = currentMatrices();
if (lastBoundMatricesValid &&
memcmp(&matrices, &lastBoundMatrices, sizeof(matrices)) == 0) {
return;
}
[activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:atIndex];
lastBoundMatrices = matrices;
lastBoundMatricesValid = YES;
}

static void drawQuad(CN1MetalPipeline pipeline,
const float vertices[8],
const float *texcoords, // may be NULL
Expand All @@ -217,13 +278,12 @@ static void drawQuad(CN1MetalPipeline pipeline,
if (activeEncoder == nil || pipelineCache == nil) return;
id<MTLRenderPipelineState> state = [pipelineCache pipelineFor:pipeline];
if (state == nil) return;
[activeEncoder setRenderPipelineState:state];
bindPipelineStateIfChanged(state);

// buffer(0): positions (8 floats = 4 x (x,y))
[activeEncoder setVertexBytes:vertices length:sizeof(float) * 8 atIndex:0];
// buffer(1): matrices
CN1MetalMatrices matrices = currentMatrices();
[activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:1];
uploadMatricesIfChanged(1);
// buffer(2): optional texcoords (only textured/alpha-mask pipelines read this)
if (texcoords != NULL) {
[activeEncoder setVertexBytes:texcoords length:sizeof(float) * 8 atIndex:2];
Expand Down Expand Up @@ -251,10 +311,9 @@ static void drawSolidPrimitive(MTLPrimitiveType primitive,
size_t byteCount = sizeof(float) * 2 * (size_t)vertexCount;
if (byteCount > 4096) return;

[activeEncoder setRenderPipelineState:state];
bindPipelineStateIfChanged(state);
[activeEncoder setVertexBytes:vertices length:byteCount atIndex:0];
CN1MetalMatrices matrices = currentMatrices();
[activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:1];
uploadMatricesIfChanged(1);
[activeEncoder setFragmentBytes:&color length:sizeof(color) atIndex:0];
[activeEncoder drawPrimitives:primitive vertexStart:0 vertexCount:(NSUInteger)vertexCount];
}
Expand Down Expand Up @@ -612,10 +671,9 @@ static void drawGradientQuad(CN1MetalPipeline pipeline,
if (activeEncoder == nil || pipelineCache == nil) return;
id<MTLRenderPipelineState> state = [pipelineCache pipelineFor:pipeline];
if (state == nil) return;
[activeEncoder setRenderPipelineState:state];
bindPipelineStateIfChanged(state);
[activeEncoder setVertexBytes:vertices length:sizeof(float) * 8 atIndex:0];
CN1MetalMatrices matrices = currentMatrices();
[activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:1];
uploadMatricesIfChanged(1);
[activeEncoder setVertexBytes:texcoords length:sizeof(float) * 8 atIndex:2];
[activeEncoder setFragmentBytes:&startColor length:sizeof(startColor) atIndex:0];
[activeEncoder setFragmentBytes:&endColor length:sizeof(endColor) atIndex:1];
Expand Down Expand Up @@ -743,7 +801,7 @@ void CN1MetalDrawAlphaMaskRadial(id<MTLTexture> texture,
if (activeEncoder == nil || pipelineCache == nil) return;
id<MTLRenderPipelineState> state = [pipelineCache pipelineFor:CN1MetalPipelineAlphaMaskRadial];
if (state == nil) return;
[activeEncoder setRenderPipelineState:state];
bindPipelineStateIfChanged(state);

float vertices[8] = {
(float)x, (float)y,
Expand All @@ -758,8 +816,7 @@ void CN1MetalDrawAlphaMaskRadial(id<MTLTexture> texture,
1.0f, 1.0f
};
[activeEncoder setVertexBytes:vertices length:sizeof(float) * 8 atIndex:0];
CN1MetalMatrices matrices = currentMatrices();
[activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:1];
uploadMatricesIfChanged(1);
[activeEncoder setVertexBytes:texcoords length:sizeof(float) * 8 atIndex:2];

// Premultiplied colours so blending produces the right output.
Expand Down Expand Up @@ -1011,6 +1068,7 @@ BOOL CN1MetalBeginMutableImageDraw(GLUIImage *image) {
savedScreenStateValid = YES;

activeEncoder = enc;
invalidateEncoderStateCache();
currentProjection = mutableProjection(w, h);
currentFramebufferWidth = w;
currentFramebufferHeight = h;
Expand All @@ -1037,6 +1095,7 @@ void CN1MetalEndMutableImageDraw(GLUIImage *image) {
// queue continue to use the screen encoder.
if (savedScreenStateValid) {
activeEncoder = savedScreenEncoder;
invalidateEncoderStateCache();
currentProjection = savedScreenProjection;
currentFramebufferWidth = savedScreenFw;
currentFramebufferHeight = savedScreenFh;
Expand Down
Loading
Loading