{"id":20971,"date":"2015-03-30T08:18:59","date_gmt":"2015-03-30T07:18:59","guid":{"rendered":"https:\/\/www.inovex.de\/\/?p=246"},"modified":"2015-03-30T08:18:59","modified_gmt":"2015-03-30T07:18:59","slug":"android-graphics-pipeline-from-button-to-framebuffer-part-2","status":"publish","type":"post","link":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/","title":{"rendered":"Android Graphics Pipeline: From Button to Framebuffer (Part 2)"},"content":{"rendered":"<p><a href=\"https:\/\/www.inovex.de\/\/android-graphics-pipeline-from-button-to-framebuffer-part-1\/\" target=\"_blank\" rel=\"noopener\">Last time<\/a>, we took a thorough look at how Android converts the Java-Side <span class=\"lang:default decode:true crayon-inline \">onDraw()<\/span>\u00a0method into a native display list on the C++-Side. This time we will go further along the Android Graphics Pipeline and take a look at how Android is drawing these display lists to the screen. We are now leaving the comfortable realm of garbage-collected Java and entering the dark and scary dungeon which is called C++. But don\u2019t worry, we\u2019ll keep it quite simple and only show relevant and interesting code bits.<!--more--><\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_82_2 counter-hierarchy ez-toc-counter ez-toc-custom ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\"><p class=\"ez-toc-title\" style=\"cursor:inherit\"><\/p>\n<\/div><nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Drawing-the-display-list\" >Drawing the display list<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Reordering-and-merging-of-operations\" >Reordering and merging of operations<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Actually-drawing-the-deferred-display-list\" >Actually drawing the (deferred) display list<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Display-List-Operations\" >Display List Operations<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Texture-Atlas\" >Texture Atlas<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Font-caching-and-rendering\" >Font caching and rendering<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#OpenGL\" >OpenGL<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Conclusion\" >Conclusion<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Download\" >Download<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Full-Listings\" >Full Listings<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Display-List\" >Display List<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#OpenGL-2\" >OpenGL<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#Get-in-touch\" >Get in touch<\/a><\/li><\/ul><\/nav><\/div>\n<h2 id=\"drawing-the-display-list\"><span class=\"ez-toc-section\" id=\"Drawing-the-display-list\"><\/span>Drawing the display list<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Before Android 4.3, rendering operations of the UI were executed in the same order the UI elements are added to the view hierarchy and therefore added to the resulting display list. This can result in the worst case scenario for GPUs, as they must switch state for every element. For example, when drawing two buttons, the GPU needs to draw the <span class=\"lang:java decode:true crayon-inline\">NinePatch<\/span>\u00a0and text for the first button, and then the same for the second button, resulting in at least 3 state changes.<\/p>\n<h3 id=\"reordering-and-merging-of-operations\"><span class=\"ez-toc-section\" id=\"Reordering-and-merging-of-operations\"><\/span>Reordering and merging of operations<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>So in order to minimize the expansive state changes, Android is reordering all drawing operations based on their type and state attributes. We are leaving our example application with only one button for a moment and are now looking at a fully arbitrary activity:<\/p>\n<figure id=\"attachment_247\" aria-describedby=\"caption-attachment-247\" style=\"width: 300px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/merging-layout-anot.png\" target=\"_blank\" rel=\"noopener\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-247 size-medium\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/merging-layout-anot-300x215.png\" alt=\"Merging Layout\" width=\"300\" height=\"215\" data-wp-pid=\"247\" \/><\/a><figcaption id=\"caption-attachment-247\" class=\"wp-caption-text\">Example Activity with overlapping elements, carefully chosen to illustrate possible problems when reordering drawing operations.<\/figcaption><\/figure>\n<p>As seen in image above, a simple approach to reordering and merging by type is not sufficient in most cases. Drawing all text elements and then the bitmap (or the other way around) does not result in the same final image as it would without reordering, which is clearly not acceptable.<\/p>\n<p>In order to correctly render the example activity, text elements A\u00a0and B\u00a0have to be drawn first, followed by the bitmap C, followed by the text element D. The first two text elements could be merged into one operation, but the text element D\u00a0cannot, as it would be overlapped by the bitmap.<\/p>\n<p>To further reduce the drawing time needed for a view hierarchy, most operations can be merged after they have been reordered. This happens in the <span class=\"lang:java decode:true crayon-inline\">DeferredDisplayList<\/span>, so-called because the execution of the drawing operations does not happen in order, but is deferred until all operations have been analyzed, reordered and merged.<\/p>\n<p>Because every display list operation is responsible for drawing itself, an operation that supports the merging of multiple operations with the same type must be able to draw multiple, different operations in one draw call. Not every operation is capable of merging, so some can only be reordered.<\/p>\n<p><a href=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/canvas-drawdisplaylist.png\" target=\"_blank\" rel=\"noopener\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-248 size-large\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/canvas-drawdisplaylist-1024x594.png\" alt=\"Canvas Drawdisplaylist\" width=\"800\" height=\"464\" data-wp-pid=\"248\" \/><\/a><\/p>\n<p>The <span class=\"lang:java decode:true crayon-inline \">OpenGLRenderer<\/span>\u00a0is an implementation of the <span class=\"lang:java decode:true crayon-inline\">Skia 2D<\/span>\u00a0drawing API, but instead of utilizing the CPU it does all the drawing hardware accelerated with OpenGL. On the way trough the pipeline, this is the first native-only class implemented in C++. The renderer is designed to be used with the <span class=\"lang:java decode:true crayon-inline \">GLES20Canvas<\/span>\u00a0and was introduced with Android 3.0. It is only used in conjunction with display lists.<\/p>\n<p>To merge multiple operations to one draw call, each operation is added to the deferred display list by calling <span class=\"lang:java decode:true crayon-inline\">addDrawOp(DrawOp)<\/span>. The drawing operation is asked to supply the\u00a0<span class=\"lang:java decode:true crayon-inline\">batchId<\/span>, which indicates the type of the operation it can be merged with, and the <span class=\"lang:java decode:true crayon-inline\">mergeIdwhich<\/span>\u00a0indicates the merged operations, by calling <span class=\"lang:java decode:true crayon-inline\">DrawOp.onDefer(&#8230;)<\/span>.<\/p>\n<p>Possible <span class=\"lang:java decode:true crayon-inline \">batchId<\/span>s\u00a0include <span class=\"lang:java decode:true crayon-inline\">OpBatch_Patch<\/span>\u00a0for a 9-Patch and <span class=\"lang:java decode:true crayon-inline \">OpBatch_Text<\/span>\u00a0for a normal text element. These are defined in a simple enum. The <span class=\"lang:java decode:true crayon-inline\">mergeId<\/span>\u00a0is determined by each <span class=\"lang:java decode:true crayon-inline\">DrawOpitself<\/span>, and is used to decide if two operations of the same <span class=\"lang:java decode:true crayon-inline\">DrawOp<\/span>\u00a0type can be merged. For a 9-Patch, the <span class=\"lang:java decode:true crayon-inline\">mergeId<\/span>\u00a0is a pointer to the asset atlas (or bitmap), for a text element it is the paint color. Multiple drawables from the same asset atlas texture can potentially be merged into one batch, resulting in a greatly reduced rendering time.<\/p>\n<p>All information about an operation is collected into a simple struct:<\/p>\n<pre class=\"lang:java decode:true \">struct DeferInfo {\n\n    \/\/ Type of operation (TextView, Button, etc.)\n\n    int batchId;\n\n    \/\/ State of operation (Text size, font, color, etc.)\n\n    mergeid_t mergeId;\n\n    \/\/ Indicates if operation is mergable\n\n    bool mergeable;\n\n};<\/pre>\n<p>After the <span class=\"lang:java decode:true crayon-inline \">batchId<\/span>\u00a0and <span class=\"lang:java decode:true crayon-inline \">mergeId<\/span>\u00a0\u00a0of an operation are determined, it will be added to the last batch if it is not mergeable. If no batch is already available, a new batch will be created. The more likely case is that the operation is mergeable. To keep track of all recently merged batches, a hashmap for each <span class=\"lang:java decode:true crayon-inline \">batchId<\/span>\u00a0is used which is called <span class=\"lang:java decode:true crayon-inline \">MergeBatches<\/span>\u00a0in the simplified algorithm. Using one hashmap for each batch avoids the need to resolve collisions with the\u00a0<span class=\"lang:java decode:true crayon-inline\">mergeId<\/span>.<\/p>\n<pre class=\"lang:java decode:true\">vector&lt;DrawBatch&gt; batches;\n\nHashmap&lt;MergeId, DrawBatch*&gt; mergingBatches[BatchTypeCount];\n\nvoid DeferredDisplayList::addDrawOp(DrawOp op):\n\n    DeferInfo info;\n\n    \/* DrawOp fills DeferInfo with its mergeId and batchId *\/\n\n    op.onDefer(info);\n\n    if(\/* op is not mergeable *\/):\n\n        \/* Add Op to last added Batch with same batchId, if first\n\n           op then create a new Batch *\/\n\n        return;\n\n    DrawBatch batch = NULL;\n\n    if(batches.isEmpty() == false):\n\n        batch = mergingBatches[info.batchId].get(info.mergeId);\n\n        if(batch != NULL &amp;&amp; \/* Op can merge with batch *\/):\n\n            batch.add(op);\n\n            mergingBatches[info.batchId].put(info.mergeId, batch);\n\n            return;\n\n        \/* Op can not merge with batch due to different states,\n\n           flags or bounds *\/\n\n        int newBatchIndex = batches.size();\n\n        for(overBatch in batches.reverse()):\n\n            if (overBatch == batch):\n\n                \/* No intersection as we found our own batch *\/\n\n                break;\n\n            if(overBatch.batchId  == info.batchId):\n\n                \/* Save position of similar batches to insert\n\n                   after (reordering) *\/\n\n                newBatchIndex == iterationIndex;\n\n            if(overBatch.intersects(localBounds)):\n\n                \/* We can not merge due to intersection *\/\n\n                batch = NULL\n\n                break;\n\n    if(batch == NULL):\n\n        \/* Create new Batch and add to mergingBatches *\/\n\n        batch = new DrawBatch(...);\n\n        mergingBatches[deferInfo.batchId].put(info.mergeId, batch);\n\n        batches.insertAt(newBatchIndex, batch);\n\n    batch.add(op);<\/pre>\n<p>If the current operation can be merged with another operation of the same <span class=\"lang:java decode:true crayon-inline \">mergeId<\/span>\u00a0\u00a0and\u00a0<span class=\"lang:java decode:true crayon-inline\">batchId<\/span>, the operation is added to the existing batch and the next operation can be added. But if it cannot be merged due to different states, drawing flags or bounding boxes, the algorithm needs to insert a new merging batch. For this to happen, the position inside the list of all batches (<span class=\"lang:java decode:true crayon-inline\">Batches<\/span>) needs to be found. In the best case, it would find a batch that shares the same state with the current drawing operation. But it is also essential that the operation does not intersect with any other batches in the process of finding a correct spot. Therefore, the list of all batches is iterated over in reverse order to find a good position and to check for intersections with other elements. In case of an intersection, the operation cannot be merged and a new <span class=\"lang:java decode:true crayon-inline\">DrawBatch<\/span>\u00a0is created and inserted into the <span class=\"lang:java decode:true crayon-inline\">MergeBatcheshashmap<\/span>. The new batch is added to <span class=\"lang:java decode:true crayon-inline \">Batches<\/span>\u00a0at the position found earlier. In any case, the operation is added to the current batch, which can be a new or an existing batch.<\/p>\n<p>The actual implementation is more complex than the simplified version presented here. There are a few optimizations worth being mentioned. The algorithm is tries to avoid overdraw by removing occluded drawing operations, and also tries to to reorder non-mergeable operations to avoid GPU state changes.<\/p>\n<h2 id=\"actually-drawing-the-deferred-display-list\"><span class=\"ez-toc-section\" id=\"Actually-drawing-the-deferred-display-list\"><\/span>Actually drawing the (deferred) display list<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>After reordering and merging the new deferred display list can finally be drawn to the screen.<\/p>\n<p><a href=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/ddl-flush.png\" target=\"_blank\" rel=\"noopener\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-249 size-full\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/ddl-flush.png\" alt=\"DDL Flush\" width=\"851\" height=\"666\" data-wp-pid=\"249\" \/><\/a><\/p>\n<p>&nbsp;<\/p>\n<p>Inside the\u00a0<span class=\"lang:java decode:true crayon-inline\">OpenGLRenderers::drawDisplayList(\u2026)<\/span>\u00a0method, the deferred display list is created filled with operations from the normal display list. The deferred display list is then asked to draw itself (<span class=\"lang:java decode:true crayon-inline\">flush(\u2026)<\/span>).<\/p>\n<pre class=\"lang:java decode:true\" title=\"OpenGLRenderer: drawDisplayList(\u2026)\">status_t OpenGLRenderer::drawDisplayList(\n\n               DisplayList* displayList, Rect&amp; dirty,\n\n               int32_t replayFlags) {\n\n    \/\/ All the usual checks and setup operations\n\n    \/\/ (quickReject, setupDraw, etc.)\n\n    \/\/ will be performed by the display list itself\n\n    if (displayList &amp;&amp; displayList-&gt;isRenderable()) {\n\n        DeferredDisplayList deferredList(*(mSnapshot-&gt;clipRect));\n\n        DeferStateStruct deferStruct(\n\n            deferredList, *this, replayFlags);\n\n        displayList-&gt;defer(deferStruct, 0);\n\n        return deferredList.flush(*this, dirty);\n\n    }\n\n    return DrawGlInfo::kStatusDone;\n\n}<\/pre>\n<p>The method <span class=\"lang:java decode:true crayon-inline \">multiDraw(\u2026)<\/span>\u00a0will be called on the first operation in that list, with all the other operations as an argument. The called operation is responsible for drawing all supplied operations at once and will also call the <span class=\"lang:java decode:true crayon-inline \">OpenGLRenderer<\/span>\u00a0\u00a0to actually execute the operation itself.<\/p>\n<h3 id=\"display-list-operations\"><span class=\"ez-toc-section\" id=\"Display-List-Operations\"><\/span><a class=\"headeranchor-link\" href=\"#display-list-operations\" name=\"user-content-display-list-operations\"><\/a>Display List Operations<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Each drawing operation to be executed on a canvas has a corresponding display list operation. All display list operations must implement the <span class=\"lang:java decode:true crayon-inline\">replay()<\/span>\u00a0method, which executes the wrapped drawing operation. These drawing operations call the <code>OpenGLRenderer<\/code> to render themselves. The reference to the renderer needs to be supplied when creating an operation. <span class=\"lang:java decode:true crayon-inline\">onDefer()<\/span>\u00a0must also be implemented and must return the operation\u2019s <span class=\"lang:java decode:true crayon-inline\">drawId<\/span>\u00a0and <span class=\"lang:java decode:true crayon-inline\">mergeId.<\/span>\u00a0Non-mergable batches are setting the draw id to <span class=\"lang:java decode:true crayon-inline\">kOpBatch_None<\/span>. Mergable operations must implement the <span class=\"lang:java decode:true crayon-inline\">multiDraw()<\/span>\u00a0method, which is used when a whole batch of merged operations need to be rendered at once.<\/p>\n<p>For example, the operation to draw a 9-Patch (called <span class=\"lang:java decode:true crayon-inline\">DrawPatchOp<\/span>) contains the following\u00a0<span class=\"lang:java decode:true crayon-inline\">multiDraw(\u2026)<\/span>\u00a0\u00a0implementation:<\/p>\n<pre class=\"lang:java decode:true \" title=\"DrawPatchOp::multiDraw(\u2026)\">virtual status_t multiDraw(OpenGLRenderer&amp; renderer, Rect&amp; dirty,\n\n        const Vector&lt;OpStatePair&gt;&amp; ops, const Rect&amp; bounds) {\n\n    \/\/ Merge all 9-Patche vertices and texture coordinates\n\n    \/\/ into one big vector\n\n    Vector&lt;TextureVertex&gt; vertices;\n\n    for (unsigned int i = 0; i &lt; ops.size(); i++) {\n\n        DrawPatchOp* patchOp = (DrawPatchOp*) ops[i].op;\n\n        const Patch* opMesh = patchOp-&gt;getMesh(renderer);\n\n        TextureVertex* opVertices = opMesh-&gt;vertices;\n\n        for (uint32_t j = 0; j &lt; opMesh-&gt;verticesCount;\n\n             j++, opVertices++) {\n\n            vertices.add(TextureVertex(opVertices-&gt;position[0],\n\n                                       opVertices-&gt;position[1],\n\n                                       opVertices-&gt;texture[0],\n\n                                       opVertices-&gt;texture[1]));\n\n        }\n\n    }\n\n    \/\/ Tell the renderer to draw multipe textured polygons\n\n    return renderer.drawPatches(mBitmap, getAtlasEntry(),\n\n                        &amp;vertices[0], getPaint(renderer));\n\n}<\/pre>\n<p>&nbsp;<\/p>\n<p>The <span class=\"lang:java decode:true crayon-inline \">batchId<\/span>\u00a0of a 9-Patch is always <span class=\"lang:java decode:true crayon-inline\">kOpBatch_Patch<\/span>, the <span class=\"lang:java decode:true crayon-inline \">mergeId<\/span>\u00a0is a pointer to the used bitmap. Therefore, all patches that use the same bitmap can be merged together. This is even more important with the use of the asset atlas, as now all heavily used 9-Patches from the Android framework can potentially be merged together as the reside on the same texture.<\/p>\n<h2 id=\"texture-atlas\"><span class=\"ez-toc-section\" id=\"Texture-Atlas\"><\/span><a class=\"headeranchor-link\" href=\"#texture-atlas\" name=\"user-content-texture-atlas\"><\/a>Texture Atlas<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The Android start-up process <span class=\"lang:java decode:true crayon-inline \">zygote<\/span>\u00a0always keeps a number of assets preloaded which are shared with all processes. These assets are containing frequently used 9-Patches and images for the standard Android framework widgets. But up until Android 4.4, every process was keeping a seperate copy of these assets on the GPU memory. Starting with Android 4.4 KitKat, these frequently used assets are now packed into a texture atlas, uploaded to the GPU and shared between all processes. Only then is merging of 9-Patches and other drawables from the standard framework possible.<\/p>\n<figure id=\"attachment_250\" aria-describedby=\"caption-attachment-250\" style=\"width: 864px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/atlas.png\" target=\"_blank\" rel=\"noopener\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-250 size-full\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/atlas.png\" alt=\"The texture atlas generated by the system to reduce GPU stress caused by switching textures too often.\" width=\"864\" height=\"955\" data-wp-pid=\"250\" \/><\/a><figcaption id=\"caption-attachment-250\" class=\"wp-caption-text\">The texture atlas generated by the system to reduce GPU stress caused by switching textures too often.<\/figcaption><\/figure>\n<p>The image above shows an asset atlas texture generated on a Nexus 7 (2013) running Android 4.4, which contains all frequently used framework assets. If you look closely, the 9-Patches do not feature the typical borders which indicate the layout and padding areas. The original asset files are still used to parse these areas on system start, but they are not used for rendering any longer.<\/p>\n<p>When booting a system the first time after an Android update (or ever), the\u00a0<span class=\"lang:java decode:true crayon-inline \">AssetAtlasService<\/span>\u00a0is regenerating the texture atlas. This atlas is then used for all subsequent reboots, until a new Android update is applied.<\/p>\n<p>To generate the atlas, the service brute-forces trough all possible atlas configurations and looks for the best one. The best configuration is determined by the maximum number of assets on the texture and the minimum texture size, which is then written to\u00a0<span class=\"lang:sh decode:true crayon-inline\">\/data\/system\/framework_atlas.config<\/span>\u00a0and contains the chosen algorithm, dimensions, whether rotations are allowed and whether padding has been added. This configuration is then used in subsequent reboots to regenerate the texture atlas. A <span class=\"lang:sh decode:true crayon-inline \">RGBA8888<\/span>\u00a0graphic buffer is allocated as the asset atlas texture and all assets are rendered onto it via the use of a temporary Skia\u00a0bitmap. This asset atlas texture is valid for the lifetime of the\u00a0<span class=\"lang:java decode:true crayon-inline\">AssetAtlasService<\/span>, only being deallocated when the system itself is shutting down.<\/p>\n<p>To actually pack all assets into the atlas, the service starts with an empty texture. After placing the first asset, the remaining space is divided into two rectangular cells. Depending on the algorithm used, this split can either be horizontal or vertical. The next asset texture is added in the first cell that is large enough to fit. This now occupied cell will be split again and the next asset is processed. The\u00a0<span class=\"lang:java decode:true crayon-inline\">AssetAtlasService<\/span>\u00a0is using multiple threads to speed up the time it takes to iterate through all combinations.<\/p>\n<p>When a new app is started, its <span class=\"lang:java decode:true crayon-inline \">HardwareRenderer<\/span>\u00a0queries the <span class=\"lang:java decode:true crayon-inline\">AssetAtlasService<\/span>\u00a0for this texture and every time the renderer needs to draw a bitmap or 9-Patch it will check the atlas first.<\/p>\n<h2 id=\"font-caching-and-rendering\"><span class=\"ez-toc-section\" id=\"Font-caching-and-rendering\"><\/span><a class=\"headeranchor-link\" href=\"#font-caching-and-rendering\" name=\"user-content-font-caching-and-rendering\"><\/a>Font caching and rendering<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In order to merge text views, a similar approach is used and a font cache is generated. But in contrast to the texture atlas, this font atlas is unique for each app and font type. The color of the font can be applied in a shader and is therefore not considered in the atlas.<\/p>\n<figure id=\"attachment_254\" aria-describedby=\"caption-attachment-254\" style=\"width: 300px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/font.png\" target=\"_blank\" rel=\"noopener\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-254 size-medium\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/font-300x167.png\" alt=\"Left: Font atlas generated by the font renderer. Right: Geometry generated on the CPU, used to render the characters.\" width=\"300\" height=\"167\" data-wp-pid=\"254\" \/><\/a><figcaption id=\"caption-attachment-254\" class=\"wp-caption-text\">Left: Font atlas generated by the font renderer. Right: Geometry generated on the CPU, used to render the characters.<\/figcaption><\/figure>\n<p>If you take a quick glance at the font atlas, you will instantly see that only a few characters are present. When taking a closer look, you will see only the used characters! If you think about how many languages Android supports, and how many characters are supported, only caching the used ones makes perfectly good sense. And because the action bar and the button are using the same font, all characters from both text views can be merged onto one texture.<\/p>\n<p>To draw the font to the screen, the renderer needs to generate a geometry to which the texture gets bound. The geometry is generated on the CPU and then drawn via the OpenGL command <span class=\"lang:java decode:true crayon-inline\">glDrawElements()<\/span>. If the device supports OpenGL ES 3.0, the <span class=\"lang:java decode:true crayon-inline \">FontRenderer<\/span>\u00a0will update and upload the font cache texture asynchronously at the start of the frame, while the GPU is mostly idle, which saves precious milliseconds per frame. The cache texture is implemented as a OpenGL Pixel Buffer Object, which makes a asynchronous upload possible.<\/p>\n<h2 id=\"opengl\"><span class=\"ez-toc-section\" id=\"OpenGL\"><\/span><a class=\"headeranchor-link\" href=\"#opengl\" name=\"user-content-opengl\"><\/a>OpenGL<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>At the start of this mini-series I promised you some raw OpenGL drawing commands. So with no further ado I present you the (not quite complete) OpenGL drawing log for the button of our simple one-button activity:<\/p>\n<pre class=\"lang:sh decode:true\">glUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 0)\n\nglGenBuffers(n = 1, buffers = [3])\n\nglBindBuffer(target = GL_ELEMENT_ARRAY_BUFFER, buffer = 3)\n\nglBufferData(target = GL_ELEMENT_ARRAY_BUFFER, size = 24576, data = [ 24576 bytes ], usage = GL_STATIC_DRAW)\n\nglVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0xbefdcf18)\n\nglVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0xbefdcf20)\n\nglVertexAttribPointerData(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 48)\n\nglVertexAttribPointerData(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 48)\n\nglDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 72, type = GL_UNSIGNED_SHORT, indices = 0x0)\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 2)\n\nglBufferSubData(target = GL_ARRAY_BUFFER, offset = 768, size = 576, data = [ 576 bytes ])\n\nglDisable(cap = GL_BLEND)\n\nglUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 33.0, 0.0, 1.0])\n\nglVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x300)\n\nglVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x308)\n\nglDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 54, type = GL_UNSIGNED_SHORT, indices = 0x0)\n\neglSwapBuffers()<\/pre>\n<p>The complete OpenGL draw call log can be seen at the end of this blog post.<\/p>\n<h2 id=\"conclusion\"><span class=\"ez-toc-section\" id=\"Conclusion\"><\/span><a class=\"headeranchor-link\" href=\"#conclusion\" name=\"user-content-conclusion\"><\/a>Conclusion<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We have seen how Android converts its view hierarchy to a series of render commands inside a display list, reorders and merges these commands and finally how these commands are executed.<\/p>\n<p>Returning to our example activity with one button, the entire view can be rendered in just 5 steps:<\/p>\n<p><a href=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/one-button-all.png\" target=\"_blank\" rel=\"noopener\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-255 size-large\" src=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/one-button-all-1024x180.png\" alt=\"Five Steps\" width=\"800\" height=\"141\" data-wp-pid=\"255\" \/><\/a><\/p>\n<ol>\n<li>The layout draws the background image, which is a linear gradient.<\/li>\n<li>Both the ActionBar and Button background 9-Patches are drawn. These two operations were merged into one batch, as both 9-Patches are located on the same texture.<\/li>\n<li>A linear gradient is drawn for the ActionBar.<\/li>\n<li>Text for the Button and the ActionBar is drawn simultaneously. As these two views use the same font, the font rendere can use the same font texture and therefore merge the two operations.<\/li>\n<li>The application\u2019s icon is drawn.<\/li>\n<\/ol>\n<p>And there you have it, we traced all the way from the view hierarchy to the final OpenGL commands, which concludes this mini-series.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Download\"><\/span>Download<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The full Bachelor&#8217;s Thesis on which this article is based is <a href=\"http:\/\/mathias-garbe.de\/files\/introduction-android-graphics.pdf\" target=\"_blank\" rel=\"noopener\">available for download<\/a>.<\/p>\n<h2 id=\"full-listings\"><span class=\"ez-toc-section\" id=\"Full-Listings\"><\/span>Full Listings<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<h3 id=\"display-list\"><span class=\"ez-toc-section\" id=\"Display-List\"><\/span><a class=\"headeranchor-link\" href=\"#display-list\" name=\"user-content-display-list\"><\/a>Display List<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<pre class=\"lang:sh decode:true\">Start display list (0x5ea4f008, PhoneWindow.DecorView, render=1)\n\n  Save 3\n\n  ClipRect 0.00, 0.00, 720.00, 1184.00\n\n  SetupShader, shader 0x5ea5af08\n\n  Draw Rect    0.00    0.00  720.00 1184.00\n\n  ResetShader\n\n  Draw Display List 0x5ea64d30, flags 0x244053\n\n  Start display list (0x5ea64d30, ActionBarOverlayLayout, render=1)\n\n    Save 3\n\n    ClipRect 0.00, 0.00, 720.00, 1184.00\n\n    Draw Display List 0x5ea5ad78, flags 0x24053\n\n    Start display list (0x5ea5ad78, FrameLayout, render=1)\n\n      Save 3\n\n      Translate (left, top) 0, 146\n\n      ClipRect 0.00, 0.00, 720.00, 1038.00\n\n      Draw Display List 0x5ea59bf8, flags 0x224053\n\n      Start display list (0x5ea59bf8, RelativeLayout, render=1)\n\n        Save 3\n\n        ClipRect 0.00, 0.00, 720.00, 1038.00\n\n        Save flags 3\n\n        ClipRect   32.00   32.00  688.00 1006.00\n\n        Draw Display List 0x5cfee368, flags 0x224073\n\n        Start display list (0x5cfee368, Button, render=1)\n\n          Save 3\n\n          Translate (left, top) 32, 32\n\n          ClipRect 0.00, 0.00, 243.00, 96.00\n\n          Draw patch    0.00    0.00  243.00   96.00\n\n          Save flags 3\n\n          ClipRect   24.00    0.00  219.00   80.00\n\n          Translate by 24.000000 23.000000\n\n          Draw Text of count 12, bytes 24\n\n          Restore to count 1\n\n        Done (0x5cfee368, Button)\n\n        Restore to count 1\n\n      Done (0x5ea59bf8, RelativeLayout)\n\n    Done (0x5ea5ad78, FrameLayout)\n\n    Draw Display List 0x5ea64ac8, flags 0x24053\n\n    Start display list (0x5ea64ac8, ActionBarContainer, render=1)\n\n      Save 3\n\n      Translate (left, top) 0, 50\n\n      ClipRect 0.00, 0.00, 720.00, 96.00\n\n      Draw patch    0.00    0.00  720.00   96.00\n\n      Draw Display List 0x5ea64910, flags 0x224053\n\n      Start display list (0x5ea64910, ActionBarView, render=1)\n\n        Save 3\n\n        ClipRect 0.00, 0.00, 720.00, 96.00\n\n        Draw Display List 0x5ea63790, flags 0x224053\n\n        Start display list (0x5ea63790, LinearLayout, render=1)\n\n          Save 3\n\n          Translate (left, top) 17, 0\n\n          ClipRect 0.00, 0.00, 265.00, 96.00\n\n          Draw Display List 0x5ea5fe80, flags 0x224053\n\n          Start display list (0x5ea5fe80,\n\n                              ActionBarView.HomeView, render=1)\n\n            Save 3\n\n            ClipRect 0.00, 0.00, 80.00, 96.00\n\n            Draw Display List 0x5ea5ed00, flags 0x224053\n\n            Start display list (0x5ea5ed00, ImageView, render=1)\n\n              Save 3\n\n              Translate (left, top) 8, 16\n\n              ClipRect 0.00, 0.00, 64.00, 64.00\n\n              Save flags 3\n\n              ConcatMatrix\n\n                [0.67 0.00 0.00] [0.00 0.67 0.00] [0.00 0.00 1.00]\n\n              Draw bitmap 0x5d33ae70 at 0.000000 0.000000\n\n              Restore to count 1\n\n            Done (0x5ea5ed00, ImageView)\n\n          Done (0x5ea5fe80, ActionBarView.HomeView)\n\n          Draw Display List 0x5ea63618, flags 0x224053\n\n          Start display list (0x5ea63618, LinearLayout, render=1)\n\n            Save 3\n\n            Translate (left, top) 80, 23\n\n            ClipRect 0.00, 0.00, 185.00, 49.00\n\n            Save flags 3\n\n            ClipRect    0.00    0.00  169.00   49.00\n\n            Draw Display List 0x5ea634a0, flags 0x224073\n\n            Start display list (0x5ea634a0, TextView, render=1)\n\n              Save 3\n\n              ClipRect 0.00, 0.00, 169.00, 49.00\n\n              Save flags 3\n\n              ClipRect    0.00    0.00  169.00   49.00\n\n              Draw Text of count 9, bytes 18\n\n              Restore to count 1\n\n            Done (0x5ea634a0, TextView)\n\n            Restore to count 1\n\n          Done (0x5ea63618, LinearLayout)\n\n        Done (0x5ea63790, LinearLayout)\n\n      Done (0x5ea64910, ActionBarView)\n\n    Done (0x5ea64ac8, ActionBarContainer)\n\n    Draw patch    0.00  146.00  720.00  178.00\n\n  Done (0x5ea64d30, ActionBarOverlayLayout)\n\nDone (0x5ea4f008, PhoneWindow.DecorView)<\/pre>\n<h3 id=\"opengl_1\"><span class=\"ez-toc-section\" id=\"OpenGL-2\"><\/span>OpenGL<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<pre class=\"lang:sh decode:true \">eglCreateContext(version = 1, context = 0)\n\neglMakeCurrent(context = 0)\n\nglGetIntegerv(pname = GL_MAX_TEXTURE_SIZE, params = [2048])\n\nglGetIntegerv(pname = GL_MAX_TEXTURE_SIZE, params = [2048])\n\nglGetString(name = GL_VERSION) = OpenGL ES 2.0 14.01003\n\nglGetIntegerv(pname = GL_MAX_TEXTURE_SIZE, params = [2048])\n\nglGenBuffers(n = 1, buffers = [1])\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 1)\n\nglBufferData(target = GL_ARRAY_BUFFER, size = 64, data = [64 bytes],\n\n             usage = GL_STATIC_DRAW)\n\nglDisable(cap = GL_SCISSOR_TEST)\n\nglActiveTexture(texture = GL_TEXTURE0)\n\nglGenBuffers(n = 1, buffers = [2])\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 2)\n\nglBufferData(target = GL_ARRAY_BUFFER, size = 131072, data = 0x0,\n\n             usage = GL_DYNAMIC_DRAW)\n\nglGetIntegerv(pname = GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,\n\n              params = [16])\n\nglGetIntegerv(pname = GL_MAX_TEXTURE_SIZE, params = [2048])\n\nglGenTextures(n = 1, textures = [1])\n\nglBindTexture(target = GL_TEXTURE_2D, texture = 1)\n\nglEGLImageTargetTexture2DOES(target = GL_TEXTURE_2D,\n\n                             image = 2138532008)\n\nglGetError(void) = (GLenum) GL_NO_ERROR\n\nglDisable(cap = GL_DITHER)\n\nglClearColor(red = 0,000000, green = 0,000000, blue = 0,000000,\n\n             alpha = 0,000000)\n\nglEnableVertexAttribArray(index = 0)\n\nglDisable(cap = GL_BLEND)\n\nglGenTextures(n = 1, textures = [2])\n\nglBindTexture(target = GL_TEXTURE_2D, texture = 2)\n\nglPixelStorei(pname = GL_UNPACK_ALIGNMENT, param = 1)\n\nglTexImage2D(target = GL_TEXTURE_2D, level = 0,\n\n             internalformat = GL_ALPHA, width = 1024, height = 512,\n\n             border = 0, format = GL_ALPHA, type = GL_UNSIGNED_BYTE,\n\n             pixels = [])\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_MIN_FILTER,\n\n                param = 9728)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_MAG_FILTER, param = 9728)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_WRAP_S, param = 33071)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_WRAP_T, param = 33071)\n\nglViewport(x = 0, y = 0, width = 800, height = 1205)\n\nglPixelStorei(pname = GL_UNPACK_ALIGNMENT, param = 1)\n\nglTexSubImage2D(target = GL_TEXTURE_2D, level = 0, xoffset = 0, yoffset = 0, width = 1024, height = 80, format = GL_ALPHA, type = GL_UNSIGNED_BYTE, pixels = 0x697b7008)\n\nglInsertEventMarkerEXT(length = 0, marker = Flush)\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 0)\n\nglBindTexture(target = GL_TEXTURE_2D, texture = 1)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_WRAP_S, param = 33071)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_WRAP_T, param = 33071)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_MIN_FILTER, param = 9729)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_MAG_FILTER, param = 9729)\n\nglCreateShader(type = GL_VERTEX_SHADER) = (GLuint) 1\n\nglShaderSource(shader = 1, count = 1, string = attribute vec4 position;\n\nattribute vec2 texCoords;\n\nuniform mat4 projection;\n\nuniform mat4 transform;\n\nvarying vec2 outTexCoords;\n\nvoid main(void) {\n\n    outTexCoords = texCoords;\n\n    gl_Position = projection * transform * position;\n\n}\n\n, length = [0])\n\nglCompileShader(shader = 1)\n\nglGetShaderiv(shader = 1, pname = GL_COMPILE_STATUS, params = [1])\n\nglCreateShader(type = GL_FRAGMENT_SHADER) = (GLuint) 2\n\nglShaderSource(shader = 2, count = 1, string = precision mediump float;\n\nvarying vec2 outTexCoords;\n\nuniform sampler2D baseSampler;\n\nvoid main(void) {\n\n    gl_FragColor = texture2D(baseSampler, outTexCoords);\n\n}\n\n, length = [0])\n\nglCompileShader(shader = 2)\n\nglGetShaderiv(shader = 2, pname = GL_COMPILE_STATUS, params = [1])\n\nglCreateProgram(void) = (GLuint) 3\n\nglAttachShader(program = 3, shader = 1)\n\nglAttachShader(program = 3, shader = 2)\n\nglBindAttribLocation(program = 3, index = 0, name = position)\n\nglBindAttribLocation(program = 3, index = 1, name = texCoords)\n\nglGetProgramiv(program = 3, pname = GL_ACTIVE_ATTRIBUTES, params = [2])\n\nglGetProgramiv(program = 3, pname = GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, params = [10])\n\nglGetActiveAttrib(program = 3, index = 0, bufsize = 10, length = [0], size = [1], type = [GL_FLOAT_VEC4], name = position)\n\nglGetActiveAttrib(program = 3, index = 1, bufsize = 10, length = [0], size = [1], type = [GL_FLOAT_VEC2], name = texCoords)\n\nglGetProgramiv(program = 3, pname = GL_ACTIVE_UNIFORMS, params = [3])\n\nglGetProgramiv(program = 3, pname = GL_ACTIVE_UNIFORM_MAX_LENGTH, params = [12])\n\nglGetActiveUniform(program = 3, index = 0, bufsize = 12, length = [0], size = [1], type = [GL_FLOAT_MAT4], name = projection)\n\nglGetActiveUniform(program = 3, index = 1, bufsize = 12, length = [0], size = [1], type = [GL_FLOAT_MAT4], name = transform)\n\nglGetActiveUniform(program = 3, index = 2, bufsize = 12, length = [0], size = [1], type = [GL_SAMPLER_2D], name = baseSampler)\n\nglLinkProgram(program = 3)\n\nglGetProgramiv(program = 3, pname = GL_LINK_STATUS, params = [1])\n\nglGetUniformLocation(program = 3, name = transform) = (GLint) 2\n\nglGetUniformLocation(program = 3, name = projection) = (GLint) 1\n\nglUseProgram(program = 3)\n\nglGetUniformLocation(program = 3, name = baseSampler) = (GLint) 0\n\nglUniform1i(location = 0, x = 0)\n\nglUniformMatrix4fv(location = 1, count = 1, transpose = false, value = [0.0025, 0.0, 0.0, 0.0, 0.0, -0.001659751, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -1.0, 1.0, -0.0, 1.0])\n\nglUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [800.0, 0.0, 0.0, 0.0, 0.0, 1205.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])\n\nglEnableVertexAttribArray(index = 1)\n\nglVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x681e7af4)\n\nglVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x681e7afc)\n\nglVertexAttribPointerData(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 4)\n\nglVertexAttribPointerData(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 4)\n\nglDrawArrays(mode = GL_TRIANGLE_STRIP, first = 0, count = 4)\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 2)\n\nglBufferSubData(target = GL_ARRAY_BUFFER, offset = 0, size = 576, data = [ 576 bytes ])\n\nglBufferSubData(target = GL_ARRAY_BUFFER, offset = 576, size = 192, data = [ 192 bytes ])\n\nglEnable(cap = GL_BLEND)\n\nglBlendFunc(sfactor = GL_SYNC_FLUSH_COMMANDS_BIT, dfactor = GL_ONE_MINUS_SRC_ALPHA)\n\nglUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 0)\n\nglGenBuffers(n = 1, buffers = [3])\n\nglBindBuffer(target = GL_ELEMENT_ARRAY_BUFFER, buffer = 3)\n\nglBufferData(target = GL_ELEMENT_ARRAY_BUFFER, size = 24576, data = [ 24576 bytes ], usage = GL_STATIC_DRAW)\n\nglVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0xbefdcf18)\n\nglVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0xbefdcf20)\n\nglVertexAttribPointerData(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 48)\n\nglVertexAttribPointerData(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 48)\n\nglDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 72, type = GL_UNSIGNED_SHORT, indices = 0x0)\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 2)\n\nglBufferSubData(target = GL_ARRAY_BUFFER, offset = 768, size = 576, data = [ 576 bytes ])\n\nglDisable(cap = GL_BLEND)\n\nglUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 33.0, 0.0, 1.0])\n\nglVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x300)\n\nglVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x308)\n\nglDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 54, type = GL_UNSIGNED_SHORT, indices = 0x0)\n\nglEnable(cap = GL_BLEND)\n\nglUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 0)\n\nglBindTexture(target = GL_TEXTURE_2D, texture = 2)\n\nglVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x696bd008)\n\nglVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x696bd010)\n\nglVertexAttribPointerData(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 80)\n\nglVertexAttribPointerData(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 80)\n\nglDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 120, type = GL_UNSIGNED_SHORT, indices = 0x0)\n\nglGenTextures(n = 1, textures = [3])\n\nglBindTexture(target = GL_TEXTURE_2D, texture = 3)\n\nglPixelStorei(pname = GL_UNPACK_ALIGNMENT, param = 4)\n\nglTexImage2D(target = GL_TEXTURE_2D, level = 0, internalformat = GL_RGBA, width = 64, height = 64, border = 0, format = GL_RGBA, type = GL_UNSIGNED_BYTE, pixels = 0x420cd930)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_MIN_FILTER, param = 9728)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_MAG_FILTER, param = 9728)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_WRAP_S, param = 33071)\n\nglTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_WRAP_T, param = 33071)\n\nglUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [64.0, 0.0, 0.0, 0.0, 0.0, 64.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 16.0, 38.0, 0.0, 1.0])\n\nglBindBuffer(target = GL_ARRAY_BUFFER, buffer = 1)\n\nglVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x0)\n\nglVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x8)\n\nglBindBuffer(target = GL_ELEMENT_ARRAY_BUFFER, buffer = 0)\n\nglDrawArrays(mode = GL_TRIANGLE_STRIP, first = 0, count = 4)\n\nglGetError(void) = (GLenum) GL_NO_ERROR\n\neglSwapBuffers()<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"Get-in-touch\"><\/span>Get in touch<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Interested in Android Development, Mobile and Embedded Systems? Have a look at our full portfolio on <a href=\"https:\/\/www.inovex.de\/de\/leistungen\/mobile\/\" target=\"_blank\" rel=\"noopener\">our website<\/a>, drop us an <a href=\"mailto:list-blog@inovex.de\">email<\/a> or call <a href=\"tel:+497216190210\">+49 721 619 021-0<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Last time, we took a thorough look at how Android converts the Java-Side onDraw()\u00a0method into a native display list on the C++-Side. This time we will go further along the Android Graphics Pipeline and take a look at how Android is drawing these display lists to the screen. We are now leaving the comfortable realm [&hellip;]<\/p>\n","protected":false},"author":20,"featured_media":12015,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"ep_exclude_from_search":false,"footnotes":""},"tags":[510],"service":[],"coauthors":[{"id":20,"display_name":"Mathias Garbe","user_nicename":"mgarbe"}],"class_list":["post-20971","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","tag-apps-2"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Android Graphics Pipeline: From Button to Framebuffer (Part 2)<\/title>\n<meta name=\"description\" content=\"In this article we take a look at how Android is drawing native display lists to the screen. We&#039;ll keep it simple and only show interesting code bits.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/\" \/>\n<meta property=\"og:locale\" content=\"de_DE\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Android Graphics Pipeline: From Button to Framebuffer (Part 2)\" \/>\n<meta property=\"og:description\" content=\"In this article we take a look at how Android is drawing native display lists to the screen. We&#039;ll keep it simple and only show interesting code bits.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/\" \/>\n<meta property=\"og:site_name\" content=\"inovex GmbH\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/inovexde\" \/>\n<meta property=\"article:published_time\" content=\"2015-03-30T07:18:59+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/titelbild-android-crop.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"2300\" \/>\n\t<meta property=\"og:image:height\" content=\"876\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Mathias Garbe\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/titelbild-android-crop-1024x390.jpg\" \/>\n<meta name=\"twitter:creator\" content=\"@inovexgmbh\" \/>\n<meta name=\"twitter:site\" content=\"@inovexgmbh\" \/>\n<meta name=\"twitter:label1\" content=\"Verfasst von\" \/>\n\t<meta name=\"twitter:data1\" content=\"Mathias Garbe\" \/>\n\t<meta name=\"twitter:label2\" content=\"Gesch\u00e4tzte Lesezeit\" \/>\n\t<meta name=\"twitter:data2\" content=\"21\u00a0Minuten\" \/>\n\t<meta name=\"twitter:label3\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data3\" content=\"Mathias Garbe\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/\"},\"author\":{\"name\":\"Mathias Garbe\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#\\\/schema\\\/person\\\/20a077747bb387e2233213d27195b789\"},\"headline\":\"Android Graphics Pipeline: From Button to Framebuffer (Part 2)\",\"datePublished\":\"2015-03-30T07:18:59+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/\"},\"wordCount\":2249,\"commentCount\":3,\"publisher\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.inovex.de\\\/wp-content\\\/uploads\\\/2015\\\/03\\\/titelbild-android-crop.jpg\",\"keywords\":[\"Apps\"],\"articleSection\":[\"Applications\",\"English Content\",\"General\"],\"inLanguage\":\"de\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/\",\"url\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/\",\"name\":\"Android Graphics Pipeline: From Button to Framebuffer (Part 2)\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.inovex.de\\\/wp-content\\\/uploads\\\/2015\\\/03\\\/titelbild-android-crop.jpg\",\"datePublished\":\"2015-03-30T07:18:59+00:00\",\"description\":\"In this article we take a look at how Android is drawing native display lists to the screen. We'll keep it simple and only show interesting code bits.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/#breadcrumb\"},\"inLanguage\":\"de\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.inovex.de\\\/wp-content\\\/uploads\\\/2015\\\/03\\\/titelbild-android-crop.jpg\",\"contentUrl\":\"https:\\\/\\\/www.inovex.de\\\/wp-content\\\/uploads\\\/2015\\\/03\\\/titelbild-android-crop.jpg\",\"width\":2300,\"height\":876,\"caption\":\"Eine Android Figur mit dem inovex Logo steht auf einem ge\u00f6ffneten Laptop\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/android-graphics-pipeline-from-button-to-framebuffer-part-2\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Android Graphics Pipeline: From Button to Framebuffer (Part 2)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#website\",\"url\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/\",\"name\":\"inovex GmbH\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"de\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#organization\",\"name\":\"inovex GmbH\",\"url\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/www.inovex.de\\\/wp-content\\\/uploads\\\/2021\\\/03\\\/inovex-logo-16-9-1.png\",\"contentUrl\":\"https:\\\/\\\/www.inovex.de\\\/wp-content\\\/uploads\\\/2021\\\/03\\\/inovex-logo-16-9-1.png\",\"width\":1921,\"height\":1081,\"caption\":\"inovex GmbH\"},\"image\":{\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/inovexde\",\"https:\\\/\\\/x.com\\\/inovexgmbh\",\"https:\\\/\\\/www.instagram.com\\\/inovexlife\\\/\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/inovex\",\"https:\\\/\\\/www.youtube.com\\\/channel\\\/UC7r66GT14hROB_RQsQBAQUQ\"]},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/#\\\/schema\\\/person\\\/20a077747bb387e2233213d27195b789\",\"name\":\"Mathias Garbe\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/911e68f92a8e63ff7f30008aaec8b66a2eb0177f8c880b64028b865b63149b24?s=96&d=retro&r=g3c26b5c18718786d4395456db2dafb83\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/911e68f92a8e63ff7f30008aaec8b66a2eb0177f8c880b64028b865b63149b24?s=96&d=retro&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/911e68f92a8e63ff7f30008aaec8b66a2eb0177f8c880b64028b865b63149b24?s=96&d=retro&r=g\",\"caption\":\"Mathias Garbe\"},\"url\":\"https:\\\/\\\/www.inovex.de\\\/de\\\/blog\\\/author\\\/mgarbe\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Android Graphics Pipeline: From Button to Framebuffer (Part 2)","description":"In this article we take a look at how Android is drawing native display lists to the screen. We'll keep it simple and only show interesting code bits.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/","og_locale":"de_DE","og_type":"article","og_title":"Android Graphics Pipeline: From Button to Framebuffer (Part 2)","og_description":"In this article we take a look at how Android is drawing native display lists to the screen. We'll keep it simple and only show interesting code bits.","og_url":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/","og_site_name":"inovex GmbH","article_publisher":"https:\/\/www.facebook.com\/inovexde","article_published_time":"2015-03-30T07:18:59+00:00","og_image":[{"width":2300,"height":876,"url":"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/titelbild-android-crop.jpg","type":"image\/jpeg"}],"author":"Mathias Garbe","twitter_card":"summary_large_image","twitter_image":"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/titelbild-android-crop-1024x390.jpg","twitter_creator":"@inovexgmbh","twitter_site":"@inovexgmbh","twitter_misc":{"Verfasst von":"Mathias Garbe","Gesch\u00e4tzte Lesezeit":"21\u00a0Minuten","Written by":"Mathias Garbe"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#article","isPartOf":{"@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/"},"author":{"name":"Mathias Garbe","@id":"https:\/\/www.inovex.de\/de\/#\/schema\/person\/20a077747bb387e2233213d27195b789"},"headline":"Android Graphics Pipeline: From Button to Framebuffer (Part 2)","datePublished":"2015-03-30T07:18:59+00:00","mainEntityOfPage":{"@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/"},"wordCount":2249,"commentCount":3,"publisher":{"@id":"https:\/\/www.inovex.de\/de\/#organization"},"image":{"@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#primaryimage"},"thumbnailUrl":"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/titelbild-android-crop.jpg","keywords":["Apps"],"articleSection":["Applications","English Content","General"],"inLanguage":"de","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/","url":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/","name":"Android Graphics Pipeline: From Button to Framebuffer (Part 2)","isPartOf":{"@id":"https:\/\/www.inovex.de\/de\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#primaryimage"},"image":{"@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#primaryimage"},"thumbnailUrl":"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/titelbild-android-crop.jpg","datePublished":"2015-03-30T07:18:59+00:00","description":"In this article we take a look at how Android is drawing native display lists to the screen. We'll keep it simple and only show interesting code bits.","breadcrumb":{"@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#breadcrumb"},"inLanguage":"de","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/"]}]},{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#primaryimage","url":"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/titelbild-android-crop.jpg","contentUrl":"https:\/\/www.inovex.de\/wp-content\/uploads\/2015\/03\/titelbild-android-crop.jpg","width":2300,"height":876,"caption":"Eine Android Figur mit dem inovex Logo steht auf einem ge\u00f6ffneten Laptop"},{"@type":"BreadcrumbList","@id":"https:\/\/www.inovex.de\/de\/blog\/android-graphics-pipeline-from-button-to-framebuffer-part-2\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.inovex.de\/de\/"},{"@type":"ListItem","position":2,"name":"Android Graphics Pipeline: From Button to Framebuffer (Part 2)"}]},{"@type":"WebSite","@id":"https:\/\/www.inovex.de\/de\/#website","url":"https:\/\/www.inovex.de\/de\/","name":"inovex GmbH","description":"","publisher":{"@id":"https:\/\/www.inovex.de\/de\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.inovex.de\/de\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"de"},{"@type":"Organization","@id":"https:\/\/www.inovex.de\/de\/#organization","name":"inovex GmbH","url":"https:\/\/www.inovex.de\/de\/","logo":{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/www.inovex.de\/de\/#\/schema\/logo\/image\/","url":"https:\/\/www.inovex.de\/wp-content\/uploads\/2021\/03\/inovex-logo-16-9-1.png","contentUrl":"https:\/\/www.inovex.de\/wp-content\/uploads\/2021\/03\/inovex-logo-16-9-1.png","width":1921,"height":1081,"caption":"inovex GmbH"},"image":{"@id":"https:\/\/www.inovex.de\/de\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/inovexde","https:\/\/x.com\/inovexgmbh","https:\/\/www.instagram.com\/inovexlife\/","https:\/\/www.linkedin.com\/company\/inovex","https:\/\/www.youtube.com\/channel\/UC7r66GT14hROB_RQsQBAQUQ"]},{"@type":"Person","@id":"https:\/\/www.inovex.de\/de\/#\/schema\/person\/20a077747bb387e2233213d27195b789","name":"Mathias Garbe","image":{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/secure.gravatar.com\/avatar\/911e68f92a8e63ff7f30008aaec8b66a2eb0177f8c880b64028b865b63149b24?s=96&d=retro&r=g3c26b5c18718786d4395456db2dafb83","url":"https:\/\/secure.gravatar.com\/avatar\/911e68f92a8e63ff7f30008aaec8b66a2eb0177f8c880b64028b865b63149b24?s=96&d=retro&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/911e68f92a8e63ff7f30008aaec8b66a2eb0177f8c880b64028b865b63149b24?s=96&d=retro&r=g","caption":"Mathias Garbe"},"url":"https:\/\/www.inovex.de\/de\/blog\/author\/mgarbe\/"}]}},"_links":{"self":[{"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/posts\/20971","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/users\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/comments?post=20971"}],"version-history":[{"count":0,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/posts\/20971\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/media\/12015"}],"wp:attachment":[{"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/media?parent=20971"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/tags?post=20971"},{"taxonomy":"service","embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/service?post=20971"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.inovex.de\/de\/wp-json\/wp\/v2\/coauthors?post=20971"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}