How swiftshader supports texture

0x1 Introduction

Here is the OpenGL ES pipeline, the pink color box represents the data buffer in the pipeline, the gray blue box represents the operation in the pipeline.

In the following of this article, I will show you how texture is used in the OpenGL ES pipeline. In order to explains it more clearly, I will dig into the texture support in Swiftshader, since Swiftshader is the software implementation of OpenGL ES pipeline, we can see the detail implementation of it.

pipeline

0x2 Application code.

Here is the code for texture generation.
It generates and binds texture, and sets the parameter for it.
GL_TEXTURE_MAG_FILTER and GL_TEXTURE_MIN_FILTER is used to set up the filtering mode for mipmaps. GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T is used to set up texture wrap modes when texture coordinate is outside the range[0.0, 1.0].

1
2
3
4
5
6
7
glGenTextures(1, mTexture, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

Here is the code for texture upload.
The texture’s content is passed by parameter pixel, the GPU driver will copy pixel into its managed internal buffer, If the GPU hardware supports tile rendering, typically there is a conversion from linear buffer to tile buffer at the same time.

1
2
3
4
5
6
7
8
9
10
11
12
glBindTexture(GL_TEXTURE_2D, mTexture);
glTexImage2D(GL_TEXTURE_2D,
0,
GL_LUMINANCE,
width,
height,
0,
GL_LUMINANCE,
GL_UNSIGNED_BYTE,
pixel);
glUniform1i(mTextureHandle, i);

0x3 Texture in Shader code.

Here is one example of glsl shader code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const char simplevs[] =
attribute vec4 position;
attribute vec2 texCoords;
varying vec2 outTexCoords;
void main(void) {
outTexCoords = texCoords;
gl_Position = position;
}";
static const char simplefs[] =
precision mediump float;
varying vec2 outTexCoords;
uniform sampler2D texture;
void main(void) {
gl_FragColor = texture2D(texture, outTexCoords);
}

In the vertex shader code, the texture’s coordinate is input by attribute texCoords, then it is output in vertex shader as outTexCoords,
Then in primitive assembly and rasterization stage, it is clipped and interpolated.
Then in fragment shader code, the clipped and interpolated texture coordinate is used to fetching specific position’s pixel from the texture.
Here is the diagram to explain the execution sequence.
sequence

0x4 Texture support in swiftshader

Let’s to see how texture is supported in swiftshader, Here is the draft diagram in swiftshader about texture support.

texture_in_swiftshader

0x41 Set texture for pipeline in glDrawXX()

At this time, texture is uploaded through glTexImage2D(), glDrawXX() is triggered. applyTexture() is used to setup texture for current draw context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void Context::applyTexture(sw::SamplerType type, int index, Texture *baseTexture)
{
……
switch(baseTexture->getTarget())
{
case GL_TEXTURE_2D:
{
Texture2D *texture = static_cast<Texture2D*>(baseTexture);
for(int mipmapLevel = 0; mipmapLevel < sw::MIPMAP_LEVELS; mipmapLevel++)
{
int surfaceLevel = mipmapLevel + baseLevel;
if(surfaceLevel > maxLevel)
{
surfaceLevel = maxLevel;
}
egl::Image *surface = texture->getImage(surfaceLevel);
device->setTextureLevel(sampler, 0, mipmapLevel, surface, sw::TEXTURE_2D);
}
}
break;
……
}

0x42 Calculate texture’s coordinate in VertexProcessor.

It generate LLVM IR for texture coordinate calculation in vertex shader code.

1
2
3
4
5
6
7
8
9
10
11
void VertexProgram::program(UInt& index)
{
……
for(size_t i = 0; i < shader->getLength(); i++)
{
const Shader::Instruction *instruction = shader->getInstruction(i);
Shader::Opcode opcode = instruction->opcode;
……
}
……
}

0x43 Clip and interpolate texture coordinate in SetupProcessor()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
void SetupRoutine::generate()
{
// Culling
if(solidTriangle)
{
Float A = (y2 - y0) * x1 + (y1 - y2) * x0 + (y0 - y1) * x2; // Area
If(A == 0.0f)
{
Return(false);
}
Int w0w1w2 = *Pointer<Int>(v0 + pos * 16 + 12) ^
*Pointer<Int>(v1 + pos * 16 + 12) ^
*Pointer<Int>(v2 + pos * 16 + 12);
A = IfThenElse(w0w1w2 < 0, -A, A);
if(state.cullMode == CULL_CLOCKWISE)
{
If(A >= 0.0f) Return(false);
}
else if(state.cullMode == CULL_COUNTERCLOCKWISE)
{
If(A <= 0.0f) Return(false);
}
}
……
Int n = *Pointer<Int>(polygon + OFFSET(Polygon,n));
Int m = *Pointer<Int>(polygon + OFFSET(Polygon,i));
If(m != 0 || Bool(!solidTriangle))// Clipped triangle
{
……
For(Int q = 0, q < state.multiSample, q++)
{
……
Xq[n] = Xq[0];
Yq[n] = Yq[0];
// Setup edge for Rasterize
{
Int i = 0;
Do
{
edge(primitive, data, Xq[i + 1 - d], Yq[i + 1 - d], Xq[i + d], Yq[i + d], q);
i++;
}
Until(i >= n)
}
}
}
……
if(state.interpolateW)
{
……
}
if(state.interpolateZ)
{
……
}
for(int interpolant = 0; interpolant < MAX_FRAGMENT_INPUTS; interpolant++)
{
for(int component = 0; component < 4; component++)
{
int attribute = state.gradient[interpolant][component].attribute;
bool flat = state.gradient[interpolant][component].flat;
bool wrap = state.gradient[interpolant][component].wrap;
if(attribute != Unused)
{
setupGradient(primitive, tri, w012, M, v0, v1, v2, OFFSET(Vertex,v[attribute][component]), OFFSET(Primitive,V[interpolant][component]), flat, sprite, state.perspective, wrap, component);
}
}
}
}

0x44 Pass parameter for pixel shader

The parameter includes texture sampler and texture’s coordinate. These parameter is encapsulated in DrawData.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
struct DrawData
{
const Constants *constants;
const void *input[MAX_VERTEX_INPUTS];
unsigned int stride[MAX_VERTEX_INPUTS];
Texture mipmap[TOTAL_IMAGE_UNITS];
const void *indices;
}
void Renderer::executeTask(int threadIndex)
{
switch(task[threadIndex].type)
{
case Task::PRIMITIVES:
……
break;
case Task::PIXELS:
{
int unit = task[threadIndex].primitiveUnit;
int visible = primitiveProgress[unit].visible;
if(visible > 0)
{
int cluster = task[threadIndex].pixelCluster;
Primitive *primitive = primitiveBatch[unit];
DrawCall *draw = drawList[pixelProgress[cluster].drawCall & DRAW_COUNT_BITS];
DrawData *data = draw->data;
PixelProcessor::RoutinePointer pixelRoutine = draw->pixelPointer;
pixelRoutine(primitive, visible, cluster, data);
}
finishRendering(task[threadIndex]);
}
break;
……
}
}
``` C++
## 0x45 Fetch pixel from texture
PixelProgram fetch pixel from texture by passing the clipped and interpolated texture coordinate.
uvwq is the texture coordinate. the following code is executed in LLVM JIT.
``` C++
Vector4f PixelProgram::sampleTexture(int samplerIndex, Vector4f &uvwq, Float4 &bias, Vector4f &dsx, Vector4f &dsy, Vector4f &offset, SamplerFunction function)
{
Pointer<Byte> texture = data + OFFSET(DrawData, mipmap) + samplerIndex * sizeof(Texture);
Vector4f c = SamplerCore(constants, state.sampler[samplerIndex]).sampleTexture(texture, uvwq.x, uvwq.y, uvwq.z, uvwq.w, bias, dsx, dsy, offset, function);
return c;
}

Here is the detail texture sample algorithm, from the following code, we can see SwiftShader uses Bilinear interpolation to sample the texture.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vector4s SamplerCore::sampleQuad2D(Pointer<Byte> &texture, Float4 &u, Float4 &v, Float4 &w, Vector4f &offset, Float &lod, Int face[4], bool secondLOD, SamplerFunction function)
{
// Bilinear interpolation
if(componentCount >= 1)
{
if(has16bitTextureComponents() && hasUnsignedTextureComponent(0))
{
c0.x = As<UShort4>(c0.x) - MulHigh(As<UShort4>(c0.x), f0u) + MulHigh(As<UShort4>(c1.x), f0u);
c2.x = As<UShort4>(c2.x) - MulHigh(As<UShort4>(c2.x), f0u) + MulHigh(As<UShort4>(c3.x), f0u);
c.x = As<UShort4>(c0.x) - MulHigh(As<UShort4>(c0.x), f0v) + MulHigh(As<UShort4>(c2.x), f0v);
}
else
{
if(hasUnsignedTextureComponent(0))
{
c0.x = MulHigh(As<UShort4>(c0.x), f1u1v);
c1.x = MulHigh(As<UShort4>(c1.x), f0u1v);
c2.x = MulHigh(As<UShort4>(c2.x), f1u0v);
c3.x = MulHigh(As<UShort4>(c3.x), f0u0v);
}
else
{
c0.x = MulHigh(c0.x, f1u1vs);
c1.x = MulHigh(c1.x, f0u1vs);
c2.x = MulHigh(c2.x, f1u0vs);
c3.x = MulHigh(c3.x, f0u0vs);
}
c.x = (c0.x + c1.x) + (c2.x + c3.x);
if(!hasUnsignedTextureComponent(0)) c.x = AddSat(c.x, c.x);// Correct for signed fractions
}
}
}