1 /* 2 * Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.prism.impl; 27 28 import com.sun.javafx.font.CharToGlyphMapper; 29 import com.sun.javafx.font.CompositeGlyphMapper; 30 import com.sun.javafx.font.FontResource; 31 import com.sun.javafx.font.FontStrike; 32 import com.sun.javafx.font.Glyph; 33 import com.sun.javafx.geom.BaseBounds; 34 import com.sun.javafx.geom.Rectangle; 35 import com.sun.javafx.geom.Point2D; 36 import com.sun.javafx.geom.transform.BaseTransform; 37 import com.sun.javafx.scene.text.GlyphList; 38 import com.sun.prism.impl.packrect.RectanglePacker; 39 import com.sun.prism.Texture; 40 import com.sun.prism.impl.shape.MaskData; 41 import com.sun.prism.paint.Color; 42 43 import java.nio.ByteBuffer; 44 import java.util.HashMap; 45 import java.util.WeakHashMap; 46 47 import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; 48 import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGER; 49 50 import com.sun.prism.ResourceFactory; 51 import com.sun.prism.Texture.WrapMode; 52 53 public class GlyphCache { 54 55 // REMIND: For a less powerful device, the size of this cache 56 // is likely something we'd want to tune as they may have much less 57 // VRAM and are less likely to be used for apps that have huge 58 // text demands. 59 // 2048 pixels introduced very noticeable pauses when trying 60 // to free 1/4 of the glyphs, which for spiral text also amounts 61 // to 1/4 of the strikes. 62 private static final int WIDTH = PrismSettings.glyphCacheWidth; // in pixels 63 private static final int HEIGHT = PrismSettings.glyphCacheHeight; // in pixels 64 private static ByteBuffer emptyMask; 65 66 private final BaseContext context; 67 private final FontStrike strike; 68 69 // segmented arrays are in blocks of 32 glyphs. 70 private static final int SEGSHIFT = 5; 71 private static final int SEGSIZE = 1 << SEGSHIFT; 72 HashMap<Integer, GlyphData[]> 73 glyphDataMap = new HashMap<Integer, GlyphData[]>(); 74 75 // Because of SEGSHIFT the 5 high bit in the key to glyphDataMap are unused 76 // Using them for subpixel 77 private static final int SUBPIXEL_SHIFT = 27; 78 79 private RectanglePacker packer; 80 81 private boolean isLCDCache; 82 83 /* Share a RectanglePacker and its associated texture cache 84 * for all uses on a particular screen. 85 */ 86 static WeakHashMap<BaseContext, RectanglePacker> greyPackerMap = 87 new WeakHashMap<BaseContext, RectanglePacker>(); 88 89 static WeakHashMap<BaseContext, RectanglePacker> lcdPackerMap = 90 new WeakHashMap<BaseContext, RectanglePacker>(); 91 92 public GlyphCache(BaseContext context, FontStrike strike) { 93 this.context = context; 94 this.strike = strike; 95 //numGlyphs = strike.getNumGlyphs(); 96 //int numSegments = (numGlyphs + SEGSIZE-1)/SEGSIZE; 97 //this.glyphs = new GlyphData[numSegments][]; 98 isLCDCache = strike.getAAMode() == FontResource.AA_LCD; 99 WeakHashMap<BaseContext, RectanglePacker> 100 packerMap = isLCDCache ? lcdPackerMap : greyPackerMap; 101 packer = packerMap.get(context); 102 if (packer == null) { 103 ResourceFactory factory = context.getResourceFactory(); 104 Texture tex = factory.createMaskTexture(WIDTH, HEIGHT, 105 WrapMode.CLAMP_NOT_NEEDED); 106 tex.contentsUseful(); 107 tex.makePermanent(); 108 if (!isLCDCache) { 109 factory.setGlyphTexture(tex); 110 } 111 tex.setLinearFiltering(false); 112 packer = new RectanglePacker(tex, WIDTH, HEIGHT); 113 packerMap.put(context, packer); 114 } 115 } 116 117 public void render(BaseContext ctx, GlyphList gl, float x, float y, 118 int start, int end, Color rangeColor, Color textColor, 119 BaseTransform xform, BaseBounds clip) { 120 121 int dstw, dsth; 122 if (isLCDCache) { 123 dstw = ctx.getLCDBuffer().getPhysicalWidth(); 124 dsth = ctx.getLCDBuffer().getPhysicalHeight(); 125 } else { 126 dstw = 1; 127 dsth = 1; 128 } 129 Texture tex = getBackingStore(); 130 VertexBuffer vb = ctx.getVertexBuffer(); 131 132 int len = gl.getGlyphCount(); 133 Color currentColor = null; 134 Point2D pt = new Point2D(); 135 136 for (int gi = 0; gi < len; gi++) { 137 int gc = gl.getGlyphCode(gi); 138 139 // If we have a supplementary character, then a special 140 // glyph is inserted in the list, which is one we skip 141 // over for rendering. It has no advance. 142 if ((gc & CompositeGlyphMapper.GLYPHMASK) == CharToGlyphMapper.INVISIBLE_GLYPH_ID) { 143 continue; 144 } 145 pt.setLocation(x + gl.getPosX(gi), y + gl.getPosY(gi)); 146 int subPixel = strike.getQuantizedPosition(pt); 147 GlyphData data = getCachedGlyph(gc, subPixel); 148 if (data != null) { 149 if (clip != null) { 150 // Always check clipping using user space. 151 if (x + gl.getPosX(gi) > clip.getMaxX()) break; 152 if (x + gl.getPosX(gi + 1) < clip.getMinX()) continue; 153 } 154 /* Will not render selected text for complex 155 * paints such as gradient. 156 */ 157 if (rangeColor != null && textColor != null) { 158 int offset = gl.getCharOffset(gi); 159 if (start <= offset && offset < end) { 160 if (rangeColor != currentColor) { 161 vb.setPerVertexColor(rangeColor, 1.0f); 162 currentColor = rangeColor; 163 } 164 } else { 165 if (textColor != currentColor) { 166 vb.setPerVertexColor(textColor, 1.0f); 167 currentColor = textColor; 168 } 169 } 170 } 171 xform.transform(pt, pt); 172 addDataToQuad(data, vb, tex, pt.x, pt.y, dstw, dsth); 173 } 174 } 175 } 176 177 private void addDataToQuad(GlyphData data, VertexBuffer vb, 178 Texture tex, float x, float y, 179 float dstw, float dsth) { 180 // We are sampling texture using nearest point sampling, for clear 181 // text. As a consequence of nearest point sampling, graphics artifacts 182 // may occur when sampling close to texel boundaries. 183 // By rounding the glyph placement we can avoid the texture boundaries. 184 // REMIND: If we start using linear sampling then we should remove 185 // rounding. 186 y = Math.round(y); 187 Rectangle rect = data.getRect(); 188 if (rect == null) { 189 // Glyph with no visual representation (whitespace) 190 return; 191 } 192 int border = data.getBlankBoundary(); 193 float gw = rect.width - (border * 2); 194 float gh = rect.height - (border * 2); 195 float dx1 = data.getOriginX() + x; 196 float dy1 = data.getOriginY() + y; 197 float dx2; 198 float dy2 = dy1 + gh; 199 float tw = tex.getPhysicalWidth(); 200 float th = tex.getPhysicalHeight(); 201 float tx1 = (rect.x + border) / tw; 202 float ty1 = (rect.y + border) / th; 203 float tx2 = tx1 + (gw / tw); 204 float ty2 = ty1 + (gh / th); 205 if (isLCDCache) { 206 dx1 = Math.round(dx1 * 3.0f) / 3.0f; 207 dx2 = dx1 + gw / 3.0f; 208 float t2x1 = dx1 / dstw; 209 float t2x2 = dx2 / dstw; 210 float t2y1 = dy1 / dsth; 211 float t2y2 = dy2 / dsth; 212 vb.addQuad(dx1, dy1, dx2, dy2, tx1, ty1, tx2, ty2, t2x1, t2y1, t2x2, t2y2); 213 } else { 214 dx1 = Math.round(dx1); 215 dx2 = dx1 + gw; 216 if (context.isSuperShaderEnabled()) { 217 vb.addSuperQuad(dx1, dy1, dx2, dy2, tx1, ty1, tx2, ty2, true); 218 } else { 219 vb.addQuad(dx1, dy1, dx2, dy2, tx1, ty1, tx2, ty2); 220 } 221 } 222 } 223 224 public Texture getBackingStore() { 225 return packer.getBackingStore(); 226 } 227 228 public void clear() { 229 glyphDataMap.clear(); 230 } 231 232 private void clearAll() { 233 // flush any pending vertices that may depend on the current state 234 // of the glyph cache texture. 235 context.flushVertexBuffer(); 236 context.clearGlyphCaches(); 237 packer.clear(); 238 } 239 240 private GlyphData getCachedGlyph(int glyphCode, int subPixel) { 241 int segIndex = glyphCode >> SEGSHIFT; 242 int subIndex = glyphCode % SEGSIZE; 243 segIndex |= (subPixel << SUBPIXEL_SHIFT); 244 GlyphData[] segment = glyphDataMap.get(segIndex); 245 if (segment != null) { 246 if (segment[subIndex] != null) { 247 return segment[subIndex]; 248 } 249 } else { 250 segment = new GlyphData[SEGSIZE]; 251 glyphDataMap.put(segIndex, segment); 252 } 253 254 // Render the glyph and insert it in the cache 255 GlyphData data = null; 256 Glyph glyph = strike.getGlyph(glyphCode); 257 if (glyph != null) { 258 byte[] glyphImage = glyph.getPixelData(subPixel); 259 if (glyphImage == null || glyphImage.length == 0) { 260 data = new GlyphData(0, 0, 0, 261 glyph.getPixelXAdvance(), 262 glyph.getPixelYAdvance(), 263 null); 264 } else { 265 // Rasterize the glyph 266 // NOTE : if the MaskData can be stored back directly 267 // in the glyph, even as an opaque type, it should save 268 // repeated work next time the glyph is used. 269 MaskData maskData = MaskData.create(glyphImage, 270 glyph.getOriginX(), 271 glyph.getOriginY(), 272 glyph.getWidth(), 273 glyph.getHeight()); 274 275 // Make room for the rectangle on the backing store 276 int border = 1; 277 int rectW = maskData.getWidth() + (2 * border); 278 int rectH = maskData.getHeight() + (2 * border); 279 int originX = maskData.getOriginX(); 280 int originY = maskData.getOriginY(); 281 Rectangle rect = new Rectangle(0, 0, rectW, rectH); 282 data = new GlyphData(originX, originY, border, 283 glyph.getPixelXAdvance(), 284 glyph.getPixelYAdvance(), 285 rect); 286 287 if (!packer.add(rect)) { 288 if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("Font Glyph Cache Cleared"); 289 // If add fails,clear up the cache. Try add again. 290 clearAll(); 291 packer.add(rect); 292 } 293 294 // We always pass skipFlush=true to backingStore.update() 295 // since we are in control of the contents of the backingStore 296 // texture and explicitly flush the vertex buffer only when 297 // it is truly needed. 298 boolean skipFlush = true; 299 300 // Upload the an empty byte array to ensure the boundary 301 // area is filled with zeros. Note that the rectangle 302 // is already padded on each edge. 303 Texture backingStore = getBackingStore(); 304 int emw = rect.width; 305 int emh = rect.height; 306 int bpp = backingStore.getPixelFormat().getBytesPerPixelUnit(); 307 int stride = emw * bpp; 308 int size = stride * emh; 309 if (emptyMask == null || size > emptyMask.capacity()) { 310 emptyMask = BufferUtil.newByteBuffer(size); 311 } 312 // try/catch is a precaution against not fitting into the store. 313 try { 314 backingStore.update(emptyMask, 315 backingStore.getPixelFormat(), 316 rect.x, rect.y, 317 0, 0, emw, emh, stride, 318 skipFlush); 319 } catch (Exception e) { 320 e.printStackTrace(); 321 return null; 322 } 323 // Upload the glyph 324 maskData.uploadToTexture(backingStore, 325 border + rect.x, 326 border + rect.y, 327 skipFlush); 328 329 } 330 segment[subIndex] = data; 331 } 332 333 return data; 334 } 335 336 static class GlyphData { 337 // The following must be defined and used VERY precisely. This is 338 // the offset from the upper-left corner of this rectangle (Java 339 // 2D coordinate system) at which the string must be rasterized in 340 // order to fit within the rectangle -- the leftmost point of the 341 // baseline. 342 private final int originX; 343 private final int originY; 344 345 // The blank boundary around the real image of the glyph on 346 // the backing store 347 private final int blankBoundary; 348 349 // The advance of this glyph 350 private final float xAdvance, yAdvance; 351 352 // The rectangle on the backing store corresponding to this glyph 353 private final Rectangle rect; 354 355 GlyphData(int originX, int originY, int blankBoundary, 356 float xAdvance, float yAdvance, Rectangle rect) 357 { 358 this.originX = originX; 359 this.originY = originY; 360 this.blankBoundary = blankBoundary; 361 this.xAdvance = xAdvance; 362 this.yAdvance = yAdvance; 363 this.rect = rect; 364 } 365 366 int getOriginX() { 367 return originX; 368 } 369 370 int getOriginY() { 371 return originY; 372 } 373 374 int getBlankBoundary() { 375 return blankBoundary; 376 } 377 378 float getXAdvance() { 379 return xAdvance; 380 } 381 382 float getYAdvance() { 383 return yAdvance; 384 } 385 386 Rectangle getRect() { 387 return rect; 388 } 389 } 390 }