Купил себе столитровый прибабах, дабы отдохнуть от этой вашей радиофизики. Чуть не оглох на обкатке, правда

Купил себе столитровый прибабах, дабы отдохнуть от этой вашей радиофизики. Чуть не оглох на обкатке, правда

​

Купил за 24600 вместо 35000 что он ща стоит, новехонький, гайковёрт на 1700нм тянет

u/int7bh — 2 days ago
▲ 9 r/Bard

Gemini March 2026 (now "unknown") Knowledge cutoff date by the way. Luna cutoff is February 2026, Sonnet is January 2026.

Question is related to March 2025 GPU. Not even 2026.

I probably should've asked WHAT, not WHICH, but that's not the point.

u/int7bh — 4 days ago
▲ 173 r/Bard

3.6 now has "Unknown" knowledge cutoff. Certified DeepMind moment

u/int7bh — 4 days ago

Продолжаю писать радиофизический мод для Minecraft: Hertzian Dynamics. Хочу просто обсудить и выговориться спустя полтора месяца.

Собстна... Пару месяцев назад, может полтора, я делал пост, который тут на удивление залетел и до сих пор в закрепе, насчет своего мода в Minecraft. Думал, начну делать апдейты быстрее, но...

Но чем дальше я это пишу, тем яснее становится неприятная вещь: можно сделать хороший DSP-тракт, нормальный шумовой бюджет и красивые приборы, но если сама модель распространения неправильная, всё остальное начинает обслуживать очень убедительную ложь.

В чем суть: проблема проявилась на довольно простом тесте.

Передатчик и приёмник стоят в 200 метрах друг от друга. Частота 7 МГц, длина волны около 43 метров. Между ними на линии визирования ставится один железный блок размером 1 кубический метр.

https://preview.redd.it/ufotry7vl8eh1.png?width=2018&format=png&auto=webp&s=cd4c2100bd9520cf7254af21334703b83bb93362

Текущая модель способна посчитать такой блок практически абсолютной преградой и закрыть канал на весь бюджет, вплоть до сотен децибел. Физически это ерунда. Радиус первой зоны Френеля в середине такого пути около 46 метров. Метровый блок закрывает крошечную долю рабочего фронта. Он не обязан быть прозрачным как материал, волна просто обходит его. На 7 МГц размер объекта слишком мал относительно длины волны, чтобы он работал как сплошная стена.

https://preview.redd.it/gnod2hers8eh1.png?width=3439&format=png&auto=webp&s=b817f2cbba16773e24e39cfa06d249d600d56564

Масштаб трагедии на графике. Огромный пузырь - это первая зона Френеля (ее радиус в центре - 46 метров). Черная линия - ось LOS (прямая видимость). Крошечная, едва заметная точка на отметке 100 метров - тот самый кубический метр железа. Лучевая модель считает, что эта точка намертво перекрывает весь пузырь.

Причина оказалась не в DDA и не в таблице проводимости. Причина в самой абстракции. Сейчас транспорт в основном лучевой, и у него нашлось несколько критических проблем:

  • Обязательный сэмпл прямо на оси, имеющий максимальный вес.
  • Всего несколько десятков точек на огромном сечении зоны Френеля.
  • Выбор худшего сечения на всём пути.
  • Вырождение зоны возле антенн.
  • Мультипуть, который ищет стены от середины пути по шести направлениям и легко гасит собственные отражённые пути об отражающую стену.

В итоге я пришёл к выводу, что бесконечно чинить этот набор поправок нет смысла. Можно убрать самые грубые артефакты, но луч не станет волной от того, что вокруг него появилось ещё пять коэффициентов.

Что хочу сделать вместо этого

Ближайшая цель не "полный Максвелл на весь Minecraft-мир". Это было бы красиво только в тексте, а на практике закончилось бы сеткой из миллиардов неизвестных и дымом из сервера. Идея в иерархии моделей.

Сначала стабилизирую текущий движок. Уберу сломанный мультипуть, заменю точечное перекрытие зоны на устойчивую оценку площади и закрою ошибки в FFI, Vulkan и состоянии движущихся радиостанций.

Модель дифракции на клине. Показывает, как на самом деле плавно (в децибелах) должен затухать сигнал при постепенном погружении препятствия в зону Френеля, вместо мгновенной \"смерти\" эфира в лучевой абстракции.

Следующий большой шаг уже полевой: маршинг-солвер для комплексного электромагнитного поля. Изначально я думал начать с обычного 2D-параболического уравнения. Но в 2D одиночный куб автоматически превращается в бесконечную стену, уходящую поперёк среза.

https://preview.redd.it/68f3jz1yl8eh1.png?width=1784&format=png&auto=webp&s=206b1e15682b628f935b6366ee9277b40e67272f

Поэтому основной вариант теперь 3D PE:

du/dx = [ i / (2*k_0) ] * (d^2 u / dy^2 + d^2 u / dz^2) + [ (i*k_0) / 2 ] * (n^2 - 1) * u

Поле маршируется по дальности, а поперечная дифракция считается спектрально. Препятствие локально подавляет часть фронта, после чего поле естественно заполняет тень с боков.

Поле хочется считать один раз на передатчик и кэшировать. При этом хранить нужно комплексную передаточную функцию (амплитуду и фазу), чтобы получить когерентную интерференцию и корректную стыковку с локальным полноволновым решением.

я заебался

Расчет интерференции прямого и отраженного от земли лучей. Вот для получения этих "лепестков" затухания и нужны комплексные числа (амплитуда и фаза). Без фазы волны просто сложились бы по мощности, выдав плоскую скучную линию без интерференционных нулей.

Где начинается настоящая боль: Макро vs Микро

PE (Parabolic Equation) отлично считает рельеф и большие дистанции, но не умеет в обратное рассеяние, щели и металлические комнаты. Для таких мест планируются локальные полноволновые "острова".

Чтобы это работало, пространство придется разделить на два уровня:

Физика Глобальный решатель (3D PE) Локальный остров (FDTD / FDFD)
Что считает Дальнобойное распространение, кривизна земли, огибание рельефа Электромагнитное поле внутри сложной геометрии
Где применяется Открытый мир, расстояния в сотни блоков Комнаты, туннели, металлические коридоры
Как работает Волна марширует только вперед (односторонняя модель) Векторный Максвелл, считает отражения во все стороны

Отдельная проблема локальных островов - это металл. Просто занулить поле внутри металлического вокселя удобно численно, но это поглощающая маска, а не идеальный проводник (PEC). Настоящий металл должен отражать поле и соблюдать граничные условия.

Также придётся аккуратно определить знак комплексной проницаемости. Достаточно один раз смешать соглашения exp(-i wt) и exp(+ikx), чтобы пассивная среда начала усиливать волну вместо поглощения.

Что мне самому сейчас интересно обсудить

Граница между физикой и вычислительным бюджетом тут довольно забавная. Полностью разрешить такой мир по (λ/10) невозможно. Поэтому сейчас думаю над несколькими вопросами:

  1. Стоит ли сразу делать 3D PE, пусть даже с жёстким бюджетом по объёму, или сохранить 2D PE для дальнего рельефа, а 3D включать только вокруг застройки?
  2. Насколько заметна игроку фаза канала, если обычная голосовая связь узкополосная, но вокруг есть несколько передатчиков и ретрансляторов?
  3. Где разумнее проводить границу полноволнового острова: по размеру области, по металлической связности, по оценке обратного рассеяния или по наличию замкнутого объёма?
  4. Нужны ли вообще случайные рэлеевские замирания для полностью статичной сцены? Вероятно, их стоит привязать к движению игроков и листвы, а не просто бросать новое число каждый тик.

В общем, проект постепенно переходит из состояния "мод с довольно серьёзным радио" в состояние "небольшой радиофизический движок, к которому зачем-то прикручен Minecraft 1.7.10".

Мне это нравится, хотя иногда открываешь формулу, потом код, потом ещё одну статью, а через несколько часов обнаруживаешь, что пытался физически корректно объяснить, почему рация Стива не ловит другую рацию за кубом железа.

Если кому-то интересны PE, FDTD/FDFD, Vulkan, антенны или просто странные технические решения, буду рад обсудить.

количество съеденной бумаги бтв

https://preview.redd.it/q8iobnwyx8eh1.jpg?width=3072&format=pjpg&auto=webp&s=68b2d6f67851b81f38d010bc5bf554d942746f3a

reddit.com
u/int7bh — 6 days ago
▲ 110 r/rust

I tried to break even more Rust rules to compute (2+3)*2 [v2]

A while back I posted the thing that replaced (2+3)*2 with a UB bingo card. People kept commenting, so I kept going. This is v2. It got worse. That was the point.

https://godbolt.org/z/frfM6bncT <--- that's what you need to read bruv

The old version was honest about one thing: it broke runtime semantics. Raw pointers, transmute, FFI setjmp/longjmp across stack frames, the whole circus. But the borrow checker was still technically happy with me. I was sneaking lies past it through channels it doesn't guard. v2 stops sneaking. Now I lie to its face on its own terms and it just nods.

What's actually new this time:

  • The borrow checker fun. Two live &amp;mut to the same i32, both used across each other, neither derived from the other so there's no reborrow to track. A &amp;i32 the compiler thinks is frozen, mutated through underneath while it's still "frozen." A reference to a local returned after its scope ends, accepted as 'static. A self-referential struct, the one thing the model literally can't describe.

There's a little Anarchy&lt;T&gt; handle that hands out references of any lifetime and any mutability to the same slot, as many at once as you want - and the call sites that use it contain zero unsafe. Hold one and there's nothing left for the checker to verify.

  • Lying in the type system itself, no unsafe at the crime scene. Declared false covariance via PhantomData&lt;fn() -&gt; &amp;'a i32&gt; on a type that controls a writable slot, so lifetimes coerce shorter with no cast and no unsafe block. The compiler trusts the marker as blindly as LLVM trusted noalias.

Also threw in autoref specialization and Deref polymorphism so a Mirror isn't an i32 but answers to i32's methods anyway. Type identity dissolves without a single transmute.

  • (2+3)*2 is now also computed at the type level. Peano numbers + recursive trait impls. &lt;Ten as Peano&gt;::N is the answer produced entirely by trait resolution chewing through the recursion, and the runtime result gets checked against it. There's also a const-eval gate that's just an out-of-bounds array index - the build dies if the invariant breaks. No assert!, no panic!, just [()][(n != 10) as usize].
  • No macros, and addr_of! btw is gone too.

println! -> snprintf + write(1, …). assert_eq! -> if … { _exit(1) }. vec! -> alloc + from_raw_parts. Every ptr::addr_of! replaced with the raw-ref operator &amp;raw const / &amp;raw mut. A macro generates text that isn't in your source, so it's a different program than the one you're reading - felt like cheating. asm! stays, because it isn't generation: the assembler string goes through the compiler as-is, it's an intrinsic. The ! is a syntactic accident there.

  • fork -> vfork

Let me be honest: someone with a great nickname asked me, paraphrasing,

>"I need (2+3)x2 but I have no MMU, can you do this with vfork instead of fork?"

So I did. No MMU means no page table to copy, which is the entire reason vfork still exists, so I leaned all the way in: the child borrows the parent's address space, writes the answer into a global, and leaves through a raw _exit so nothing unwinds. No pipe, no copy. The parent is frozen until the child is gone, so the read always sees the write. Standing on the edge of the vfork contract on purpose.

  • Bullying the cache and the bus. Non-temporal movnti store straight past every cache level, sfence to drain the write-combining buffer, clflush to evict the line by hand, mfence, then read it back from DRAM. The bits never change. The whole memory hierarchy does a round trip it was specifically built to let you skip.
  • A misaligned read out of a #[repr(C, packed)] record, value parked one byte in so it straddles its own alignment. Never form a reference to a packed field (instant UB), only raw pointers.
  • Unwinding as a data channel (panic_any -> catch_unwind -> downcast), a calling-convention violation (calling a 3-arg extern "C" fn through a pointer typed with one arg), and niche-layout abuse (smuggling the value inside Option&lt;NonNull&gt;).

Everything from mu v1 is still in there - the /proc/self/mem round trip, process_vm_readv on itself, the GS-segment and thread-name storage, the hand-decomposed vtable dispatch, the memcpy over an AtomicI32, the self-modifying JIT blob on an RWX page. v2 just stacks the type-system stuff on top.

It's one translation unit, Linux-only, x86-64-only, and it prints 10. Every new function is an identity, so the chain 2 -&gt; +3 -&gt; ×2 never actually moves.

Credits: u/torsten_dev for the public-function-pointer idea - a function's address is a relocation, a symbolic constant LLVM can't reason about as an integer, so addr.saturating_add(x).wrapping_sub(addr) survives to run as an actual barrier (the floated (x ^ a) ^ a version doesn't work, that folds to x no matter what a is and the compiler proves it without ever knowing a). And u/Cats_and_Shit (lmao) for the vfork suggestion.

u/int7bh — 1 month ago

Making the physically-grounded radio mod for 1.7.10 GTNH. Uses Vulkan, btw.

Mod that actually tries to simulate a radio signal. Why? Because I wanted MIKU FM stations in Minecraft. That's all. That's what I'm showing here lmao.

NOTE: I'll upload it someday. After some code refactors, tho. Maybe in two weeks. Or one. No clue.

NOTE 2: Models and textures are belong to HBM Nuclear Tech Mod. These are temp. placeholders, but whatever. Thanks to bobcat.

This is for 1.7.10 (GTNH, needs lwjgl3ify). Instead of "are you within X blocks, yes/no", the whole signal path is simulated. The DSP and propagation math live in a Rust crate talking to Vulkan, the Forge mod drives it through Java 21's FFM.

Real modulation modes: AM, narrow/wide FM, SSB, CW, plus a noise jammer (I didn't show it because the jamming model sucks duh). When you transmit, the audio is modulated to baseband (FM integrates the audio into phase, AM rides it on the envelope, SSB builds the analytic signal with a Hilbert transform, etc.). Every transmitter in range gets frequency-shifted into your receiver's passband by a per-pair oscillator, they're all summed, a noise floor is added, then it's demodulated back. So two stations close on the dial genuinely interfere and a jammer actually buries you.

tech details ahh post goes below

Propagation is a link budget, btw. Free-space loss is the real Friis equation, 20*log(d) + 20*log(f) - 147.55 dB. Obstacles are a voxel DDA raycast, but through the first Fresnel zone rather than a single line: a bundle of rays that pinch together at both endpoints and bulge in the middle (the zone radius is roughly 0.5*sqrt(lambda*d), tens of metres on VHF over a long path). So a wall beside the line only blocks part of the zone and costs a few dB while the signal diffracts around it, but seal a transmitter inside iron and every ray passes through the metal and the path dies completely. Materials attenuate by their loss regime: conductors scale as sqrt(f), dielectrics roughly linearly, iron is about 200 dB/m (a wall), glass a few dB/m.

Noise is physical, starts at the real thermal floor, kT0 of about -174 dBm/Hz, scaled by your receiver bandwidth and noise figure, then the man-made noise of your environment is added on top (ITU-R P.372, which falls with frequency, so a city is tens of dB noisier than open country on HF). That produces a kinda real result: on HF the external noise dominates and a better receiver barely moves the floor, while on VHF the receiver's own figure sets it. The S-meter reads the actual S/N from the signal power over the noise power, before the AGC flattens it.

The world is flat, so there'd be no horizon and a ground wave would run forever. So Earth curvature is modelled... probably. It works in core, but not in Minecraft somewhy. But what is has is the 4/3-effective-Earth bulge between two antennas, d1*d2/(2k*Re), rises into the path past the radio horizon (sqrt(2k*Re*h), the familiar 4.12*sqrt(h) km) and the diffraction over it kills the link. Antenna height drives the horizon directly, so stacking antenna blocks raises your effective height and you reach further, literally through blocks. Earth radius is a config knob if you want a t i g h t e r world (why?)

HF is the exception. It bounces off the ionosphere: a foF2 lookup over the day sets the maximum usable frequency by the secant law, go above it and the signal escapes to space; below it the D-layer absorption follows 1/(f+fL)^2 and the solar zenith angle, so daytime eats the low bands and night opens them up. Skywave ignores the curvature entirely, which is exactly why shortwave reaches over the horizon and VHF can't.

Other bits: a transmitter has a Station mode that plays .qoa music files and embeds the station name in the signal as an inaudible 16/18 kHz FSK subcarrier, so a receiver or the analyzer pulls the name straight off the air (and loses it in the noise when the signal gets weak, like real RDS). There's an SDR-style spectrum analyzer with a waterfall and click-to-tune, a relay block, and the receive audio is band-limited (300-3000 Hz on voice, with FM de-emphasis) so it sounds like a radio instead of a clean speaker with hiss.

u/int7bh — 2 months ago

Пишу мод на симуляцию радиосвязи для Minecraft 1.7.10 с GPU-ускорением через Vulkan

Уже несколько недель ковыряю довольно странный pet-проект и захотелось поделиться, потому что стек получился нестандартный, и проблемы по дороге попадаются интересные.

!! АХТУНГ!!! ДЛИННОПОСТ!!1

Идея простая до неприличия. Беру Minecraft 1.7.10 (да, тот самый, GTNH-style модпаки), и пишу для него мод, где радиосвязь работает по-настоящему. Не "поставил блок передатчика, поставил блок приёмника, услышал звук". А вот так: у каждого передатчика есть несущая частота, полоса, мощность в ваттах, тип модуляции (AM, NFM, WFM, USB, LSB, CW), микрофон захватывает голос игрока через OpenAL, модулирует на несущую, дальше сигнал летит через воксельный мир с поглощением по материалам (камень глушит сильнее дерева, железо вообще закрывает путь), огибает препятствия через дифракцию Буллингтона по knife edge, на HF отражается от ионосферы по таблице foF2 с учётом времени суток и солнечной активности, ловит доплер от движения, ну и затухание Рэлея сверху для атмосферы. Приёмник всё это демодулирует обратно в PCM и через позиционный звук в OpenAL выдаёт игроку.

Спектроанализатор тоже сделал отдельным блоком. Открываешь GUI, видишь честный SDR-style водопад с центральной частотой, спаном от 1 кГц до 20 кГц, можешь зумиться колесом, кликом перенастраивать, видеть огибающие FM, боковые SSB, шумовой пол со своей текстурой. Получилось очень похоже на gqrx или SDR# по ощущениям.

Стек такой. Сам мод на Java 21 (через lwjgl3ify, который патчит 1.7.10 до современной JVM). Вся тяжёлая DSP и физика распространения волн вынесена в отдельный Rust воркспейс, rf-core называется. Там Vulkan-компьют для FFT, Cooley-Tukey radix-2 на 4096 точек в шейред мемори, остальное пока на CPU потому что прибыли от GPU на коротких операциях нет. Связь Java с Rust через FFM (Project Panama, который в 21-й Java preview), без всякого JNI. Маршалинг структур через MemoryLayout, downcall handles в статических final полях для агрессивного инлайна джитом.

Пайплайн железный. Сервер каждый тик собирает живые излучения, для каждого приёмника зовёт mgr.mix(...), который для всех emission прогоняет солвер распространения, складывает baseband, добавляет тепловой шум, прогоняет AGC, отдаёт IQ-чанк. Демодулятор тут же сворачивает IQ в int16 PCM, пакет летит на клиент, клиент скармливает буфер OpenAL источнику с правильной 3D-позицией. От нажатия PTT до звука в ухе у второго игрока проходит около 100 миллисекунд.

Что было больно. Вот несколько штук, которые реально съели много времени.

Один и тот же передатчик слушают одновременно три потребителя. Сам блок-приёмник, скрытый приёмник спектроанализатора, плюс handheld радио у игрока в руке. Первая версия mix'a у каждого из них честно дергала produce_baseband, что внутри Rust дренировало audioring и продвигало фазу модулятора. Результат: первый получал нормальный звук, остальные получали зиропаддед аудио с прыгнувшей фазой, и водопад на анализаторе показывал симметричный диагональный муар, который пропадал стоило удалить приёмник. Исправил кешем baseband на тик внутри ActiveEmission. Модулятор теперь продвигается строго один раз за серверный тик, на максимум из запросов всех потребителей.

OpenAL сурс-пул на Windows ограничен 32 источниками. Когда игрок ходил с ручным радио, клиент создавал новый источник AL на каждый блок перемещения, потому что ключ голоса в моей системе включал координаты. Через минуту ходьбы драйвер начинал отдавать AL_OUT_OF_MEMORY, после чего любой вызов с нулевым handle лил в лог AL_INVALID_NAME сотнями строк в секунду. Перевёл voicekey на стабильный идентификатор от сервера (UUID для handheld, координата только для блоков), добавил LRU-эвикшн и таймаут, развязал обновление позиции от создания источника.

Трагедия для ушей.

FM-демодулятор на пустой несущей выдавал адский белый шум. Это математически правильно, atan2 на чистом гауссовом шуме равномерно распределён в [-pi, pi], после умножения на fmGain и clamp'а получается оглушительный нойзфлор. Реальные радио решают это сквелчом. Сделал классический squelch по коэффициенту вариации огибающей. У чистого FM-карьера CV около нуля, у Rayleigh шума около 0.52, между ними линейный рамп гейна, со сглаживанием один пол. На паузе между нотами в тест-тона (music) режиме сквелч теперь закрывается за 20 миллисекунд и не оглушает.

Что ещё хочу. Уже думаю про мост с проигрывателем (поставил проигрыватель рядом с передатчиком, заиграла пластинка в эфир), про чесночный режим для шифрования голоса между двумя радиостанциями с общим ключом, и про моделирование Sporadic-E и Auroral Backscatter для HF аномалий. Возможно, добавлю реалистичные ограничения по току на батарейку handheld'а, чтобы 100-ваттная передача быстро её сажала.

Кому интересно ковыряться в такой странной комбинации технологий, или у кого есть опыт с FFM или Vulkan compute и хочется обсудить подходы, буду рад, кину репо. Особенно интересен фидбек от тех, кто занимается реальной DSP и видит, где я грубо упрощаю физику. Знаю что foF2 LUT это позорно грубо, но игре больше и не надо, иначе никто не услышит свой первый QSO за разумное время.

youtube.com
u/int7bh — 2 months ago

Очередная порция мазохизма с Vulkan: добавил кучу аудитов в самописный Vulkan-отладчик

Продолжаю, в общем-то, писать свой отладчик и крейт на Rust под Vulkan, в текущий момент отладчик может визуализировать почти все, что добавляет крейт.

Были добавлены (имплементированы в GUI из родного крейта, с которым GUI общается по IPC):

  • Детектор зависаний. Сторожевой тред крутится в фоне, мониторит фенсы по таймауту. Если сабмит не отсигналил за N секунд, ловлю это до того как драйвер сам отвалится по TDR. К каждому сабмиту можно прицепить буфер хлебных крошек - цпу-видимый буфер куда пайплайн пишет маркеры через cmdFillBuffer. После зависания читаю его и вижу последний выполнившийся маркер и первый неотработавший. Это и есть точка зависания, плюс-минус один диспатч. В отладчике отдельная вкладка со списком зависаний, по клику открывается трейл маркеров со статусами OK/HUNG/PENDING.
  • Журнал сабмитов. Кольцевой лог последних сабмитов с очередью, лейблом, фенсом, командниками, семафорами. Когда прилетает ERROR_DEVICE_LOST, дампится в краш-репорт. Реально полезно когда зависание произошло три минуты назад и непонятно вообще какой вообще сабмит виноват и почему.
  • Профайлер аллокаций. Перехватывает каждый Allocator::allocate, делает force_capture бэктрейса, фильтрует фреймы стд и моей либы, и первый внешний фрейм считает за место вызова. Накапливает статистику по местам - сколько байт активно, сколько было пиково, сколько раз вызывали. Раз в секунду делает топ-N снапшот и шлет во вьювер. Открыл вкладку, увидел что какой-то один тупой хелпер выделяет 4гб - дальше уже моя проблема.
  • Аудит пайплайнов. Реестр лейаутов и пайплайнов, плюс валидация на бинд. Если пайплайн ожидает 3 дескрипторных сета, а ты забиндил 2 - кричит до того как драйвер пошлет тебя нахуй. Работает на хэшах лейаутов, поэтому ловит и subtle несовместимости которые валидационный слой иногда пропускает.
  • Шейдерный принтф. Бридж от VK_EXT_debug_printf. Мессенджер валидационного слоя парсит DEBUG-PRINTF сообщения, классифицирует по стадиям (вершинный/фрагментный/компьют/трассировка/мэш), и шлет в отдельную вкладку с фильтрами по стадии и дедупом по локации и префиксу формата. Дедюп нужен потому что принтф в горячем цикле спамит тысячи строк в секунду, без него вкладка превращается в кашу. Вместо одной строки на вызов получаешь "это сообщение x4823" - сразу видно что принтф внутри тайтового цикла.
  • Аудит дескрипторов. Параллельная карта "каждый ресурс на который ссылается дескрипторный сет". Когда ресурс уничтожается, проходит по всем сетам и ищет битые ссылки. Валидационный слой ловит это только если включить трекинг времени жизни объектов, который по дефолту никто не включает. У меня ловится автоматом плюс показывается какой именно биндинг в каком сете протух.
  • Сбой устройства. Враппер над тремя расширениями VK_EXT_device_fault, VK_NV_device_diagnostic_checkpoints, VK_AMD_buffer_marker. Каждое есть только на части железа, но любое из них лучше чем ничего. В обработчике потери устройства вызываю collect_all и получаю вендор-специфичное описание плюс последние NV чекпоинты плюс статус AMD маркеров. Прикручено к репортеру крашей, в отчете отдельная markdown-секция.
  • Время жизни объектов. track_caller на регистрации каждого Vulkan-объекта, в репорте на закрытии показывает все недоудаленное с указанием файла, строки и колонки создания. Плюс счетчик использований - сколько раз объект реально биндился. Если счетчик равен нулю значит объект создали и забыли, классика. В отдельной вкладке табличка живые/уничтоженные/осиротевшие с сортировкой и группировкой по типам.
  • Алиасинг. Ловит чтение-после-записи и запись-после-записи без барьера в одной сессии записи. В отладчике каждый конфликт открывается в виде таймлайна выполнения где видно номер операции с лейблом, между ними красная полоса "NO BARRIER - undefined behavior". На пустячке полезно понять кто именно перетёр кому что.

И обновил интерфейс:

  • Свой парсер TTF и растеризатор. Не было желания тащить в зеро-депенсенси крейт rusttype или fontdue, поэтому написал сам (по факту, вытянул из своей же игры) - cmap format 4, glyf+loca, простые и составные глифы, аналитический cканлайновый растеризатор с правилом ненулевой обмотки и 5x суперсэмплингом по вертикали. Кэш глифов с ключом (glyph_id, целочисленный px размер). Если TTF не лежит в assets, откат на встроенный 5x8 битмап, как раньше.
  • Интерфейс стал строже. Меньше декоративных скруглений, больше структурных линий, добавил настройки масштаба интерфейса и шрифта потому что 1080p на 27 дюймах и 4к на 14 это разные миры. Сплиттеры между сайдбаром, центром и инспектором перетаскиваются и сохраняются между запусками. Бар вкладок теперь умеет открывать новую через комбобокс с морфирующейся иконкой плюс-в-шеврон, а не статичный кружок. Зачем? Да не ебу. Просто.

Это все. Если кому интересно - скоро уже выложу на гх, ссылку под постом оставлю. Все так же ноль зависимостей кроме ash, все так же Win32 FFI напрямую, все так же мазохизм. Заодно хуй че сломается.

u/int7bh — 3 months ago
▲ 7 r/rust

So, three months ago I started writing a Vulkan wrapper alongside my Super Hexagon clone because every existing one made me angry for different reasons. ash is too low level (although i love it), vulkano makes choices for you, wgpu is a different abstraction entirely. Fine, i'll do it myself, how hard can it be?

Turns out hard. But that's not the point... and I made it zero-dependency. It relies on ash only. Without egui, winit or etc. Pure FFI, pure ash.

The point is two weeks ago I was debugging a missing barrier between a compute write and a fragment read, and I sat in renderdoc for forty minutes scrolling through a frame capture trying to figure out which of my 60 dispatches was the one. and I thought, you know what, I have all this state in my own library already, I just need to render it somewhere.

So now I have an ipc (im tired pressing shift sorry) ring buffer between my lib and a separate viewer process, and the viewer draws:

  • A frame graph that updates live. Sugiyama layout because i tried force-directed first and it looked like spaghetti. Nodes for passes and resources, edges for dependencies, it animates when things change so you can see what got added.
  • A sync dag with queue lanes and submission bars. Detects deadlock cycles between queues. tints lanes red when it finds one. Found three real bugs in my own code with this, which is either good (it works) or bad (i had three deadlock bugs).
  • Memory layout. Horizontal strips per VkDeviceMemory with colored rectangles for allocations. Hover for call site and age. Heap budget bars on top. Nothing groundbreaking, but it's right there in the same window as everything else.
  • A pass profiler and a separate gpu timeline. CPU side shows how long recording took, gpu side shows actual gpu timestamps split by stage. They don't align in time because the clocks aren't correlated without vkGetCalibratedTimestamps which i didn't bother wiring up.
  • Validation log with filtering, search, click-to-pin. When a vl warning mentions an image, the node for that image gets a little red exclamation badge.
  • Canary monitor for buffer overflows when using my hardened allocator. shows hex diff of expected vs actual guard bytes with carets under the ones that differ. You can tell from the pattern what wrote there: 0xCD means uninit, 0xDD means freed memory, ASCII means a string ended up in your vertex buffer somehow.
  • Determinism checker. Runs the same workload N times, hashes outputs, table goes red if hashes diverge. opens a bmp showing diff pixels. caught one (1) actual atomic race in my code so i guess it works.
  • Tabs at the top, draggable, pinnable, persisted across runs. a scrubber for rewinding the last 16k events. ctrl+p freezes the stream so you can stare at a frame.
  • The renderer is a cpu rasterizer that copies into a swapchain image. no pipelines, no shaders. I did this partly because i didn't want to write shaders for my debug tool and partly because if my main vulkan code is broken i still want the debug tool to render.

here's the thing though!

It's coupled to my own vulkan wrapper. The ipc protocol is mine, the event kinds are mine. someone on vulkano (why tho lmao) would need to write a bridge, someone on wgpu wouldn't benefit because wgpu hides most of what this shows. It's also windows only - rendering is portable but the window code is win32. and the viewer is one binary where every view shares an Rc&lt;RefCell&lt;World&gt;&gt;, which works fine but probably wouldn't survive a code review.

if you've shipped a niche tool like this, was the maintenance worth it?

btw yes i know about gpuopen rdna profiler. yes i tried it. yes it's better at what it does. mine does different things.

u/int7bh — 3 months ago

Стоило давно это сделать, просто как-то не задумывался. Но в принципе, теперь у моего крейта есть отдельный движок, где можно все отлаживать. Зачем вам это нужно? Понятия не имею.

TL;DR: написал визуализатор для своего Vulkan-движка чтобы не сдохнуть от рендердока, потому что я конченный дебил.

Короче. Каждый раз когда у меня что-то ломалось в графике, я открывал рендердок, ждал пока он прожуёт 38^(10000мб) дамп, искал в нём один забытый барьер и закрывал рендердок. Потом это повторялось через час. В какой-то момент я просто сел и написал свой инструмент потому что всё равно быстрее.

Что есть сейчас.

  • Граф кадра. Все ноды (passes, resources, submits) и связи между ними. Лейаут через Sugiyama чтобы 200 нод не превращались в кашу. Когда что-то добавляется или удаляется, граф плавно перестраивается, а не моргает.
  • Sync DAG. Лайны очередей, бары сабмитов, детектор циклов и орфан-семафоров. Если compute ждёт graphics а graphics ждёт compute - я это вижу до того как получу смерть устройства.
  • Память. Полоски VkDeviceMemory с цветными квадратиками аллокаций. Наводишь - видно вызовы, размер, возраст. Сверху бары по хипам. Утечку видно сразу, не надо ждать пока прод упадёт по OOM.
  • Профилировщик пассов и жэпэу-таймлайном отдельно. Первый показывает сколько проц писал команды, второй настоящие гпу-таймстемпы. Раздельно потому что таймстемп-период это ад и без vkGetCalibratedTimestamps они не коррелируют, врать не буду.
  • Валидация. Лог с фильтрами по серьезности (severity), поиском, автопрыжком на ошибку. Связан с графом - если варнинг про image layout, нода с этим имеджем подсвечивается красной точкой.
  • Мониторинг канарейки (гуард бэндов). Интеграция с hardened allocator из моего крейта - читай hardened malloc из GrapheneOS, который я прикрутил к Vulkan. Когда что-то пишет за границы буфера, видно паттерн актуальных байт против ожидаемых, с подсветкой отличающихся. 0xCD - кто-то прочитал неинит, 0xDD - UAF, ASCII - строка улетела не туда. Полезно.
  • Детерминизм. Таблица ранов с агрегатным хешем. Если десятый ран отличается от первых девяти - строка красная, кнопка открывает диф-битмапу с покрашенными в красное пикселями которые разошлись. Невидимые гонки атомиков становятся видимыми.
  • Табы наверху, можно открывать несколько одинаковых, перетаскивать, пинить. Сохраняется между запусками. Скруббер для прокрутки назад на 16к последних событий, Ctrl+P замораживает поток. Снапшоттинг в файл - можно прислать кому-то и тот откроет, если не взорвется.

Архитектура примитивная до неприличия. Кольцевая память между моей либой и вьювером, 256 байт на запись, лоззи при переполнении. Продюсер пишет, вьювер читает. Падает один - другой ждёт.

  • Рендер вообще без пайплайнов. CPU растеризатор пишет в BGRA буфер, потом vkCmdCopyBufferToImage в свопчейны. Никаких шейдеров, никаких VE внутри инструмента валидации.

Чего нет.

  • Capture/replay отдельных кадров. Это рендердок, я туда не лезу.
  • ГПУ-сталлинг на уровне волн. Это Nsight и RGP.
  • Linux и macOS. Пока только винда, рендер платформонезависимый, но клей к отвратительным в реализации X11/Wayland не написан.

Хз, надо ли публиковать вообще, но если вдруг кому тут понадобится и кто точно такой же мазохист как и я - вылью на гх. А, нуида, все снова zero-dependency, кроме ash, разумеется, иначе с вулканом я там вообще не поработаю. А так... Ну, Win FFI)))

Если будете говорить что я переизобрёл <ээээээээнструмент> - я знаю, мне быстрее было написать чем учиться <эээээээээээнструменту>. Суп вилкой есть тоже можно.

*Да, Tracy я видел. Мне нужен был вулканоспецифичный аудит барьеров и трекинг дескрипторов, а не зоны таймлайна.

u/int7bh — 3 months ago