Merge remote-tracking branch 'origin/master' into claude/issue-50

# Conflicts:
#	project.cpp
This commit is contained in:
catbot 2026-06-16 16:59:05 +00:00
commit f2f5a770c0
7 changed files with 311 additions and 21 deletions

View file

@ -72,6 +72,30 @@ std::uint32_t Font::GetLineWidth(const std::string_view text, float size) {
return lineWidth;
}
std::size_t Font::NearestCursorByte(std::string_view text, float size, float target) {
if (target <= 0.0f) return 0;
float scale = stbtt_ScaleForPixelHeight(&font, size);
std::size_t best = 0; // byte offset 0 → caret before the first glyph
float bestDist = target; // |target - 0|, and target > 0 here
float cumWidth = 0.0f;
std::size_t i = 0;
while (i < text.size()) {
std::uint32_t cp = DecodeUtf8(text, i); // i advances to the next boundary
if (cp == 0) break;
int advance, lsb;
stbtt_GetCodepointHMetrics(&font, static_cast<int>(cp), &advance, &lsb);
cumWidth += (int)(advance * scale); // match GetLineWidth's per-glyph rounding
float d = std::abs(target - cumWidth);
if (d < bestDist) {
bestDist = d;
best = i; // byte offset just past this codepoint
} else {
break; // monotonic advance ⇒ already past the closest boundary
}
}
return best;
}
float Font::LineHeight(float size) {
float scale = stbtt_ScaleForPixelHeight(&font, size);
return (ascent - descent + lineGap) * scale;

View file

@ -117,23 +117,12 @@ std::size_t Crafter::InputField_HitTestCursor(const InputField& f,
Font& font, float fontSize,
const InputFieldColors& colors)
{
// Single O(n) cumulative-advance pass over the value, keyed at codepoint
// byte boundaries — matching cursorPos, which is a byte offset. The old
// loop re-walked the prefix for every boundary (O(n^2) glyph metric
// lookups) and could even land mid-codepoint on multibyte input.
float target = clickX - (rect.x + colors.paddingX);
if (target <= 0.0f) return 0;
std::size_t best = 0;
float bestDist = std::abs(target);
for (std::size_t i = 1; i <= f.value.size(); ++i) {
std::string_view sub(f.value.data(), i);
float w = static_cast<float>(font.GetLineWidth(sub, fontSize));
float d = std::abs(target - w);
if (d < bestDist) {
bestDist = d;
best = i;
} else {
break;
}
}
return best;
return font.NearestCursorByte(f.value, fontSize, target);
}
void Crafter::DrawInputField(UIBuffer& buf, const InputField& f, Rect rect,

View file

@ -182,15 +182,41 @@ void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader
const void* push, std::uint32_t pushBytes,
std::uint32_t gx, std::uint32_t gy, std::uint32_t gz) {
if (!firstDispatchThisFrame_) {
VkMemoryBarrier mb {
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
// Every UI pass read-modify-writes the same swapchain storage image
// (later passes overdraw earlier ones), so each dispatch must observe
// the previous dispatch's writes. The only resource the hazard touches
// is that one image, so scope the dependency to its subresource with a
// VkImageMemoryBarrier rather than a queue-wide VkMemoryBarrier: same
// correctness, but only this image's shader caches are flushed —
// unrelated buffers/images are no longer invalidated. The image stays
// in VK_IMAGE_LAYOUT_GENERAL (bound as a storage image), so this is a
// pure memory dependency, no layout transition.
//
// The execution dependency is still COMPUTE->COMPUTE over the whole
// queue, so the passes do NOT overlap — this is a narrower cache flush
// only. Eliminating the barriers entirely requires fusing the passes
// into one dispatch (tracked in #47); do not attempt that here.
VkImageMemoryBarrier ib {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = window_->images[window_->currentBuffer],
.subresourceRange = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
vkCmdPipelineBarrier(cmd,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0, 1, &mb, 0, nullptr, 0, nullptr);
0, 0, nullptr, 0, nullptr, 1, &ib);
}
firstDispatchThisFrame_ = false;
shader.Dispatch(cmd, push, pushBytes, gx, gy, gz);