001package polaris.sdk.ui.render; 002import polaris.sdk.ui.*; 003import java.nio.*; 004/** Collects clipped drawing commands and submits them with one JNI call per UI frame. */ 005public final class NativePainter implements Painter,AutoCloseable { 006 public static final int STRIDE=64;private final NativeBackend backend;private final GlyphAtlas atlas; 007 private FloatBuffer commands=allocate(8192);private int count;private Rect clip=new Rect(0,0,1,1);private float uiWidth,uiHeight;private boolean closed;private Theme settings=Theme.dark();private CornerRadii cornerProfile;private float dpiX=1,dpiY=1;private int shapeDepth; 008 public NativePainter(NativeBackend backend){if(backend==null)throw new NullPointerException("backend");this.backend=backend;backend.check();atlas=new GlyphAtlas(backend);} 009 private static FloatBuffer allocate(int count){return ByteBuffer.allocateDirect(count*STRIDE*4).order(ByteOrder.nativeOrder()).asFloatBuffer();} 010 /** Supply physical dimensions before text layout to avoid a first-frame DPI mismatch. */ 011 public void begin(float width,float height,int pixelWidth,int pixelHeight){ 012 if(pixelWidth<=0||pixelHeight<=0||width<=0||height<=0)throw new IllegalArgumentException("viewport"); 013 dpiX=pixelWidth/width;dpiY=pixelHeight/height;begin(width,height); 014 } 015 @Override public void configure(Theme theme){backend.check();if(closed)throw new IllegalStateException("Painter closed");if(theme==null)throw new NullPointerException("theme");if(theme.antialiasing==null||theme.textMode==null)throw new IllegalArgumentException("Missing render quality/text mode");if(!Float.isFinite(theme.textContrast)||theme.textContrast<.5f||theme.textContrast>2||!Float.isFinite(theme.cornerSmoothing)||theme.cornerSmoothing<0||theme.cornerSmoothing>1||!Float.isFinite(theme.letterSpacing))throw new IllegalArgumentException("Invalid global render settings");settings=theme;atlas.configure(theme);} 016 @Override public void corners(CornerRadii profile){cornerProfile=profile;} 017 public void begin(float width,float height){backend.check();if(closed)throw new IllegalStateException("Painter closed");if(width<=0||height<=0)throw new IllegalArgumentException("viewport");count=0;shapeDepth=0;commands.clear();cornerProfile=null;uiWidth=width;uiHeight=height;clip=new Rect(0,0,width,height);} 018 public void flush(int framebuffer,int colorAttachment,int pw,int ph,float sigma,float mx,float my){if(shapeDepth!=0)throw new IllegalStateException("Unbalanced shaped clips: "+shapeDepth);if(pw<=0||ph<=0)return;dpiX=pw/uiWidth;dpiY=ph/uiHeight;commands.position(0);backend.submit(commands,count,framebuffer,colorAttachment,pw,ph,uiWidth,uiHeight,sigma,mx,my);} 019 public void flushWindow(int width,int height,float sigma,float mx,float my){flush(0,0x0405,width,height,sigma,mx,my);} 020 public int commandCount(){return count;}public int glyphCount(){return atlas.size();}public int sdfGlyphCount(){return atlas.sdfCount();}public String fontFamily(){return atlas.family();} 021 @Override public void clip(Rect r){if(r==null)throw new NullPointerException("clip");clip=r;} 022 @Override public void pushShapeClip(Rect bounds,float radius,CornerRadii profile){ 023 if(bounds==null)throw new NullPointerException("bounds"); 024 if(!Float.isFinite(bounds.x)||!Float.isFinite(bounds.y)||!Float.isFinite(bounds.w)||!Float.isFinite(bounds.h)||!Float.isFinite(radius)||bounds.w<0||bounds.h<0)throw new IllegalArgumentException("Invalid shaped clip"); 025 if(shapeDepth>=32)throw new IllegalStateException("Maximum shaped-clip nesting is 32"); 026 int off=command(10,bounds.x,bounds.y,bounds.w,bounds.h,0xffffffff); 027 CornerRadii previous=cornerProfile;cornerProfile=profile; 028 try{commands.put(off+5,radius);rounding(off,radius,settings);}finally{cornerProfile=previous;} 029 shapeDepth++; 030 } 031 @Override public void popShapeClip(){if(shapeDepth<=0)throw new IllegalStateException("Shaped clip stack underflow");command(11,0,0,0,0,0xffffffff);shapeDepth--;} 032 private int command(int kind,float x,float y,float w,float h,int color){boolean stateCommand=kind==9||kind==10||kind==11;if(!stateCommand&&(clip.empty()||(color>>>24)==0))return -1;if(!stateCommand&&kind!=4&&new Rect(x,y,w,h).intersect(clip).empty())return -1; 033 if(count>=65536)throw new IllegalStateException("UI draw command limit exceeded (65,536)");if((count+1)*STRIDE>commands.capacity()){FloatBuffer larger=allocate(Math.min(65536,commands.capacity()/STRIDE*2));commands.position(0);larger.put(commands);commands=larger;} 034 int off=(count++)*STRIDE;for(int i=0;i<STRIDE;i++)commands.put(off+i,0);commands.put(off,kind);commands.put(off+1,x);commands.put(off+2,y);commands.put(off+3,w);commands.put(off+4,h);commands.put(off+6,((color>>16)&255)/255f);commands.put(off+7,((color>>8)&255)/255f);commands.put(off+8,(color&255)/255f);commands.put(off+9,(color>>>24)/255f);commands.put(off+18,clip.x);commands.put(off+19,clip.y);commands.put(off+20,clip.w);commands.put(off+21,clip.h);commands.put(off+27,settings.antialiasing.ordinal());commands.put(off+45,settings.textContrast);return off; 035 } 036 @Override public void glass(float x,float y,float w,float h,float radius,Theme t,float mix){int off=command(0,x,y,w,h,(Math.round(MotionValue.clamp(t.surfaceOpacity)*255)<<24)|(t.surface&0xffffff));if(off>=0){commands.put(off+5,radius);commands.put(off+22,mix);commands.put(off+23,t.refraction);commands.put(off+24,t.shine);commands.put(off+25,t.tint);commands.put(off+26,t.dispersion);commands.put(off+27,t.antialiasing.ordinal());rounding(off,radius,t);color(off+36,t.highlight);color(off+40,t.shadow);}} 037 @Override public void fill(float x,float y,float w,float h,float radius,int color){if(w<=0||h<=0)return;int off=command(1,x,y,w,h,color);if(off>=0){commands.put(off+5,radius);rounding(off,radius,settings);}} 038 @Override public void stroke(float x,float y,float w,float h,float radius,float thickness,int color){if(w<=0||h<=0)return;int off=command(1,x,y,w,h,color);if(off>=0){commands.put(off+5,radius);commands.put(off+10,thickness);rounding(off,radius,settings);}} 039 @Override public void line(float x,float y,float x2,float y2,float thickness,int color){int off=command(4,x,y,x2-x,y2-y,color);if(off>=0)commands.put(off+10,thickness);} 040 @Override public void ring(float x,float y,float size,float thickness,float progress,float start,int color){int off=command(3,x,y,size,size,color);if(off>=0){commands.put(off+10,thickness);commands.put(off+11,progress);commands.put(off+12,start);}} 041 @Override public void colorPlane(float x,float y,float w,float h,float hue){colorPlaneAlpha(x,y,w,h,hue,1);} 042 @Override public void colorPlaneAlpha(float x,float y,float w,float h,float hue,float alpha){int off=command(5,x,y,w,h,Theme.alpha(0xffffffff,alpha));if(off>=0)commands.put(off+11,hue);} 043 044 @Override public void text(String text,float x,float y,float size,int color){textWeightScaled(text,x,y,size,color,400,1);} 045 @Override public void textScaled(String text,float x,float y,float size,int color,float scale){textWeightScaled(text,x,y,size,color,400,scale);} 046 @Override public void textWeight(String text,float x,float y,float size,int color,int weight){textWeightScaled(text,x,y,size,color,weight,1);} 047 private void color(int off,int c){commands.put(off,((c>>16)&255)/255f);commands.put(off+1,((c>>8)&255)/255f);commands.put(off+2,(c&255)/255f);commands.put(off+3,(c>>>24)/255f);} 048 private void rounding(int off,float radius,Theme theme){ 049 float minSize=Math.min(commands.get(off+3),commands.get(off+4)); 050 // Circular thumbs/avatars and capsules remain circular unless explicitly overridden. 051 boolean cubic=theme.bezierCorners&&radius>0&&radius<minSize*.5f-.001f; 052 if(cornerProfile!=null){ 053 for(Corner corner:Corner.values()){BezierCorner q=cornerProfile.get(corner);commands.put(off+28+corner.ordinal(),q.radius);commands.put(off+32+corner.ordinal(),q.handle());} 054 cubic=true; 055 }else{float r=radius<0?minSize/2:radius;for(int i=0;i<4;i++){commands.put(off+28+i,r);commands.put(off+32+i,.55228475f+.44771525f*theme.cornerSmoothing);}} 056 commands.put(off+44,cubic?1:0); 057 } 058 private GlyphAtlas.Glyph layoutGlyph(int cp,float size,int weight){ 059 boolean sdf=settings.textMode==TextMode.SDF||settings.textMode==TextMode.AUTO&&size*dpiY>24; 060 return atlas.get(cp,size,weight,dpiY,sdf,settings.antialiasing!=Antialiasing.OFF); 061 } 062 @Override public void textWeightScaled(String text,float x,float y,float size,int color,int weight,float scale){ 063 if(text==null||text.isEmpty()||size<=0||scale<=0)return; 064 // Snap the run origin/baseline first so fractional advances do not make different letters 065 // jump at different pointer subpixels. Animation uses continuous SDF positions instead. 066 if(settings.textPixelSnap && Math.abs(scale-1)<.001f && (settings.textMode==TextMode.COVERAGE || settings.textMode==TextMode.AUTO&&size*dpiY<=24)){ 067 x=Math.round(x*dpiX)/dpiX;y=Math.round((y+size*.84f)*dpiY)/dpiY-size*.84f; 068 } 069 float xx=x,yy=y; 070 boolean moving=Math.abs(scale-1)>.018f; 071 for(int i=0;i<text.length();){int cp=text.codePointAt(i);i+=Character.charCount(cp);if(cp=='\n'){xx=x;yy+=(size+5)*scale;continue;} 072 GlyphAtlas.Glyph layout=layoutGlyph(cp,size,weight),glyph=layout; 073 if(settings.textMode==TextMode.AUTO){ 074 // Warm the alternate cache once, so press/release never rasterizes at each animation scale. 075 GlyphAtlas.Glyph scalable=atlas.get(cp,size,weight,dpiY,true,true); 076 if(moving||size*dpiY>24)glyph=scalable; 077 } 078 float gw=glyph.w/glyph.emPixels*size*scale,gh=glyph.h/glyph.emPixels*size*scale; 079 float gx=xx+glyph.offsetXEm*size*scale,gy=yy+(size*.84f+glyph.offsetYEm*size)*scale; 080 if(settings.textPixelSnap&&!glyph.sdf&&Math.abs(scale-1)<.001f){gx=Math.round(gx*dpiX)/dpiX;gy=Math.round(gy*dpiY)/dpiY;} 081 int off=command(2,gx,gy,gw,gh,color); 082 if(off>=0){ 083 commands.put(off+11,glyph.sdf?1:0);commands.put(off+12,glyph.range); 084 commands.put(off+13,glyph.x/(float)GlyphAtlas.SIZE);commands.put(off+14,glyph.y/(float)GlyphAtlas.SIZE); 085 commands.put(off+15,glyph.w/(float)GlyphAtlas.SIZE);commands.put(off+16,glyph.h/(float)GlyphAtlas.SIZE); 086 commands.put(off+17,Float.intBitsToFloat(glyph.texture)); 087 } 088 xx+=(layout.advanceEm*size+(i<text.length()&&text.charAt(i)!='\n'?settings.letterSpacing:0))*scale; 089 } 090 } 091 @Override public float textWidth(String text,float size){return textWidth(text,size,400);} 092 @Override public float textWidth(String text,float size,int weight){ 093 if(text==null||text.isEmpty())return 0;float current=0,longest=0; 094 for(int i=0;i<text.length();){int cp=text.codePointAt(i);i+=Character.charCount(cp);if(cp=='\n'){longest=Math.max(longest,current);current=0;}else current+=layoutGlyph(cp,size,weight).advanceEm*size+(i<text.length()&&text.charAt(i)!='\n'?settings.letterSpacing:0);} 095 return Math.max(longest,current); 096 } 097 @Override public void backdrop(){command(9,0,0,0,0,0xffffffff);} 098 @Override public void close(){if(closed)return;atlas.close();closed=true;} 099}