Update to v083 release.

byuu says:

This release adds preliminary Nintendo / Famicom emulation. It's only
a week or two old, so a lot of work still needs to be done before it can
compete with the most popular NES emulators.

It's important to clarify: bsnes is primarily an SNES emulator. That
will always be its forte and my core focus. I have added Game Boy
support previously for Super Game Boy emulation, and I've added NES
support mostly for something fun to work on to break up the monotony of
working on one system for seven years now. Obviously, I'd like the
emulation to be accurate and highly compatible, but I simply cannot
afford to invest the same amount of time and money into any other
systems.

Still, either way the NES and GB emulation serve as fun side-diversions,
and allow for a unified emulator interface with all of bsnes' unique
features applied to all systems. My personal favorite feature is
mightymo's extended built-in cheat code database that now also includes
NES and Game Boy codes. And it even works in Super Game Boy mode now,
too!

I'm also not worried about speed at all: so long as NES/GB are faster
than SNES/compatibility, it's fine by me. Note that due to the NES audio
running at 1.78MHz, and Game Boy audio at 4MHz stereo, a more
sophisticated audio resampler was needed: Ryphecha (Mednafen author) has
graciously written a first-rate resampler: it is a band-limited
Kaiser-windowed polyphase sinc resampler. It is combined with two
highpass filters to remove DC bias. The filter itself is SSE optimized,
but even still, approximately 50% of CPU usage for NES/GB emulation goes
to the audio filtering alone. However, you now have the best sound
possible for NES and Game Boy emulation as a result.

The GUI has also been heavily re-structured to accommodate multiple
emulators from the same interface. As such, it's quite likely a few bugs
are still lurking here and there. Please report them and I'll iron them
out for the next release.

Changelog:
- license is now GPLv3
- re-structured GUI as a multi-system emulator
- added NES emulation [byuu, Ryphecha]
- added NES ICs: MMC1, MMC2, MMC3, MMC4, MMC5, VRC4, VRC6+audio, VRC7,
  Sunsoft-5B+audio, Bandai-LZ93D50
- added NES boards: AxROM, BNROM, CNROM, ExROM, FxROM, GxROM, NROM,
  PxROM, SxROM, TxROM, UxROM
- Game Boy emulation improvements [Jonas Quinn]
- SNES core outputs full 19-bit color (4-bit luma included) for more
  accurate color reproduction (~5% speed hit)
- audio resampler is now a band-limited polyphase resampler [Ryphecha]
- cheat database includes NES+GB codes as well [mightymo, tukuyomi]
- lots of other changes
This commit is contained in:
Tim Allen 2011-10-14 21:05:25 +11:00
parent ef85f7ccb0
commit 7fa8ad755d
24 changed files with 1294 additions and 668 deletions

File diff suppressed because it is too large Load Diff

View File

@ -96,8 +96,6 @@
_wfullpath(fn, nall::utf16_t(filename), _MAX_PATH);
strcpy(resolvedname, nall::utf8_t(fn));
for(unsigned n = 0; resolvedname[n]; n++) if(resolvedname[n] == '\\') resolvedname[n] = '/';
unsigned length = strlen(resolvedname);
if(resolvedname[length] != '/') strcpy(resolvedname + length, "/");
return resolvedname;
}

View File

@ -14,7 +14,7 @@ static string iNES(const uint8_t *data, unsigned size) {
unsigned prgram = 0;
unsigned chrram = chrrom == 0 ? 8192 : 0;
print("iNES mapper: ", mapper, "\n");
//print("iNES mapper: ", mapper, "\n");
output.append("cartridge\n");
@ -119,7 +119,7 @@ static string iNES(const uint8_t *data, unsigned size) {
output.append("\t\tprg rom=", prgrom, " ram=", prgram, "\n");
output.append("\t\tchr rom=", chrrom, " ram=", chrram, "\n");
print(output, "\n");
//print(output, "\n");
return output;
}

View File

@ -17,6 +17,7 @@ namespace NES {
#include <libco/libco.h>
#include <nall/platform.hpp>
#include <nall/algorithm.hpp>
#include <nall/array.hpp>
#include <nall/crc32.hpp>
@ -24,7 +25,6 @@ namespace NES {
#include <nall/endian.hpp>
#include <nall/file.hpp>
#include <nall/function.hpp>
#include <nall/platform.hpp>
#include <nall/property.hpp>
#include <nall/serializer.hpp>
#include <nall/stdint.hpp>

View File

@ -34,7 +34,7 @@ void Cartridge::load(Mode cartridge_mode, const char *markup) {
nvram.reset();
parse_markup(markup);
print(markup, "\n\n");
//print(markup, "\n\n");
if(ram_size > 0) {
ram.map(allocate<uint8>(ram_size, 0xff), ram_size);
@ -45,7 +45,22 @@ void Cartridge::load(Mode cartridge_mode, const char *markup) {
ram.write_protect(false);
crc32 = crc32_calculate(rom.data(), rom.size());
switch((Mode)mode) {
case Mode::Normal:
case Mode::BsxSlotted:
sha256 = nall::sha256(rom.data(), rom.size());
break;
case Mode::Bsx:
sha256 = nall::sha256(bsxflash.memory.data(), bsxflash.memory.size());
break;
case Mode::SufamiTurbo:
sha256 = nall::sha256(sufamiturbo.slotA.rom.data(), sufamiturbo.slotA.rom.size());
break;
case Mode::SuperGameBoy:
sha256 = GameBoy::cartridge.sha256();
break;
}
system.load();
loaded = true;

View File

@ -17,6 +17,7 @@ namespace SNES {
#include <libco/libco.h>
#include <nall/platform.hpp>
#include <nall/algorithm.hpp>
#include <nall/any.hpp>
#include <nall/array.hpp>
@ -26,7 +27,6 @@ namespace SNES {
#include <nall/file.hpp>
#include <nall/function.hpp>
#include <nall/moduloarray.hpp>
#include <nall/platform.hpp>
#include <nall/priorityqueue.hpp>
#include <nall/property.hpp>
#include <nall/serializer.hpp>

View File

@ -65,14 +65,14 @@ void Video::update() {
break;
}
uint16_t *data = (uint16_t*)ppu.output;
uint32_t *data = (uint32_t*)ppu.output;
if(ppu.interlace() && ppu.field()) data += 512;
if(hires) {
//normalize line widths
for(unsigned y = 0; y < 240; y++) {
if(line_width[y] == 512) continue;
uint16_t *buffer = data + y * 1024;
uint32_t *buffer = data + y * 1024;
for(signed x = 255; x >= 0; x--) {
buffer[(x * 2) + 0] = buffer[(x * 2) + 1] = buffer[x];
}

View File

@ -62,6 +62,7 @@ obj/phoenix.o: phoenix/phoenix.cpp $(call rwildcard,phoenix/*)
$(phoenix_compile)
obj/resource.o: $(ui)/resource.rc
# windres --target=pe-i386 $(ui)/resource.rc obj/resource.o
windres $(ui)/resource.rc obj/resource.o
# targets

View File

@ -4,7 +4,7 @@ Config *config = 0;
Config::Config() {
attach(video.driver = "", "Video::Driver");
attach(video.filter = "None", "Video::Filter");
attach(video.shader = "None", "Video::Shader");
attach(video.shader = "Blur", "Video::Shader");
attach(video.synchronize = true, "Video::Synchronize");
attach(video.correctAspectRatio = true, "Video::CorrectAspectRatio");

View File

@ -62,6 +62,7 @@ void FileBrowser::open(const string &title, unsigned requestedMode, function<voi
callback = requestedCallback;
if(mode == &filterModes[requestedMode]) {
setVisible();
fileList.setFocused();
return;
}
mode = &filterModes[requestedMode];
@ -75,6 +76,7 @@ void FileBrowser::open(const string &title, unsigned requestedMode, function<voi
filterLabel.setText(filterText);
setVisible();
fileList.setFocused();
}
void FileBrowser::setPath(const string &path) {

View File

@ -324,14 +324,18 @@ void MainWindow::synchronize() {
}
void MainWindow::setupVideoFilters() {
lstring files = directory::files({ application->basepath, "filters/" }, "*.filter");
if(files.size() == 0) files = directory::files({ application->userpath, "filters/" }, "*.filter");
string path = { application->basepath, "filters/" };
lstring files = directory::files(path, "*.filter");
if(files.size() == 0) {
path = { application->userpath, "filters/" };
files = directory::files(path, "*.filter");
}
reference_array<RadioItem&> group;
settingsVideoFilterList = new RadioItem[files.size()];
for(unsigned n = 0; n < files.size(); n++) {
string name = files[n];
videoFilterName.append({ application->userpath, "filters/", name });
videoFilterName.append({ path, name });
if(auto position = name.position(".filter")) name[position()] = 0;
settingsVideoFilterList[n].setText(name);
@ -351,14 +355,18 @@ void MainWindow::setupVideoFilters() {
}
void MainWindow::setupVideoShaders() {
lstring files = directory::files({ application->basepath, "shaders/" }, { "*.", config->video.driver, ".shader" });
if(files.size() == 0) files = directory::files({ application->userpath, "shaders/" }, { "*.", config->video.driver, ".shader" });
string path = { application->basepath, "shaders/" };
lstring files = directory::files(path, { "*.", config->video.driver, ".shader" });
if(files.size() == 0) {
path = { application->userpath, "shaders/" };
files = directory::files(path, { "*.", config->video.driver, ".shader" });
}
reference_array<RadioItem&> group;
settingsVideoShaderList = new RadioItem[files.size()];
for(unsigned n = 0; n < files.size(); n++) {
string name = files[n];
videoShaderName.append({ application->userpath, "shaders/", name });
videoShaderName.append({ path, name });
if(auto position = name.position(string{ ".", config->video.driver, ".shader" })) name[position()] = 0;
settingsVideoShaderList[n].setText(name);

View File

@ -49,7 +49,7 @@ Application::Application(int argc, char **argv) {
inputManager = new InputManager;
utility = new Utility;
title = "bsnes v082.34";
title = "bsnes v083";
string fontFamily = Intrinsics::platform() == Intrinsics::Platform::Windows ? "Tahoma, " : "Sans, ";
normalFont = { fontFamily, "8" };

View File

@ -6,12 +6,18 @@ flags := -fPIC -O3 -I. -Iobj -fomit-frame-pointer
link := -s
objects :=
ifeq ($(platform),x)
flags += -fopenmp
endif
objects += out/Pixellate2x.filter
objects += out/Scanline-Dark.filter
objects += out/Scanline-Light.filter
objects += out/Scale2x.filter
objects += out/LQ2x.filter
objects += out/HQ2x.filter
objects += out/Overscan.filter
objects += out/Phosphor3x.filter
compile = $(cpp) $(link) $(flags) -o $@ -shared $<
@ -25,13 +31,15 @@ out/Scanline-Light.filter: Scanline/Scanline-Light.cpp Scanline/*
out/Scale2x.filter: Scale2x/Scale2x.cpp Scale2x/*
out/LQ2x.filter: LQ2x/LQ2x.cpp LQ2x/*
out/HQ2x.filter: HQ2x/HQ2x.cpp HQ2x/*
out/Overscan.filter: Overscan/Overscan.cpp Overscan/*
out/Phosphor3x.filter: Phosphor3x/Phosphor3x.cpp Phosphor3x/*
build: $(objects)
install:
mkdir -p ~/.config/batch/filters
chmod 777 ~/.config/batch/filters
cp out/*.filter ~/.config/batch/filters
mkdir -p ~/.config/bsnes/filters
chmod 777 ~/.config/bsnes/filters
cp out/*.filter ~/.config/bsnes/filters
clean:
rm out/*.filter

View File

@ -0,0 +1,30 @@
#include <nall/platform.hpp>
#include <nall/stdint.hpp>
using namespace nall;
extern "C" {
void filter_size(unsigned&, unsigned&);
void filter_render(uint16_t*, unsigned, const uint16_t*, unsigned, unsigned, unsigned);
};
dllexport void filter_size(unsigned &width, unsigned &height) {
}
dllexport void filter_render(
uint16_t *output, unsigned outputPitch,
const uint16_t *input, unsigned inputPitch,
unsigned width, unsigned height
) {
outputPitch >>= 1, inputPitch >>= 1;
#pragma omp parallel for
for(unsigned y = 0; y < height; y++) {
const uint16_t *in = input + y * inputPitch;
uint16_t *out = output + y * outputPitch;
for(unsigned x = 0; x < width; x++) {
uint16_t pixel = *in++;
if(x < 8 || x >= width - 8 || y < 8 || y >= height - 8) pixel = 0;
*out++ = pixel;
}
}
}

View File

@ -0,0 +1,45 @@
#include <nall/platform.hpp>
#include <nall/stdint.hpp>
using namespace nall;
extern "C" {
void filter_size(unsigned&, unsigned&);
void filter_render(uint16_t*, unsigned, const uint16_t*, unsigned, unsigned, unsigned);
};
dllexport void filter_size(unsigned &width, unsigned &height) {
width *= 3;
height *= 3;
}
dllexport void filter_render(
uint16_t *output, unsigned outputPitch,
const uint16_t *input, unsigned inputPitch,
unsigned width, unsigned height
) {
outputPitch >>= 1, inputPitch >>= 1;
#pragma omp parallel for
for(unsigned y = 0; y < height; y++) {
const uint16_t *in = input + y * inputPitch;
uint16_t *out0 = output + y * outputPitch * 3;
uint16_t *out1 = output + y * outputPitch * 3 + outputPitch;
uint16_t *out2 = output + y * outputPitch * 3 + outputPitch + outputPitch;
for(unsigned x = 0; x < width; x++) {
uint16_t full = *in++, half = (full >> 1) & 0x3def;
*out0++ = (full & 0x7c00);
*out1++ = (full & 0x7c00);
*out2++ = (half & 0x7c00);
*out0++ = (full & 0x03e0);
*out1++ = (full & 0x03e0);
*out2++ = (half & 0x03e0);
*out0++ = (full & 0x001f);
*out1++ = (full & 0x001f);
*out2++ = (half & 0x001f);
}
}
}

View File

@ -0,0 +1,17 @@
shader language=GLSL
vertex~
void main(void) {
gl_Position = ftransform();
gl_TexCoord[0] = gl_MultiTexCoord0;
}
fragment~ filter=linear
uniform sampler2D rubyTexture;
void main(void) {
vec4 rgb = texture2D(rubyTexture, gl_TexCoord[0].xy);
vec4 intens = smoothstep(0.2,0.8,rgb) + normalize(vec4(rgb.xyz, 1.0));
if(fract(gl_FragCoord.y * 0.5) > 0.5) intens = rgb * 0.8;
gl_FragColor = intens;
}

View File

@ -0,0 +1,61 @@
shader language=GLSL
vertex~
uniform vec2 rubyTextureSize;
void main()
{
float x = 0.5 * (1.0 / rubyTextureSize.x);
float y = 0.5 * (1.0 / rubyTextureSize.y);
vec2 dg1 = vec2( x, y);
vec2 dg2 = vec2(-x, y);
vec2 dx = vec2(x, 0.0);
vec2 dy = vec2(0.0, y);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_TexCoord[1].xy = gl_TexCoord[0].xy - dg1;
gl_TexCoord[1].zw = gl_TexCoord[0].xy - dy;
gl_TexCoord[2].xy = gl_TexCoord[0].xy - dg2;
gl_TexCoord[2].zw = gl_TexCoord[0].xy + dx;
gl_TexCoord[3].xy = gl_TexCoord[0].xy + dg1;
gl_TexCoord[3].zw = gl_TexCoord[0].xy + dy;
gl_TexCoord[4].xy = gl_TexCoord[0].xy + dg2;
gl_TexCoord[4].zw = gl_TexCoord[0].xy - dx;
}
fragment~ filter=linear
vec4 compress(vec4 in_color, float threshold, float ratio)
{
vec4 diff = in_color - vec4(threshold);
diff = clamp(diff, 0.0, 100.0);
return in_color - (diff * (1.0 - 1.0/ratio));
}
uniform sampler2D rubyTexture;
uniform vec2 rubyTextureSize;
void main()
{
vec3 c00 = texture2D(rubyTexture, gl_TexCoord[1].xy).xyz;
vec3 c01 = texture2D(rubyTexture, gl_TexCoord[4].zw).xyz;
vec3 c02 = texture2D(rubyTexture, gl_TexCoord[4].xy).xyz;
vec3 c10 = texture2D(rubyTexture, gl_TexCoord[1].zw).xyz;
vec3 c11 = texture2D(rubyTexture, gl_TexCoord[0].xy).xyz;
vec3 c12 = texture2D(rubyTexture, gl_TexCoord[3].zw).xyz;
vec3 c20 = texture2D(rubyTexture, gl_TexCoord[2].xy).xyz;
vec3 c21 = texture2D(rubyTexture, gl_TexCoord[2].zw).xyz;
vec3 c22 = texture2D(rubyTexture, gl_TexCoord[3].xy).xyz;
vec2 tex = gl_TexCoord[0].xy;
vec2 texsize = rubyTextureSize;
vec3 first = mix(c00, c20, fract(tex.x * texsize.x + 0.5));
vec3 second = mix(c02, c22, fract(tex.x * texsize.x + 0.5));
vec3 mid_horiz = mix(c01, c21, fract(tex.x * texsize.x + 0.5));
vec3 mid_vert = mix(c10, c12, fract(tex.y * texsize.y + 0.5));
vec3 res = mix(first, second, fract(tex.y * texsize.y + 0.5));
vec4 final = vec4(0.26 * (res + mid_horiz + mid_vert) + 3.5 * abs(res - mix(mid_horiz, mid_vert, 0.5)), 1.0);
gl_FragColor = compress(final, 0.8, 5.0);
}

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shader language="GLSL">
<fragment><![CDATA[
shader language=GLSL
fragment~ filter=linear
uniform sampler2D rubyTexture;
uniform vec2 rubyInputSize;
uniform vec2 rubyTextureSize;
@ -18,5 +17,3 @@
gl_FragColor = texture2D(rubyTexture, coord);
}
]]></fragment>
</shader>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<shader language="GLSL">
<vertex><![CDATA[
void main(void) {
gl_Position = ftransform();
gl_TexCoord[0] = gl_MultiTexCoord0;
}
]]></vertex>
<fragment><![CDATA[
uniform sampler2D rubyTexture;
void main(void) {
vec4 rgb = texture2D(rubyTexture, gl_TexCoord[0].xy);
vec4 intens = smoothstep(0.2,0.8,rgb) + normalize(vec4(rgb.xyz, 1.0));
if(fract(gl_FragCoord.y * 0.5) > 0.5) intens = rgb * 0.8;
gl_FragColor = intens;
}
]]></fragment>
</shader>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shader language="GLSL">
<vertex><![CDATA[
shader language=GLSL
vertex~
uniform vec2 rubyTextureSize;
void main() {
@ -22,9 +21,8 @@
gl_TexCoord[4].xy = gl_TexCoord[0].xy + dg2;
gl_TexCoord[4].zw = gl_TexCoord[0].xy - dx;
}
]]></vertex>
<fragment><![CDATA[
fragment~ filter=nearest
uniform sampler2D rubyTexture;
const float mx = 0.325; // start smoothing wt.
@ -69,5 +67,3 @@
gl_FragColor.xyz = w1 * c10 + w2 * c21 + w3 * c12 + w4 * c01 + (1.0 - w1 - w2 - w3 - w4) * c11;
}
]]></fragment>
</shader>

View File

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<shader language="GLSL">
<vertex><![CDATA[
shader language=GLSL
vertex~
void main() {
gl_Position = ftransform();
gl_TexCoord[0] = gl_MultiTexCoord0;
}
]]></vertex>
<fragment><![CDATA[
fragment~ filter=nearest
uniform sampler2D rubyTexture;
uniform vec2 rubyTextureSize;
@ -40,5 +38,3 @@
gl_FragColor = averageColor;
}
]]></fragment>
</shader>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shader language="GLSL">
<vertex><![CDATA[
shader language=GLSL
vertex~
uniform vec2 rubyTextureSize;
void main() {
@ -24,9 +23,8 @@
gl_TexCoord[3] = gl_TexCoord[0] - offsety; //top
gl_TexCoord[4] = gl_TexCoord[0] + offsety; //bottom
}
]]></vertex>
<fragment><![CDATA[
fragment~ filter=nearest
uniform sampler2D rubyTexture;
uniform vec2 rubyTextureSize;
@ -40,8 +38,8 @@
colB = texture2DProj(rubyTexture, gl_TexCoord[3]); //B (top)
colH = texture2DProj(rubyTexture, gl_TexCoord[4]); //H (bottom)
sel = fract(gl_TexCoord[0].xy * rubyTextureSize.xy); //where are we (E0-E3)?
//E0 is default
sel = fract(gl_TexCoord[0].xy * rubyTextureSize.xy); //where are we (E0-E3)? E0 is default
if(sel.y >= 0.5) { tmp = colB; colB = colH; colH = tmp; } //E1 (or E3): swap B and H
if(sel.x >= 0.5) { tmp = colF; colF = colD; colD = tmp; } //E2 (or E3): swap D and F
@ -51,5 +49,3 @@
gl_FragColor = col;
}
]]></fragment>
</shader>

View File

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<shader language="HLSL">
<source><![CDATA[
shader~ language=HLSL
texture rubyTexture;
float4 vec;
@ -26,5 +24,3 @@
{
pass p0 { PixelShader = compile ps_2_0 DiffColorPass(); }
}
]]></source>
</shader>

View File

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<shader language="GLSL">
<vertex><![CDATA[
uniform vec2 rubyTextureSize;
void main()
{
float x = 0.5 * (1.0 / rubyTextureSize.x);
float y = 0.5 * (1.0 / rubyTextureSize.y);
vec2 dg1 = vec2( x, y);
vec2 dg2 = vec2(-x, y);
vec2 dx = vec2(x, 0.0);
vec2 dy = vec2(0.0, y);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_TexCoord[1].xy = gl_TexCoord[0].xy - dg1;
gl_TexCoord[1].zw = gl_TexCoord[0].xy - dy;
gl_TexCoord[2].xy = gl_TexCoord[0].xy - dg2;
gl_TexCoord[2].zw = gl_TexCoord[0].xy + dx;
gl_TexCoord[3].xy = gl_TexCoord[0].xy + dg1;
gl_TexCoord[3].zw = gl_TexCoord[0].xy + dy;
gl_TexCoord[4].xy = gl_TexCoord[0].xy + dg2;
gl_TexCoord[4].zw = gl_TexCoord[0].xy - dx;
}
]]></vertex>
<fragment><![CDATA[
vec4 compress(vec4 in_color, float threshold, float ratio)
{
vec4 diff = in_color - vec4(threshold);
diff = clamp(diff, 0.0, 100.0);
return in_color - (diff * (1.0 - 1.0/ratio));
}
uniform sampler2D rubyTexture;
uniform vec2 rubyTextureSize;
void main ()
{
vec3 c00 = texture2D(rubyTexture, gl_TexCoord[1].xy).xyz;
vec3 c01 = texture2D(rubyTexture, gl_TexCoord[4].zw).xyz;
vec3 c02 = texture2D(rubyTexture, gl_TexCoord[4].xy).xyz;
vec3 c10 = texture2D(rubyTexture, gl_TexCoord[1].zw).xyz;
vec3 c11 = texture2D(rubyTexture, gl_TexCoord[0].xy).xyz;
vec3 c12 = texture2D(rubyTexture, gl_TexCoord[3].zw).xyz;
vec3 c20 = texture2D(rubyTexture, gl_TexCoord[2].xy).xyz;
vec3 c21 = texture2D(rubyTexture, gl_TexCoord[2].zw).xyz;
vec3 c22 = texture2D(rubyTexture, gl_TexCoord[3].xy).xyz;
vec2 tex = gl_TexCoord[0].xy;
vec2 texsize = rubyTextureSize;
vec3 first = mix(c00, c20, fract(tex.x * texsize.x + 0.5));
vec3 second = mix(c02, c22, fract(tex.x * texsize.x + 0.5));
vec3 mid_horiz = mix(c01, c21, fract(tex.x * texsize.x + 0.5));
vec3 mid_vert = mix(c10, c12, fract(tex.y * texsize.y + 0.5));
vec3 res = mix(first, second, fract(tex.y * texsize.y + 0.5));
vec4 final = vec4(0.26 * (res + mid_horiz + mid_vert) + 3.5 * abs(res - mix(mid_horiz, mid_vert, 0.5)), 1.0);
gl_FragColor = compress(final, 0.8, 5.0);
}
]]></fragment>
</shader>