Repository navigation
Scale lines, blur and texsize with the render resolution (implements #682) - #1047
johnneerdael wants to merge 7 commits into
Conversation
…ectM-visualizer#682) projectM, like Milkdrop, draws waveforms, custom waves, shape outlines and motion vectors as 1 px GL lines. The higher the render resolution, the smaller the share of the picture they cover, so many presets get thin and dark at 1080p and 4K. projectm_opengl_set_line_reference_size() draws them as quads instead, one instance per segment with miter joins. Lines are 1 px wide at and below the reference area and widen by sqrt(render area / reference area) above it. Thick lines keep Milkdrop's four offset passes with scaled offsets, dots get whole-pixel sizes with area-compensated alpha, and edges stay hard unless projectm_opengl_set_line_antialiasing() is enabled. Above the reference area, Milkdrop's waveform sample-count rule and the spiro/hash wave fade follow the reference size too, so dense waves don't wash out. The default reference size is 0, which keeps the existing GL lines. If the driver rejects the line shaders, GL lines are drawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
So presets allocate nothing for scaled lines while no line reference size is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Milkdrop builds its blur levels as fixed fractions of its canvas, blurred by a few of their own texels, and preset shaders step by texsize texels. Both are a share of the picture only at the canvas size the preset was made on, so at 4K blurs are narrower and texel-step effects move slower and smaller than authored, whatever the line width. Above the line reference area: - the blur levels are made as from a source of the reference's area (the render size divided by the line scale); the first pass reads the source through its mipmaps at log2 of the scale, so every source pixel counts; - texsize (_c7), texsize_main, mip_x/y/avg and the per-frame and per-pixel pixelsx/pixelsy report that canvas size. Textures and their sampling stay at the render size. Without a reference size, or at or below its area, nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Since the line drawing is more a rendering feature than a preset thing, the You could also reuse the |
|
@kblaschke Moving the reusable line renderer and batching code into Renderer makes sense. LineGeometry currently also contains MilkDrop-specific sizing and waveform rules, so I’d keep those in MilkdropPreset and separate out the generic geometry. I’ll look at extending Mesh for the instanced path. Besides instanced drawing, it needs custom attribute layouts, divisors and per-strip offsets for the overlapping point data. I’d also move the generic line shader resources so Renderer stays independent of MilkdropPreset. |
Keep MilkDrop sizing and thick-line pass policies in MilkdropPreset. Extend Mesh with streamed instance data, custom attribute layouts and instanced draws, and use it for line strips and motion-vector quads. Validate with 207 unit tests, desktop/GLES shader linking, GLES syntax checks and 45 paired real-GL regression cases with exact RGB frame matches, including shader failure fallback.
Summary
Implements #682, Improve line rendering on higher resolutions. Closes #682.
projectM draws waveforms, custom waves, shape outlines and motion vectors as 1 px GL lines, like Milkdrop. At 3840×2160 a 1 px line covers about a third of the share of the picture it had at Milkdrop's 1024×768, where it would need to be 3.25 px wide. Line-heavy presets get thin and dark on 4K screens, and presets that feed their lines back into the image can go almost black. The blur levels and the
texsizesteps in preset shaders shrink the same way.This PR adds an opt-in line reference size. When it's set, lines are drawn as quads (one instance per segment, miter joins) that are 1 px wide at the reference area and wider above it, so a line keeps its share of the picture. Thick lines, waveform dots, the blur levels and the canvas size that preset code sees scale the same way, and so do two Milkdrop rules that depend on the render width. The default is 0, which renders exactly as before.
I maintain ProjectM TV, which has shipped this on Android TV since its 2.2 release. As I mentioned in the issue, scaling lines down below 1080p made presets darker, so lines never get thinner than 1 px. This is a clean port to current
masterwithout the TV app's other renderer changes.API
I'd suggest 1024×768, Milkdrop's authoring resolution. 0 in either dimension is the default. The SDL test UI reads
Line Reference Width,Line Reference HeightandLine Antialiasingfromconfig.inp; all three are off by default.How lines are sized
With render size W×H and reference Wr×Hr, the line scale is
s = max(1, sqrt(W·H / (W_r·H_r)))and a line isspixels wide. Using the area keeps the share of the picture the same at any aspect ratio; at the reference's aspect ratio it's just H / Hr. With 1024×768 that gives 1.62 px at 1920×1080 and 3.25 px at 3840×2160. The lower bound of 1 is there because thinner, faded lines made feedback presets go dark when I tested them.The width is measured along the line's minor axis, as GL lines do. A GL line covers one pixel per column (or row) at any angle, so a 45° line is only 1/√2 px wide perpendicular to itself. The quad uses a perpendicular offset of
s/2 · max(|dx|, |dy|)for a unit direction (dx, dy), which gives a diagonal quad line the same pixel coverage as the GL line it replaces.Each instance also reads the points before and after its segment. Corners are offset along the bisector of the two normals and lengthened by 1/cos(θ/2) so both edges stay at the right distance. For turns sharper than about 120° (cos < 0.49) and at strip ends the segment keeps its own normal, which avoids long spikes. I didn't use the round joins suggested in the issue because they draw every joint twice, which brightens additive and feedback presets.
Thick lines use Milkdrop's own scheme: the thin line drawn four times, offset by (0, 0), (x, 0), (x, y) and (0, y), with the offsets scaled by
s. That's one reference pixel for the main wave and half a pixel for custom waves and shape outlines, as projectM does today. A single wider line hits pixels a different number of times, and feedback presets amplified that by 10 to 30% in testing.Dots stay GL points. A dot is
b·spixels rounded up to a whole pixel, wherebis 2 for main-wave dots and thick custom-wave dots and 1 otherwise, and its alpha is multiplied by(b·s)² / ceil(b·s)²so its brightness matches its intended area. Main-wave dots are drawn once at that size instead of four times at one-pixel offsets.Edges are hard by default, like GL lines, because presets that feed their image back are tuned for that; a soft edge made one such preset 14% darker.
projectm_opengl_set_line_antialiasing()turns on a one-pixel ramp.At the reference size a quad line lights the same pixels as Milkdrop's 1 px line, apart from a few at sharp turns. On GLES the quads are moved by 1/64 px so that a line lying exactly on a pixel border lights the same row as a Mali-G52's GL lines (checked on a device in ProjectM TV). Desktop GL needs no offset.
Other Milkdrop quantities that follow the reference
These use the reference size instead of the render size above the reference area. At or below it, nothing changes.
samples > width / 3). At 4K that means 480 points instead of the 160 an author working at 1024 px wide saw. The rule now usesW / s, the reference-equivalent width. Without this,$$$ Royal - Mashup (103)was 2.3× as bright as authored at 1080p (ablation from the original work).Waveform::MaximizeColors()fades wave modes 2, 3 and 5 by the texture size, and now uses the reference size.W/s × H/s). The first pass reads the frame through its mipmaps atlog2(s)(textureLodwith a trilinear sampler, afterglGenerateMipmap), so every source pixel still contributes.texsize.zwtexels and turn UVs into pixels withtexsize.xy, tuned on a canvas of about the reference size.texsize,texsize_main,mip_x/y/avgand the per-frame and per-pixelpixelsx/pixelsynow reportW/s × H/s. Textures and their sampling stay at the render size, andtexsize_<name>for real textures is unchanged.Implementation
LineGeometryholds the maths as plain functions (LineScale,LineStyleFor,DotStyleFor,SampleDecisionWidth,MaximizeColorsTextureSize,BlurSourceFor,ShaderCanvasSize), plusLineBatch, which pads strips so that instance i reads points i to i+3.LineRendererlives inPresetState, one per preset. It uploads a batch and draws it withglDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, segments). Its program is compiled on first use and shared through the instance'sShaderCache, and its buffers are created on first use too.Waveform,CustomWaveformandCustomShape(outlines) use it when a reference size is set.MotionVectorsdraws all vectors in one instanced call with its own vertex shader, which reads the warp's u/v texture the same way the existing one does.BlurTexture::Update()takes the line scale, andMilkdropShader,PerFrameContextandPerPixelContextreportShaderCanvasSize(). If a line program fails to compile or link, that preset draws GL lines. The new and changed shaders link as GLSL 3.30 and GLSL ES 3.00.Evidence
The images, data, harness and preset files are on a separate evidence branch of my fork, with a README that covers the method and limits. None of it is part of this PR.
Each preset is rendered on the same frames three ways: upstream
masterat 3840×2160, this PR at 3840×2160 with reference 1024×768, and upstreammasterat 1182×665. That last one is 16:9 with the area of 1024×768, roughly what the preset authors saw, so I call it the authored look. The harness only uses the public C API. It ran on an Apple M4 Pro (OpenGL 4.1) with fixed synthetic audio,projectm_set_frame_time(frame / 30)and a 48×32 mesh. Images are frame 239, and brightness is mean luma over frames 120 to 239.Unmodified upstream doesn't render the same frame twice. Random and noise textures,
rand_presetand the firstTimeKeeperupdate are seeded fromrandom_deviceor the wall clock, and new framebuffer textures start out undefined. So I built both libraries with the same evidence-only patch that fixes those seeds. It isn't part of this PR.I rendered 124 Cream of the Crop presets (every 8th by name, stopped after 124). 103 of them have a usable brightness at the authored size. Mean luma relative to the authored look:
At 4K this PR is closer to the authored look for 77 presets and further away for 22. The six that end up above 1.5× are feedback presets that react strongly to small differences, for example
271 nz+ m21(0.39× to 3.15×) and$$$ Royal - Mashup (158)(0.76× to 1.62×); the evidence README lists all six. What's left is the bilinear smoothing of the feedback image per real texel, which this PR leaves alone.With the reference size at 0, 109 of 123 presets give byte-identical frames to upstream. The other 14 also vary between repeated runs of unmodified upstream, and in reruns each of them gave at least one frame that upstream also gave.
Real presets mix lines with feedback, so I also measured the strokes on their own in ProjectM TV, which uses the same line code. These were synthetic presets without feedback, with the canvas read through a point-sampling composite and energy taken as linear sums. Compared with 1 px GL lines at 1182×665, thin and thick custom waves at 1080p and 4K were within 0.47% in energy and 0.44% in lit area, with nearly the same distribution of per-pixel hit counts (largest histogram bin difference 0.0036). Alpha-blended thick custom waves were within 0.35%. (data, method)
Presets with the clearest difference
I re-rendered the 30 presets with the largest 4K difference at full size and kept the 16 where the change toward the authored look is easy to see. Each image shows upstream at 4K, this PR at 4K and the authored look, downscaled. The labels sit above the pixels and nothing is brightened.
13 (mean luma: upstream 0.029, this PR 0.150, authored 0.149)
A Feature Preset Master Exclusive ORB - Planetary Alignment Acidburn - Hexocollie, Flexi AdamFX,Martin b (mean luma: upstream 0.251, this PR 0.261, authored 0.279)
$$$ Royal - Mashup (295) (mean luma: upstream 0.005, this PR 0.253, authored 0.221)
Amandio C - Three (Flexi shader code) (mean luma: upstream 0.418, this PR 0.432, authored 0.423)
12 more presets
AdamFX 2 Geiss - Mash-Up Sphere Xibit Graffiti Warp me Oval Interprise3 (mean luma: upstream 0.370, this PR 0.334, authored 0.332)
$$$ Royal - Mashup (541) (mean luma: upstream 0.114, this PR 0.263, authored 0.234)
Adam Fx 2 Zylot - Age of Science Fierceness viynal - mash0000 - wheel of misanthropic misfortune (mean luma: upstream 0.429, this PR 0.424, authored 0.416)
14 (mean luma: upstream 0.008, this PR 0.162, authored 0.158)
$$$ Royal - Mashup (286) (mean luma: upstream 0.566, this PR 0.522, authored 0.446)
$$$ Royal - Mashup (118) (mean luma: upstream 0.181, this PR 0.354, authored 0.291)
$$$ Royal - Mashup (137) (mean luma: upstream 0.480, this PR 0.610, authored 0.609)
$$$ Royal - Mashup (184) (mean luma: upstream 0.164, this PR 0.153, authored 0.135)
$$$ Royal - Mashup (1) (mean luma: upstream 0.275, this PR 0.321, authored 0.321)
$$$ Royal - Mashup (355) (mean luma: upstream 0.092, this PR 0.226, authored 0.216)
Bdrv EoS_Phat blah_blah bdrv etAL rolo nz (mean luma: upstream 0.355, this PR 0.321, authored 0.323)
$$$ Royal - Mashup (422) (mean luma: upstream 0.199, this PR 0.281, authored 0.278)
Lines only vs. lines plus blur and texsize
Each image shows upstream at 4K, the first commit (lines only) at 4K, this PR at 4K and the authored look. With only the line width scaled, the authored structure still comes out too fine at 4K. Once the blur levels and
texsizefollow the reference, strokes, blobs and cells get their authored size.A Feature Preset Master Exclusive ORB - Planetary Alignment Acidburn - Hexocollie, Flexi AdamFX,Martin b
AdamFX 2 Geiss - Mash-Up Sphere Xibit Graffiti Warp me Oval Interprise3
Amandio C - Three (Flexi shader code)
$$$ Royal - Mashup (286)
100% crops
Unscaled 640×360 regions of the two 4K frames, taken where upstream and this PR differ most.
AdamFX 2 Geiss - Mash-Up Sphere Xibit Graffiti Warp me Oval Interprise3
A Feature Preset Master Exclusive ORB - Planetary Alignment Acidburn - Hexocollie, Flexi AdamFX,Martin b
Amandio C - Three (Flexi shader code)
5 more crops
Adam Fx 2 Zylot - Age of Science Fierceness viynal - mash0000 - wheel of misanthropic misfortune
$$$ Royal - Mashup (286)
$$$ Royal - Mashup (137)
$$$ Royal - Mashup (355)
Bdrv EoS_Phat blah_blah bdrv etAL rolo nz
Cost
Median frame time at 4K with
glFinisharound each frame, two runs each:Mashup (103) gets faster because the point-count rule draws 160 points at 4K instead of 480. The blur presets get a little faster because the blur levels are smaller at 4K, even with the per-frame mipmap generation. These are desktop numbers. On an Ugoos AM6 (Mali-G52, Android 9), ProjectM TV measured the line drawing at 0.9 to 3.9% slower on line-heavy presets at 1080p and 4K, and a blur-heavy preset 24% faster at 4K.
Renderer refactor
Reusable colored points, line batching, line drawing and the generic line shaders live in
Renderer. MilkDrop-specific sizing, waveform sample/fade rules, dot styles, shader-canvas policy and thick-line pass offsets remain inMilkdropPreset.Renderer::Meshnow owns and streams interleaved instance data, configures custom attribute layouts/divisors with per-strip offsets, and draws a separate vertex/instance count. Both line strips and motion-vector quads use it. The existing indexed draw path is retained. The motion-vector vertex shader remains with the preset code because it samples the preset warp coordinates; it shares Renderer’s line fragment shader.Testing
projectM-unittest: all 207 tests pass, including 39 newLineGeometry/LineBatchtests covering scale, sample width, fade size, blur source, shader canvas size, thick-line passes and offsets, dot sizes and strip padding. Six Mesh tests cover the unchanged indexed path, interleaved instance layouts and strip offsets, divisors, streaming uploads, independent vertex/instance counts, empty draws and lazy buffer ownership.glslangValidator -l: the line programs and the changed blur program link as#version 330and#version 300 es.USE_GLESC++ syntax check; the GLSL 300 ES programs link withglslangValidator. This is not a GLES device-runtime claim.I haven't built this branch on Linux, Windows, Emscripten or Android (that's up to CI here), and I haven't run this port on a GLES device. The same code runs on Mali-G52 and Mali-G310 Android TVs in ProjectM TV. I haven't checked NVIDIA Tegra, where pixel-border ties may land on a different row.
Not included
Risk and compatibility
MaximizeColors()now returns the color and the caller sets it, and the first blur pass reads withtextureLod(…, 0)on the same non-mipmapped linear sampler. The byte-identity check above covers this.BlurTexture::Update()gets a parameter with a default value.🤖 Generated with Claude Code