Skip to content

Commit 5743c5d

Browse files
Changed: Increase Sixel and iTerm Image limits
The `TerminalBitmap.MAX_BITMAP_SIZE` defines the max size of a Terminal bitmap for its pixels. Each pixel is stored on 4 bytes for a `Bitmap.Config.ARGB_8888` bitmap color config. The value should normally be between `100-200MB` depending on device and Android version. Check the variable docs for more info. The sixel image size cannot be greater than `TerminalBitmap.MAX_BITMAP_SIZE`. The repeat value for sixel Graphics Repeat Introducer command cannot be greater than `TerminalSixel.SIXEL__MAX_REPEAT` (`8192`). The iTerm image data sent for `File=` and `MultipartFile=` protocols cannot be greater than `TerminalBitmap.MAX_BITMAP_SIZE` bytes.
1 parent d566f41 commit 5743c5d

5 files changed

Lines changed: 306 additions & 69 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.termux.terminal;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.io.InputStreamReader;
7+
import java.util.Properties;
8+
import java.util.regex.Matcher;
9+
import java.util.regex.Pattern;
10+
11+
public class AndroidUtils {
12+
13+
/**
14+
* Get system properties as {@link Properties} from `/system/bin/getprop` command output
15+
* that reads the `/system/build.prop` file.
16+
*
17+
* Sourced from https://github.com/termux/termux-app/blob/30ebb2dee381d292ade0f2868cfde0f9f20b89fe/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java#L170.
18+
*/
19+
public static Properties getSystemProperties(String logTag) {
20+
Properties systemProperties = new Properties();
21+
22+
// getprop commands returns values in the format `[key]: [value]`
23+
// Regex matches string starting with a literal `[`,
24+
// followed by one or more characters that do not match a closing square bracket as the key,
25+
// followed by a literal `]: [`,
26+
// followed by one or more characters as the value,
27+
// followed by string ending with literal `]`
28+
// multiline values will be ignored
29+
Pattern propertiesPattern = Pattern.compile("^\\[([^]]+)]: \\[(.+)]$");
30+
31+
try {
32+
Process process = new ProcessBuilder()
33+
.command("/system/bin/getprop")
34+
.redirectErrorStream(true)
35+
.start();
36+
37+
InputStream inputStream = process.getInputStream();
38+
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
39+
String line, key, value;
40+
41+
while ((line = bufferedReader.readLine()) != null) {
42+
Matcher matcher = propertiesPattern.matcher(line);
43+
if (matcher.matches()) {
44+
key = matcher.group(1);
45+
value = matcher.group(2);
46+
if (key != null && value != null && !key.isEmpty() && !value.isEmpty())
47+
systemProperties.put(key, value);
48+
}
49+
}
50+
51+
bufferedReader.close();
52+
process.destroy();
53+
54+
} catch (IOException e) {
55+
Logger.logStackTraceWithMessage(null, logTag, "Failed to get run \"/system/bin/getprop\" to get system properties.", e);
56+
}
57+
58+
//for (String key : systemProperties.stringPropertyNames()) {
59+
// Logger.logVerbose(null, logTag, key + ": " + systemProperties.get(key));
60+
//}
61+
62+
return systemProperties;
63+
}
64+
65+
}

terminal-emulator/src/main/java/com/termux/terminal/TerminalBitmap.java

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
package com.termux.terminal;
22

3+
import android.app.WallpaperManager;
34
import android.graphics.Bitmap;
45
import android.graphics.BitmapFactory;
6+
import android.graphics.Canvas;
7+
import android.graphics.Paint;
8+
import android.graphics.RecordingCanvas;
9+
import android.graphics.Rect;
10+
import android.graphics.RectF;
11+
import android.os.Build;
12+
13+
import java.util.Properties;
514

615
/**
716
* A terminal bitmap for images.
@@ -10,6 +19,110 @@ public class TerminalBitmap {
1019

1120
public static final String LOG_TAG = "TerminalBitmap";
1221

22+
private static int initMaxBitmapSize() {
23+
int defaultSize =
24+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM ?
25+
150 * 1024 * 1024 : // 150 MB
26+
100 * 1024 * 1024; // 100 MB
27+
28+
Properties systemProperties = AndroidUtils.getSystemProperties(LOG_TAG);
29+
String maxTextureSizeString = systemProperties.getProperty("ro.hwui.max_texture_allocation_size");
30+
31+
if (maxTextureSizeString == null) return defaultSize;
32+
33+
try {
34+
int maxTextureSize = Integer.parseInt(maxTextureSizeString);
35+
return maxTextureSize > 0 ? maxTextureSize : defaultSize;
36+
}
37+
catch (Exception e) {
38+
return defaultSize;
39+
}
40+
}
41+
42+
/**
43+
* The max size of a Terminal {@link Bitmap} for its pixels. The limit is defined as per how
44+
* `RecordingCanvas.MAX_BITMAP_SIZE` value is defined, check below for details. The value should
45+
* normally be between `100-200MB` depending on device and Android version.
46+
*
47+
* Each pixel is stored on 4 bytes for a {@link Bitmap.Config#ARGB_8888} bitmap color config.
48+
* The bitmap will have following memory usage for its respective resolution (`width x height x 4`).
49+
* - 1280x720 (HD): 3,686,400 bytes/3.6MB.
50+
* - 1920x1080 (FHD): 8,294,400 bytes/8MB.
51+
* - 2560x1440 (QHD): 14,745,600 bytes/14.7MB.
52+
* - 3840x2160 (4K UHD): 33,177,600 bytes/33MB.
53+
* - 7680x4320 (8K UHD): 132,710,400 bytes/132MB.
54+
* .
55+
* - https://en.wikipedia.org/wiki/Display_resolution_standards#High-definition
56+
*
57+
* The terminal uses {@link Canvas#drawBitmap(Bitmap, Rect, RectF, Paint)} to draw the bitmap
58+
* when `TerminalRenderer.render()` is called.
59+
*
60+
* The {@link Canvas} class defines `Canvas.MAXIMUM_BITMAP_SIZE` for the maximum dimension
61+
* for a bitmap which is returned by {@link Canvas#getMaximumBitmapWidth()} and
62+
* {@link Canvas#getMaximumBitmapHeight()}. It is hardcoded with the value `32766` as defined by
63+
* Skia (2D graphics library), which technically has the limit `32767` as it requires supporting
64+
* math on 16-bit buffers.
65+
* - https://cs.android.com/android/_/android/platform/frameworks/base/+/f61970fc79e9c5cf340fa942597628242361864a
66+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/graphics/java/android/graphics/Canvas.java;l=76-78
67+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:external/skia/src/shaders/SkImageShader.cpp;l=254-267
68+
*
69+
* The {@link RecordingCanvas} class defines `RecordingCanvas.MAX_BITMAP_SIZE` for the
70+
* maximum size (not dimension) for a bitmap, which is checked by
71+
* `RecordingCanvas.throwIfCannotDraw()` when `BaseRecordingCanvas.drawBitmap()` is called.
72+
* The `RecordingCanvas` is a specialized implementation of the `Canvas` class that is designed
73+
* to record draw commands for deferred rendering instead of executing draw commands instantly.
74+
* By recording draw commands, they can be cached so that complex views can be efficiently
75+
* re-drawn without recalculating them again for every frame. The caching part is similar to
76+
* how a terminal behaves, where it stores all the bitmaps for rendering depending on scroll
77+
* position. So both {@link RecordingCanvas} and a terminal require similar limits on bitmap
78+
* sizes considering memory consumption limits of apps, and multiple bitmaps being loaded
79+
* instead of a single one like for wallpapers, hence why `TerminalBitmap.MAX_BITMAP_SIZE` is
80+
* synced with {@link RecordingCanvas}.
81+
* The `RecordingCanvas.MAX_BITMAP_SIZE` is set from `ro.hwui.max_texture_allocation_size`
82+
* system property if set for Android `>= 12`, otherwise `150MB` (`100MB` for Android `10-14`).
83+
* The values `>= 150MB` are enough to support `7680x4320` (8K UHD) bitmaps.
84+
* Some devices like larger xiaomi devices have `ro.hwui.max_texture_allocation_size` set to `209715200` (`200MB`).
85+
* - https://cs.android.com/android/_/android/platform/frameworks/base/+/e4d011201cea40d46cb2b2eef401db8fddc5c9c6
86+
* - https://cs.android.com/android/_/android/platform/frameworks/base/+/0e717a9d06ded980908649393bd73e46ffafcd54
87+
* - https://cs.android.com/android/_/android/platform/frameworks/base/+/97396260ed06cc9d1834d4d8e4e649a3ef09f1f3
88+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/graphics/java/android/graphics/RecordingCanvas.java;l=42-50
89+
*
90+
* The Android wallpaper manager service also checks if dimensions of cropped wallpaper exceeds
91+
* max texture size that the GPU can support, otherwise it will cause System UI to keep crashing
92+
* because it can not initialize EGL with an appropriate surface. The `GLHelper.getMaxTextureSize()`
93+
* returns the max texture size, which is defined by `sys.max_texture_size` system property if set,
94+
* otherwise by value for `GL_MAX_TEXTURE_SIZE`. The `sys.max_texture_size` defines the maximum
95+
* width or height of a texture, not total size. Its value can be low like `2048` or high like
96+
* `16384` for 16K support.
97+
* - https://cs.android.com/android/_/android/platform/frameworks/base/+/32c6a7c691b0d91085c1ed13fe6f1c473c94b4c8
98+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/services/core/java/com/android/server/wallpaper/WallpaperCropper.java;l=461
99+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/services/core/java/com/android/server/wallpaper/GLHelper.java;l=145
100+
* - https://developer.android.com/reference/android/opengl/GLES10#GL_MAX_TEXTURE_SIZE
101+
*
102+
* The {@link WallpaperManager#getDesiredMinimumWidth()} and {@link WallpaperManager#getDesiredMinimumHeight()}
103+
* can also be called to get minimum suggested width and height of the wallpaper that an app
104+
* should use when setting the wallpaper. This normally is equal to the width and height of the
105+
* current device display, but the width can be higher than display width if the homescreen is
106+
* scrollable horizontally with multiple pages, in which case the width returned is equal to
107+
* entire workspace width. The launcher apps can provide Android their desired width and height
108+
* dimensions depending on the homescreen pages config by calling
109+
* {@link WallpaperManager#suggestDesiredDimensions(int, int)}, which also ensures that values
110+
* passed are scaled down to `sys.max_texture_size` system property if its set.
111+
* - https://cs.android.com/android/_/android/platform/frameworks/base/+/289c273ec49462c7bfdbf6238e9016936da7307c
112+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/core/java/android/app/WallpaperManager.java;l=2737-2794
113+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java;l=2330-2366
114+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java;l=108-115
115+
* - https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:frameworks/base/core/java/android/view/Display.java;l=1052-1063
116+
*
117+
* If an app specifies `largeHeap=true` in its `AndroidManifest.xml`, then it can be allocated
118+
* larger heap memory to load larger bitmaps maps instead of resulting in an OOM. The Termux app
119+
* does not have it enabled, and hence is more likely to have OOMs when loading larger bitmaps.
120+
* - https://developer.android.com/guide/topics/manifest/application-element#largeHeap
121+
* - https://developer.android.com/topic/performance/memory
122+
*/
123+
public static final int MAX_BITMAP_SIZE = initMaxBitmapSize();
124+
125+
13126

14127
protected final TerminalSessionClient mClient;
15128

@@ -304,8 +417,15 @@ public static Bitmap resizeBitmap(String logTag, String label, TerminalSessionCl
304417

305418
Bitmap newBitmap;
306419
try {
307-
int[] pixels = new int[bitmap.getAllocationByteCount()];
420+
int newBitmapSize = bitmapWidth * bitmapHeight * 4;
421+
if (newBitmapSize < 0 || newBitmapSize > MAX_BITMAP_SIZE) {
422+
Logger.logError(client, logTag, "The new " + label + " bitmap after resize with" +
423+
" width " + bitmapWidth + " and height " + bitmapHeight +
424+
" has size " + newBitmapSize + " greater than max bitmap size " + MAX_BITMAP_SIZE);
425+
return null;
426+
}
308427

428+
int[] pixels = new int[bitmap.getAllocationByteCount()];
309429
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
310430

311431
newBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);

terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -630,13 +630,22 @@ public synchronized void sixelClear() {
630630
mTerminalSixel = null;
631631
}
632632

633+
/**
634+
* Clears the {@link #mTerminalSixel} by setting it to `null` and logs error.
635+
* Call this on error if further sixel commands/data should be parsed to prevent them from
636+
* printing on terminal, but sixel rendering should be ignored.
637+
*/
638+
public synchronized void sixelIgnore() {
639+
Logger.logError(mClient, LOG_TAG, "Ignoring sixel rendering");
640+
mTerminalSixel = null;
641+
}
642+
633643
public synchronized boolean sixelReadData(int codePoint, int repeat) {
634644
// If an error occurred during processing (like OOM), then remaining sixel command is
635645
// completely read, but is ignored.
636646
if (mTerminalSixel != null) {
637647
if (!mTerminalSixel.readData(codePoint, repeat)) {
638-
// Ignore further commands/data.
639-
mTerminalSixel = null;
648+
sixelIgnore();
640649
return false;
641650
}
642651
}
@@ -648,8 +657,7 @@ public synchronized boolean sixelResize(int sixelWidth, int sixelHeight) {
648657
// completely read, but is ignored.
649658
if (mTerminalSixel != null) {
650659
if (!mTerminalSixel.resize(sixelWidth, sixelHeight)) {
651-
// Ignore further commands/data.
652-
mTerminalSixel = null;
660+
sixelIgnore();
653661
return false;
654662
}
655663
}

0 commit comments

Comments
 (0)