sRGB

sRGB Color Space and Texture Formats

Image bytes are sRGB-encoded: byte 128 is not half the light of 255 but about a fifth, since the encoding spends more values on dark tones, as the eye does, while lighting math needs linear light. An SRGB8_ALPHA8 texture is decoded to linear as it is sampled, before filtering; RGBA8 returns the bytes divided by 255:

The same bytes sampled from an RGBA8 and an SRGB8_ALPHA8 textureJavaScript
const sample = (format, byte) => {
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, gl.createTexture());
  gl.texImage2D(gl.TEXTURE_2D, 0, format, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
    new Uint8Array([byte, byte, byte, 255]));
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.activeTexture(gl.TEXTURE1);                   // GLH.evaluate's target goes to unit 1
  return GLH.evaluate(gl, 'result = texture(uTexture, vec2(0.5));',
    'uniform sampler2D uTexture;')[0].toFixed(4);
};
for (const byte of [64, 128, 188, 255]) {
  const [plain, srgb] = [gl.RGBA8, gl.SRGB8_ALPHA8].map((format) => sample(format, byte));
  console.log(byte, 'RGBA8', plain, 'SRGB8_ALPHA8', srgb);
}
Output
64 RGBA8 0.2510 SRGB8_ALPHA8 0.0513
128 RGBA8 0.5020 SRGB8_ALPHA8 0.2158
188 RGBA8 0.7373 SRGB8_ALPHA8 0.5039
255 RGBA8 1.0000 SRGB8_ALPHA8 1.0000

The canvas expects sRGB values, so a linear result must be encoded again, roughly pow(color, vec3(1.0 / 2.2)). The shelf so far lights sRGB colors directly, a common shortcut, with srgb: false; Normal Maps and PBR's PBR shader works in linear light. Data textures (normals, roughness) are never sRGB. Other handy WebGL 2 formats: R8 and RG8 for masks, RGBA16F for HDR, and the integer formats of texture().

The same byte ramp sampled from RGBA8, from SRGB8_ALPHA8 (decoded to linear), and re-encoded for the screenHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  .wrap { position: relative; max-width: 600px; }
  canvas { display: block; width: 100%; }
  .lbl { position: absolute; left: 6px; font-family: monospace; color: #fff; text-shadow: 0 0 3px #000; }
  table { max-width: 600px; width: 100%; border-collapse: collapse; font-family: monospace; margin-top: 4px; }
  td, th { padding: 2px 6px; border-bottom: 1px solid #e3ddd0; text-align: left; }
</style>
<div class="wrap">
  <canvas id="c" width="600" height="210"></canvas>
  <div class="lbl" style="top:6%">RGBA8: bytes / 255</div>
  <div class="lbl" style="top:39%">SRGB8_ALPHA8: decoded to linear light</div>
  <div class="lbl" style="top:72%">SRGB8_ALPHA8, then pow(c, 1/2.2) for the canvas</div>
</div>
<table id="t"></table>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPos; out vec2 vUV;
void main() { vUV = aPos; gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0); }`],
  [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float; in vec2 vUV; uniform sampler2D uRamp; uniform bool uEncode; out vec4 c;
void main() {
  vec3 color = texture(uRamp, vec2(vUV.x, 0.5)).rgb;
  if (uEncode) color = pow(color, vec3(1.0 / 2.2));        // back to (roughly) sRGB
  c = vec4(color, 1.0);
}`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0);

// One ramp of 16 bytes: 0, 17, 34 ... 255, stored twice with different internal formats.
const bytes = new Uint8Array(16 * 4);
for (let i = 0; i < 16; i++) bytes.set([i * 17, i * 17, i * 17, 255], i * 4);
function ramp(format) {
  const t = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, t);
  gl.texImage2D(gl.TEXTURE_2D, 0, format, 16, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, bytes);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  return t;
}
const plain = ramp(gl.RGBA8), srgb = ramp(gl.SRGB8_ALPHA8);
const uEncode = gl.getUniformLocation(program, 'uEncode');
[[plain, false], [srgb, false], [srgb, true]].forEach(([t, encode], row) => {
  gl.bindTexture(gl.TEXTURE_2D, t);
  gl.uniform1i(uEncode, encode);
  gl.viewport(0, 140 - row * 70, 600, 68);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
});

// Read the sampled values back through a float render target, for the table.
gl.getExtension('EXT_color_buffer_float');
const target = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, target);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, 16, 1);
gl.bindFramebuffer(gl.FRAMEBUFFER, gl.createFramebuffer());
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target, 0);
gl.viewport(0, 0, 16, 1);
gl.uniform1i(uEncode, 0);
const read = (t) => { gl.bindTexture(gl.TEXTURE_2D, t); gl.drawArrays(gl.TRIANGLES, 0, 6);
  const out = new Float32Array(64); gl.readPixels(0, 0, 16, 1, gl.RGBA, gl.FLOAT, out); return out; };
const a = read(plain), b = read(srgb);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
document.getElementById('t').innerHTML = '<tr><th>byte</th><th>RGBA8</th><th>SRGB8_ALPHA8</th></tr>' +
  [4, 8, 11, 15].map(i => `<tr><td>${i * 17}</td><td>${a[i * 4].toFixed(4)}</td><td>${b[i * 4].toFixed(4)}</td></tr>`).join('');
</script>