659 lines
25 KiB
Java

/*
* Copyright (c) 2023, Oracle 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. Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
*/
package sun.font;
import java.awt.geom.Point2D;
import sun.font.GlyphLayout.GVData;
import sun.java2d.Disposer;
import sun.java2d.DisposerRecord;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import static java.lang.foreign.MemorySegment.NULL;
import java.lang.foreign.SequenceLayout;
import java.lang.foreign.StructLayout;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.UnionLayout;
import static java.lang.foreign.ValueLayout.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;
import java.util.Optional;
import java.util.WeakHashMap;
public class HBShaper {
/*
* union _hb_var_int_t {
* uint32_t u32;
* int32_t i32;
* uint16_t u16[2];
* int16_t i16[2];
* uint8_t u8[4];
* int8_t i8[4];
* };
*/
private static final UnionLayout VarIntLayout = MemoryLayout.unionLayout(
JAVA_INT.withName("u32"),
JAVA_INT.withName("i32"),
MemoryLayout.sequenceLayout(2, JAVA_SHORT).withName("u16"),
MemoryLayout.sequenceLayout(2, JAVA_SHORT).withName("i16"),
MemoryLayout.sequenceLayout(4, JAVA_BYTE).withName("u8"),
MemoryLayout.sequenceLayout(4, JAVA_BYTE).withName("i8")
).withName("_hb_var_int_t");
/*
* struct hb_glyph_position_t {
* hb_position_t x_advance;
* hb_position_t y_advance;
* hb_position_t x_offset;
* hb_position_t y_offset;
* hb_var_int_t var;
* };
*/
private static final StructLayout PositionLayout = MemoryLayout.structLayout(
JAVA_INT.withName("x_advance"),
JAVA_INT.withName("y_advance"),
JAVA_INT.withName("x_offset"),
JAVA_INT.withName("y_offset"),
VarIntLayout.withName("var")
).withName("hb_glyph_position_t");
/**
* struct hb_glyph_info_t {
* hb_codepoint_t codepoint;
* hb_mask_t mask;
* uint32_t cluster;
* hb_var_int_t var1;
* hb_var_int_t var2;
* };
*/
private static final StructLayout GlyphInfoLayout = MemoryLayout.structLayout(
JAVA_INT.withName("codepoint"),
JAVA_INT.withName("mask"),
JAVA_INT.withName("cluster"),
VarIntLayout.withName("var1"),
VarIntLayout.withName("var2")
).withName("hb_glyph_info_t");
private static VarHandle getVarHandle(StructLayout struct, String name) {
VarHandle h = struct.arrayElementVarHandle(PathElement.groupElement(name));
/* insert 0 offset so don't need to pass arg every time */
return MethodHandles.insertCoordinates(h, 1, 0L).withInvokeExactBehavior();
}
private static final VarHandle x_offsetHandle;
private static final VarHandle y_offsetHandle;
private static final VarHandle x_advanceHandle;
private static final VarHandle y_advanceHandle;
private static final VarHandle codePointHandle;
private static final VarHandle clusterHandle;
private static final MethodHandles.Lookup MH_LOOKUP;
private static final Linker LINKER;
private static final SymbolLookup SYM_LOOKUP;
private static final MethodHandle malloc_handle;
private static final MethodHandle create_face_handle;
private static final MethodHandle dispose_face_handle;
private static final MethodHandle jdk_hb_shape_handle;
/* hb_jdk_font_funcs_struct is a pointer to a harfbuzz font_funcs
* object which references the 5 following upcall stubs.
* The singleton shared font_funcs ptr is passed down in each
* call to shape() and installed on the hb_font.
*/
private static final MemorySegment hb_jdk_font_funcs_struct;
private static final MemorySegment get_var_glyph_stub;
private static final MemorySegment get_nominal_glyph_stub;
private static final MemorySegment get_h_advance_stub;
private static final MemorySegment get_v_advance_stub;
private static final MemorySegment get_contour_pt_stub;
private static final MemorySegment store_layout_results_stub;
private static FunctionDescriptor
getFunctionDescriptor(MemoryLayout retType,
MemoryLayout... argTypes) {
return (retType == null) ?
FunctionDescriptor.ofVoid(argTypes) :
FunctionDescriptor.of(retType, argTypes);
}
private static MethodHandle getMethodHandle
(String mName,
FunctionDescriptor fd) {
try {
MethodType mType = fd.toMethodType();
return MH_LOOKUP.findStatic(HBShaper.class, mName, mType);
} catch (IllegalAccessException | NoSuchMethodException e) {
return null;
}
}
static {
MH_LOOKUP = MethodHandles.lookup();
LINKER = Linker.nativeLinker();
SYM_LOOKUP = SymbolLookup.loaderLookup().or(LINKER.defaultLookup());
FunctionDescriptor mallocDescriptor =
FunctionDescriptor.of(ADDRESS, JAVA_LONG);
MemorySegment malloc_symbol = SYM_LOOKUP.findOrThrow("malloc");
@SuppressWarnings("restricted")
MethodHandle tmp1 = LINKER.downcallHandle(malloc_symbol, mallocDescriptor);
malloc_handle = tmp1;
FunctionDescriptor createFaceDescriptor =
FunctionDescriptor.of(ADDRESS, ADDRESS);
MemorySegment create_face_symbol = SYM_LOOKUP.findOrThrow("HBCreateFace");
@SuppressWarnings("restricted")
MethodHandle tmp2 = LINKER.downcallHandle(create_face_symbol, createFaceDescriptor);
create_face_handle = tmp2;
FunctionDescriptor disposeFaceDescriptor = FunctionDescriptor.ofVoid(ADDRESS);
MemorySegment dispose_face_symbol = SYM_LOOKUP.findOrThrow("HBDisposeFace");
@SuppressWarnings("restricted")
MethodHandle tmp3 = LINKER.downcallHandle(dispose_face_symbol, disposeFaceDescriptor);
dispose_face_handle = tmp3;
FunctionDescriptor shapeDesc = FunctionDescriptor.ofVoid(
JAVA_FLOAT, // ptSize
ADDRESS, // matrix
ADDRESS, // face
ADDRESS, // chars
JAVA_INT, // len
JAVA_INT, // script
JAVA_INT, // offset
JAVA_INT, // limit
JAVA_INT, // baseIndex
JAVA_FLOAT, // startX
JAVA_FLOAT, // startY
JAVA_INT, // flags,
JAVA_INT, // slot,
ADDRESS, // ptr to harfbuzz font_funcs object.
ADDRESS); // store_results_fn
MemorySegment shape_sym = SYM_LOOKUP.findOrThrow("jdk_hb_shape");
@SuppressWarnings("restricted")
MethodHandle tmp4 = LINKER.downcallHandle(shape_sym, shapeDesc);
jdk_hb_shape_handle = tmp4;
Arena garena = Arena.global(); // creating stubs that exist until VM exit.
FunctionDescriptor get_var_glyph_fd = getFunctionDescriptor(JAVA_INT, // return type
ADDRESS, ADDRESS, JAVA_INT, JAVA_INT, ADDRESS, ADDRESS); // arg types
MethodHandle get_var_glyph_mh =
getMethodHandle("get_variation_glyph", get_var_glyph_fd);
@SuppressWarnings("restricted")
MemorySegment tmp5 = LINKER.upcallStub(get_var_glyph_mh, get_var_glyph_fd, garena);
get_var_glyph_stub = tmp5;
FunctionDescriptor get_nominal_glyph_fd = getFunctionDescriptor(JAVA_INT, // return type
ADDRESS, ADDRESS, JAVA_INT, ADDRESS, ADDRESS); // arg types
MethodHandle get_nominal_glyph_mh =
getMethodHandle("get_nominal_glyph", get_nominal_glyph_fd);
@SuppressWarnings("restricted")
MemorySegment tmp6 = LINKER.upcallStub(get_nominal_glyph_mh, get_nominal_glyph_fd, garena);
get_nominal_glyph_stub = tmp6;
FunctionDescriptor get_h_adv_fd = getFunctionDescriptor(JAVA_INT, // return type
ADDRESS, ADDRESS, JAVA_INT, ADDRESS); // arg types
MethodHandle get_h_adv_mh =
getMethodHandle("get_glyph_h_advance", get_h_adv_fd);
@SuppressWarnings("restricted")
MemorySegment tmp7 = LINKER.upcallStub(get_h_adv_mh, get_h_adv_fd, garena);
get_h_advance_stub = tmp7;
FunctionDescriptor get_v_adv_fd = getFunctionDescriptor(JAVA_INT, // return type
ADDRESS, ADDRESS, JAVA_INT, ADDRESS); // arg types
MethodHandle get_v_adv_mh =
getMethodHandle("get_glyph_v_advance", get_v_adv_fd);
@SuppressWarnings("restricted")
MemorySegment tmp8 = LINKER.upcallStub(get_v_adv_mh, get_v_adv_fd, garena);
get_v_advance_stub = tmp8;
FunctionDescriptor get_contour_pt_fd = getFunctionDescriptor(JAVA_INT, // return type
ADDRESS, ADDRESS, JAVA_INT, JAVA_INT, ADDRESS, ADDRESS, ADDRESS); // arg types
MethodHandle get_contour_pt_mh =
getMethodHandle("get_glyph_contour_point", get_contour_pt_fd);
@SuppressWarnings("restricted")
MemorySegment tmp9 = LINKER.upcallStub(get_contour_pt_mh, get_contour_pt_fd, garena);
get_contour_pt_stub = tmp9;
/* Having now created the font upcall stubs, we can call down to create
* the native harfbuzz object holding these.
*/
FunctionDescriptor createFontFuncsDescriptor = FunctionDescriptor.of(
ADDRESS, // hb_font_funcs* return type
ADDRESS, // glyph_fn upcall stub
ADDRESS, // variation_fn upcall stub
ADDRESS, // h_advance_fn upcall stub
ADDRESS, // v_advance_fn upcall stub
ADDRESS); // contour_pt_fn upcall stub
MemorySegment create_font_funcs_symbol = SYM_LOOKUP.findOrThrow("HBCreateFontFuncs");
@SuppressWarnings("restricted")
MethodHandle create_font_funcs_handle =
LINKER.downcallHandle(create_font_funcs_symbol, createFontFuncsDescriptor);
MemorySegment s = null;
try {
s = (MemorySegment)create_font_funcs_handle.invokeExact(
get_nominal_glyph_stub,
get_var_glyph_stub,
get_h_advance_stub,
get_v_advance_stub,
get_contour_pt_stub);
} catch (Throwable t) {
t.printStackTrace();
}
hb_jdk_font_funcs_struct = s;
FunctionDescriptor store_layout_fd =
FunctionDescriptor.ofVoid(
JAVA_INT, // slot
JAVA_INT, // baseIndex
JAVA_INT, // offset
JAVA_FLOAT, // startX
JAVA_FLOAT, // startX
JAVA_FLOAT, // devScale
JAVA_INT, // charCount
JAVA_INT, // glyphCount
ADDRESS, // glyphInfo
ADDRESS); // glyphPos
MethodHandle store_layout_mh =
getMethodHandle("store_layout_results", store_layout_fd);
@SuppressWarnings("restricted")
MemorySegment tmp10 = LINKER.upcallStub(store_layout_mh, store_layout_fd, garena);
store_layout_results_stub = tmp10;
x_offsetHandle = getVarHandle(PositionLayout, "x_offset");
y_offsetHandle = getVarHandle(PositionLayout, "y_offset");
x_advanceHandle = getVarHandle(PositionLayout, "x_advance");
y_advanceHandle = getVarHandle(PositionLayout, "y_advance");
codePointHandle = getVarHandle(GlyphInfoLayout, "codepoint");
clusterHandle = getVarHandle(GlyphInfoLayout, "cluster");
}
/*
* This is expensive but it is done just once per font.
* The unbound stub could be cached but the savings would
* be very low in the only case it is used.
*/
@SuppressWarnings("restricted")
private static MemorySegment getBoundUpcallStub
(Arena arena, Class<?> clazz, Object bindArg, String mName,
MemoryLayout retType, MemoryLayout... argTypes) {
try {
FunctionDescriptor nativeDescriptor =
(retType == null) ?
FunctionDescriptor.ofVoid(argTypes) :
FunctionDescriptor.of(retType, argTypes);
MethodType mType = nativeDescriptor.toMethodType();
mType = mType.insertParameterTypes(0, clazz);
MethodHandle mh = MH_LOOKUP.findStatic(HBShaper.class, mName, mType);
MethodHandle bound_handle = mh.bindTo(bindArg);
return LINKER.upcallStub(bound_handle, nativeDescriptor, arena);
} catch (IllegalAccessException | NoSuchMethodException e) {
return null;
}
}
private static int get_nominal_glyph(
MemorySegment font_ptr, /* Not used */
MemorySegment font_data, /* Not used */
int unicode,
MemorySegment glyph, /* pointer to location to store glyphID */
MemorySegment user_data /* Not used */
) {
Font2D font2D = scopedVars.get().font();
int glyphID = font2D.charToGlyph(unicode);
@SuppressWarnings("restricted")
MemorySegment glyphIDPtr = glyph.reinterpret(4);
glyphIDPtr.setAtIndex(JAVA_INT, 0, glyphID);
return (glyphID != 0) ? 1 : 0;
}
private static int get_variation_glyph(
MemorySegment font_ptr, /* Not used */
MemorySegment font_data, /* Not used */
int unicode,
int variation_selector,
MemorySegment glyph, /* pointer to location to store glyphID */
MemorySegment user_data /* Not used */
) {
Font2D font2D = scopedVars.get().font();
int glyphID = font2D.charToVariationGlyph(unicode, variation_selector);
@SuppressWarnings("restricted")
MemorySegment glyphIDPtr = glyph.reinterpret(4);
glyphIDPtr.setAtIndex(JAVA_INT, 0, glyphID);
return (glyphID != 0) ? 1 : 0;
}
private static final float HBFloatToFixedScale = ((float)(1 << 16));
private static final int HBFloatToFixed(float f) {
return ((int)((f) * HBFloatToFixedScale));
}
private static int get_glyph_h_advance(
MemorySegment font_ptr, /* Not used */
MemorySegment font_data, /* Not used */
int glyph,
MemorySegment user_data /* Not used */
) {
FontStrike strike = scopedVars.get().fontStrike();
Point2D.Float pt = strike.getGlyphMetrics(glyph);
return (pt != null) ? HBFloatToFixed(pt.x) : 0;
}
private static int get_glyph_v_advance(
MemorySegment font_ptr, /* Not used */
MemorySegment font_data, /* Not used */
int glyph,
MemorySegment user_data /* Not used */
) {
FontStrike strike = scopedVars.get().fontStrike();
Point2D.Float pt = strike.getGlyphMetrics(glyph);
return (pt != null) ? HBFloatToFixed(pt.y) : 0;
}
/*
* This class exists to make the code that uses it less verbose
*/
private static class IntPtr {
MemorySegment seg;
IntPtr(MemorySegment seg) {
}
void set(int i) {
seg.setAtIndex(JAVA_INT, 0, i);
}
}
private static int get_glyph_contour_point(
MemorySegment font_ptr, /* Not used */
MemorySegment font_data, /* Not used */
int glyph,
int point_index,
MemorySegment x_ptr, /* ptr to return x */
MemorySegment y_ptr, /* ptr to return y */
MemorySegment user_data /* Not used */
) {
IntPtr x = new IntPtr(x_ptr);
IntPtr y = new IntPtr(y_ptr);
if ((glyph & 0xfffe) == 0xfffe) {
x.set(0);
y.set(0);
return 1;
}
FontStrike strike = scopedVars.get().fontStrike();
Point2D.Float pt = ((PhysicalStrike)strike).getGlyphPoint(glyph, point_index);
x.set(HBFloatToFixed(pt.x));
y.set(HBFloatToFixed(pt.y));
return 1;
}
record ScopedVars (
Font2D font,
FontStrike fontStrike,
GVData gvData,
Point2D.Float point) {}
static final ScopedValue<ScopedVars> scopedVars = ScopedValue.newInstance();
static void shape(
Font2D font2D,
FontStrike fontStrike,
float ptSize,
float[] mat,
MemorySegment hbface,
char[] text,
GVData gvData,
int script,
int offset,
int limit,
int baseIndex,
Point2D.Float startPt,
int flags,
int slot) {
/*
* ScopedValue is needed so that call backs into Java during
* shaping can locate the correct instances of these to query or update.
* The alternative of creating bound method handles is far too slow.
*/
ScopedVars vars = new ScopedVars(font2D, fontStrike, gvData, startPt);
ScopedValue.where(scopedVars, vars)
.run(() -> {
try (Arena arena = Arena.ofConfined()) {
float startX = (float)startPt.getX();
float startY = (float)startPt.getY();
MemorySegment matrix = arena.allocateFrom(JAVA_FLOAT, mat);
MemorySegment chars = arena.allocateFrom(JAVA_CHAR, text);
jdk_hb_shape_handle.invokeExact(
ptSize, matrix, hbface, chars, text.length,
script, offset, limit,
baseIndex, startX, startY, flags, slot,
hb_jdk_font_funcs_struct,
store_layout_results_stub);
} catch (Throwable t) {
}
});
}
private static int getFontTableData(Font2D font2D,
int tag,
MemorySegment data_ptr_out) {
/*
* On return, the data_out_ptr will point to memory allocated by native malloc,
* so it will be freed by the caller using native free - when it is
* done with it.
*/
@SuppressWarnings("restricted")
MemorySegment data_ptr = data_ptr_out.reinterpret(ADDRESS.byteSize());
if (tag == 0) {
data_ptr.setAtIndex(ADDRESS, 0, NULL);
return 0;
}
byte[] data = font2D.getTableBytes(tag);
if (data == null) {
data_ptr.setAtIndex(ADDRESS, 0, NULL);
return 0;
}
int len = data.length;
MemorySegment zero_len = NULL;
try {
zero_len = (MemorySegment)malloc_handle.invokeExact((long)len);
} catch (Throwable t) {
}
if (zero_len.equals(NULL)) {
data_ptr.setAtIndex(ADDRESS, 0, NULL);
return 0;
}
@SuppressWarnings("restricted")
MemorySegment mem = zero_len.reinterpret(len);
MemorySegment.copy(data, 0, mem, JAVA_BYTE, 0, len);
data_ptr.setAtIndex(ADDRESS, 0, mem);
return len;
}
/* WeakHashMap is used so that we do not retain temporary fonts
*
* The value is a class that implements the 2D Disposer, so
* that the native resources for temp. fonts can be freed.
*
* Installed fonts should never be cleared from the map as
* they are permanently referenced.
*/
private static final WeakHashMap<Font2D, FaceRef>
faceMap = new WeakHashMap<>();
static MemorySegment getFace(Font2D font2D) {
FaceRef ref;
synchronized (faceMap) {
ref = faceMap.computeIfAbsent(font2D, FaceRef::new);
}
return ref.getFace();
}
private static class FaceRef implements DisposerRecord {
private Font2D font2D;
private MemorySegment face;
// get_table_data_fn uses an Arena managed by GC,
// so we need to keep a reference to it here until
// this FaceRef is collected.
private MemorySegment get_table_data_fn;
private FaceRef(Font2D font) {
this.font2D = font;
}
private synchronized MemorySegment getFace() {
if (face == null) {
createFace();
if (face != null) {
Disposer.addObjectRecord(font2D, this);
}
font2D = null;
}
return face;
}
private void createFace() {
try {
get_table_data_fn = getBoundUpcallStub(Arena.ofAuto(),
Font2D.class,
font2D, // bind arg
"getFontTableData", // method name
JAVA_INT, // return type
JAVA_INT, ADDRESS); // arg types
if (get_table_data_fn == null) {
return;
}
face = (MemorySegment)create_face_handle.invokeExact(get_table_data_fn);
} catch (Throwable t) {
}
}
@Override
public void dispose() {
try {
dispose_face_handle.invokeExact(face);
} catch (Throwable t) {
}
}
}
/* Upcall to receive results of layout */
private static void store_layout_results(
int slot,
int baseIndex,
int offset,
float startX,
float startY,
float devScale,
int charCount,
int glyphCount,
MemorySegment /* hb_glyph_info_t* */ glyphInfo,
MemorySegment /* hb_glyph_position_t* */ glyphPos
) {
GVData gvdata = scopedVars.get().gvData();
Point2D.Float startPt = scopedVars.get().point();
float x=0, y=0;
float advX, advY;
float scale = 1.0f / HBFloatToFixedScale / devScale;
int initialCount = gvdata._count;
int maxGlyphs = (charCount > glyphCount) ? charCount : glyphCount;
int maxStore = maxGlyphs + initialCount;
boolean needToGrow = (maxStore > gvdata._glyphs.length) ||
((maxStore * 2 + 2) > gvdata._positions.length);
if (needToGrow) {
gvdata.grow(maxStore-initialCount);
}
int glyphPosLen = glyphCount * 2 + 2;
long posSize = glyphPosLen * PositionLayout.byteSize();
@SuppressWarnings("restricted")
MemorySegment glyphPosArr = glyphPos.reinterpret(posSize);
long glyphInfoSize = glyphCount * GlyphInfoLayout.byteSize();
@SuppressWarnings("restricted")
MemorySegment glyphInfoArr = glyphInfo.reinterpret(glyphInfoSize);
for (int i = 0; i < glyphCount; i++) {
int storei = i + initialCount;
int cluster = (int)clusterHandle.get(glyphInfoArr, (long)i) - offset;
gvdata._indices[storei] = baseIndex + cluster;
int codePoint = (int)codePointHandle.get(glyphInfoArr, (long)i);
gvdata._glyphs[storei] = (slot | codePoint);
int x_offset = (int)x_offsetHandle.get(glyphPosArr, (long)i);
int y_offset = (int)y_offsetHandle.get(glyphPosArr, (long)i);
gvdata._positions[(storei*2)] = startX + x + (x_offset * scale);
gvdata._positions[(storei*2)+1] = startY + y - (y_offset * scale);
int x_advance = (int)x_advanceHandle.get(glyphPosArr, (long)i);
int y_advance = (int)y_advanceHandle.get(glyphPosArr, (long)i);
x += x_advance * scale;
y += y_advance * scale;
}
int storeadv = initialCount + glyphCount;
gvdata._count = storeadv;
// The final slot in the positions array is important
// because when the GlyphVector is created from this
// data it determines the overall advance of the glyphvector
// and this is used in positioning the next glyphvector
// during rendering where text is broken into runs.
// We also need to report it back into "pt", so layout can
// pass it back down for any next run.
advX = startX + x;
advY = startY + y;
gvdata._positions[(storeadv*2)] = advX;
gvdata._positions[(storeadv*2)+1] = advY;
startPt.x = advX;
startPt.y = advY;
}
}