เรานั่งคุยกับเบ็น โปรแกรมเมอร์ UI ของเราคนหนึ่งผู้แก้ไขบัค "ตัวหนังสือรวน" ซึ่งส่งผลเสียกับ Path of Exile มาอย่างยาวนาน ในเมื่อมีคนสนใจเรื่องราวของบัคนี้ เขาจึงยินดีเขียนรายงานเผยสาเหตุ (Post-Mortem) ของบัคนี้ให้กับเรา มาอ่านกันได้ทางด้านล่างนะคะ!



หลังจากที่เราประกาศไปว่าพบวิธีแก้ไขบัค “ตัวหนังสือรวน” ที่ผู้เล่นหลายต่อหลายคนพบมาตลอด 6 ปีที่ผ่านมา ก็มีผู้เล่นบางส่วนให้ความสนใจกับเรื่องนี้ อยากเห็นรายงานเผยสาเหตุเกี่ยวกับปัญหาที่มีมาหลายต่อหลายปี เอาจริงๆ ผมก็อยากลองเขียนโพสต์ที่มีข้อมูลทางเทคนิคเข้าสักวันเหมือนกัน ฉะนั้นก็มาเริ่มกันเลยครับ! บัคนี้เป็นที่รู้จักในหลายต่อหลายชื่อ บ้างก็เรียกว่าตัวหนังสือ “เละเทะ” “คอร์รัปต์” (Corrupted) หรือ “แครงเกิล” (Krangled) แต่ภายในบริษัทเราเรียกกันว่า “ตัวหนังสือรวน” ดังนั้นผมจะเรียกแบบนั้นแล้วกันนะ

ผมบอกได้เลยว่าบัคนี้มันเข้ามาใน Codebase ของวันที่ 25 เมษายน 2016 แล้วเข้าไปอยู่ใน Production ในแพทช์ 2.3.0 ในลีก Prophecy บัคนี้เกิดขึ้นมาเพราะมีการเปลี่ยนโครงสร้างภายใน (Refactoring) ของเอ็นจิ้นตัวหนังสือเพื่อรองรับกับ Path of Exile เวอร์ชัน Xbox ครับผม

อาการ

ผมกล้าพูดว่าผู้เล่นส่วนใหญ่เคยพบบัคนี้มาก่อนในหลายต่อหลายปีที่ผ่านมา บัคนี้มักจะปรากฏเมื่อเล่นติดต่อกันนานๆ แต่ผู้เล่นบางคนพบปัญหานี้บ่อยกว่าผู้เล่นคนอื่น มันส่งผลต่อตัวหนังสือทุกชนิดภายในเกม และมีผลที่เห็นได้ชัดอยู่ 2 ประการ: ประการแรกคือทำให้ Kerning หรือระยะห่างระหว่างสัญลักษณ์ต่างๆ ที่แสดงผล นั้นมีขนาดเล็กหรือใหญ่เกินไป



และอีกประการหนึ่งคือการทำให้มีการแสดงผลอักษรต่างๆ ด้วยสัญลักษณ์ที่ไม่ถูกต้อง:


ผู้เล่นที่ช่างสังเกตจะเห็นว่าสำหรับปัญหาประการที่สอง เราสามารถใช้การเข้ารหัสลับแบบจับคู่ (Substitution Cipher) ในการกู้คืนตัวอักษรเก่าออกมาได้

ยกตัวอย่างเช่น: "Pevpf`^l D^j^db" -> "Physical Damage" โดยให้เครื่องหมาย ^ นั้นตรงกับอักษร "a" (ตัวอย่างภาษาไทยคือ “งศิยแหึรฬิร ขิรมิฟ” -> “ความเสียหาย กายภาพ” โดยให้อักษร “ข” นั้นตรงกับอักษร “ก”)

เรื่องหนึ่งที่เราแปลกใจก็คือการที่อักษรใหญ่จะถูกแทนที่ด้วยอักษรคนละแบบ (หรืออาจไม่ถูกแทนที่เลยด้วยซ้ำ) เมื่อเทียบกับอักษรเล็ก

การล่าบัค

ป้ายบัค (Bug Ticket) แรกที่สุดของปัญหานี้สร้างขึ้นในวันที่ 4 มิถุนายน 2016 โดยสร้างจากรายงานต่างๆ ในเว็บบอร์ดหลังจากที่ลีก Prophecy ออกมาไม่นาน เราพบอุปสรรคอย่างใหญ่หลวงตรงที่เราไม่สามารถหาทางทำให้บัคนี้เกิดซ้ำได้ในเครื่องของพวกเราเลย มันปรากฏขึ้นแบบพบได้ยากพร้อมทั้งไม่มีแบบแผน จากที่ผมได้ยินมา บัคนี้ปรากฏในเครื่องของโปรแกรมเมอร์เพียงครั้งสองครั้งเท่านั้น ซึ่งเป็นกุญแจสำคัญในการไขความลับว่ามีสิ่งใดอยู่ในหน่วยความจำเพื่อหาข้อมูลว่ามีอะไรที่ผิดปกติ หากเราไม่มีวิธีทำให้บัคนี้เกิดซ้ำได้ เราทำได้เพียงแก้ไขแบบคาดคะเนล่วงหน้าแล้วหวังว่าจะไม่มีใครรายงานปัญหานี้ต่อไป ในเมื่อเราไม่พบข้อมูลอะไรและมันไม่ใช่ปัญหาใหญ่อะไร ปัญหานี้จึงถูกปรับลดความสำคัญให้อยู่ในระดับรองลงมาเพื่อให้มีเวลาเพิ่มเติมในการสร้างสรรค์ฟีเจอร์ใหม่ๆ และทำการแก้ไขอื่นๆ

ผู้พัฒนาหลายคน (รวมถึงตัวผม) พยายามลองค้นหาปัญหามาตลอดหลายปีที่ผ่านมา โดยมีการเพิ่มลิงค์ไปยังการรายงานจากผู้เล่นเพิ่มเข้ามาทุกสองเดือนเพื่อย้ำเตือนว่ามีปัญหาน่าฉงนเช่นนี้อยู่ จากการรวบรวมรายงานต่างๆ สกรีนช็อต รวมถึงประสบการณ์ของผมเอง ผมบอกได้ดังต่อไปนี้:

  • มันส่งผลต่อสไตล์ของฟอนต์ (การผสมผสานระหว่างแบบตัวพิมพ์ (Typeface) ขนาด สถานะเอียง/หนา) แทนที่จะเป็นการแสดงผลตัวหนังสือหรือ String บางอย่างโดยเฉพาะ
  • มันไม่เกี่ยวข้องกับการสร้างพื้นผิว การคอร์รัปต์ หรือปัญหา Alias เพราะว่าสัญลักษณ์ต่างๆ ไม่ทะลุออกมาหรือตัดครึ่ง เรายืนยันเรื่องนี้ได้จากการพบบัคนี้ไม่กี่ครั้งในเครื่องของโปรแกรมเมอร์เช่นกัน
  • การออกจากระบบจะไม่แก้ปัญหานี้ในกรณีส่วนใหญ่ ต้องปิดและเปิดตัวเกมใหม่เท่านั้น
  • ผมสังเกตว่าเราไม่เคยพบรายงานนี้จากระบบ Xbox, PlayStation หรือ MacOS ซึ่งช่วยให้ผมจำกัดปัญหาได้ดีที่สุดไปสู่โค้ดบางส่วนของเอ็นจิ้นตัวหนังสือ

ในช่วงการออกแพทช์ Scourge ผมสังเกตว่าเกิดการรายงานบัคนี้บ่อยกว่าเดิม และเวลาผมเล่นเกมด้วยตัวเอง ผมก็เริ่มพบบัคนี้ได้บ่อยกว่าเดิมเช่นกัน ผมบันทึกวิดีโอบัคเหล่านี้ รวบรวมภาพต่างๆ จากผู้เล่น แล้วเริ่มตั้งสมมติฐานไว้บางอย่าง แต่ก็ยังไม่พบวิธีไหนที่ทำให้บัคนี้เกิดซ้ำ นอกจาก “เล่นเกมไปสักพัก” เมื่อไม่กี่สัปดาห์ก่อน ผมมีช่วงที่ว่างจากงานที่ได้รับมอบหมายอยู่บ้าง ผมจึงตัดสินใจลองค้นหาทางออกอย่างจริงจังอีกครั้งด้วยการใช้เวลาไม่กี่วันในการถลำลึกเพื่อทำความเข้าใจกับเอ็นจิ้นตัวหนังสืออย่างถ่องแท้

วิธีแก้

ขณะที่ผมถลำลึกไปกับโค้ดของเอ็นจิ้นตัวหนังสือ ผมพบกับฟังก์ชันนี้:

SCRIPT_CACHE* ShapingEngineUniscribe::GetFontScriptCache( const Resources::Font& font )
{
    const auto font_resource = font.GetResource()->GetPointer();
    // `font_script_caches` here is a map of `const FontResource*` to `SCRIPT_CACHE` values
    auto it = font_script_caches.find( font_resource );
    if( it == std::end( font_script_caches ) )
        it = font_script_caches.emplace( std::make_pair( font_resource, nullptr ) ).first;
    return &it->second;
}

หากจะอธิบายกับคนที่ไม่ใช่โปรแกรมเมอร์แล้วละก็ ฟังก์ชันนี้มีการอ้างถึงทรัพยากรฟอนต์บางอย่าง แล้วใช้ที่อยู่ของมันในหน่วยความจำเป็น Key (Lookup Value) สำหรับ Data Object (วัตถุข้อมูล) SCRIPT_CACHE และจะสร้าง Entry ใหม่หาก Data Object นี้ไม่มีอยู่จริง แล้วฟังก์ชันนี้จะคืน Pointer กลับไปยัง Object SCRIPT_CACHE ซึ่งทำให้ตัวเรียกฟังก์ชันแก้ไข SCRIPT_CACHE ที่เก็บไว้ ซึ่งจะไม่เก็บการเปลี่ยนแปลงใดๆ ในการบันทึกใน `font_script_caches`
Object SCRIPT_CACHE เป็น Data Object แบบ Opaque (ข้อมูลชนิดที่ไม่เผยโครงสร้าง) ที่ถูกใช้โดยไลบรารี่ Windows Uniscribe (ซึ่งเราใช้กับตัวเกมเวอร์ชันวินโดว์เท่านั้น) เอกสารกำกับของ Uniscribe ไม่บอกว่าทำการเก็บข้อมูลอะไรเอาไว้บ้าง บอกไว้เพียงว่าแอพพลิเคชั่นจะต้องเก็บข้อมูลเหล่านี้ 1 อย่างต่อการใช้ “สไตล์อักษร” แต่ละอย่าง ดูจากผลของบัคตัวหนังสือรวนแล้ว เราอนุมานได้ว่าอย่างน้อยมันจะต้องใช้ในการ Kerning ของอักษรต่างๆ และการบันทึกอักษรต่างๆ เข้ากับพื้นผิวสัญลักษณ์ต่างๆ

มองเผินๆ แล้วฟังก์ชันนี้เหมือนจะทำสิ่งที่สมเหตุสมผล ซึ่งเป็นเหตุที่ทำให้ไม่มีใครพบปัญหานี้มาตลอดหลายต่อหลายปีผ่านมา คุณจะพบปัญหานี้ก็ต่อเมื่อคุณรู้ว่าทรัพยากรฟอนต์ที่ไม่ถูกใช้งานจะถูกถ่ายข้อมูลออกไปได้ จากนั้นจะเกิดบัคนี้ขึ้นเมื่อฟอนต์อื่น (แบบตัวพิมพ์ สไตล์ หรือขนาดที่ต่างไปจากเดิม) ถูกโหลดโดยตัวจัดการทรัพยากรในที่อยู่ของความจำ (Memory Location) เดียวกันทุกประการ ทำให้ฟอนต์ใหม่ใช้ SCRIPT_CACHE ของฟอนต์เก่า
เมื่อผมรู้เรื่องนี้ ผมจึงทำการทดสอบเพื่อยืนยันสมมติฐานว่าเรื่องนี้เป็นสาเหตุจริงหรือไม่

การบังคับให้ฟอนต์ทุกฟอนต์ใช้ Script Cache เดียวกันทำให้เกิดผลนี้ทันทีที่เริ่มเปิดเกม:


เจ๋งเป้ง! มันแสดงให้เห็นอาการทั้งสองประเภท ซึ่งเป็นการยืนยันอีกว่าผลเหล่านี้เกิดขึ้นจากปัญหาเดียวกัน และไม่ใช่ปัญหาสองประการที่แยกจากกัน จากนั้นผมจึงสามารถทำให้เกิดบัคนี้ซ้ำได้ตามธรรมชาติด้วยการจงใจโหลดและถ่ายข้อมูลฟอนต์ให้มากที่สุดเท่าที่จะทำได้ จนกว่าจะมีฟอนต์ใหม่ที่ใช้ที่อยู่ของหน่วยความจำที่เดียวกับฟอนต์เก่า:


ในเมื่อเรารู้ปัญหาแล้ว เราก็มีวิธีแก้ไม่กี่วิธี: เราสามารถย้าย Object SCRIPT_CACHE ให้เป็นของ Object Resource::Font แล้วลบ SCRIPT_CACHE เก่าเมื่อฟอนต์ถูกถ่ายข้อมูลออกไปแล้ว หรือสลับ Lookup Value จากตำแหน่งของหน่วยความจำ (Memory Address) ให้ขึ้นอยู่กับแบบตัวพิมพ์ ขนาด หรือสไตล์ของฟอนต์นั้นๆ ซึ่งทำให้ฟอนต์หนึ่งมีลักษณะเฉพาะตัวไม่เหมือนใครนั่นเอง ตัวเลือกเหล่านี้ใช้ได้หมด แต่ว่าวิธีการต่างๆ มีข้อดีข้อเสียต่างกันไป ซึ่งต้องนำมาประเมินว่าวิธีการไหนจะเข้ากับระบบขนาดใหญ่กว่านี้ได้ดีเท่าไร

สรุป

ต้นเหตุของบัคนั้นไม่น่าสนใจด้วยตัวมันเองครับ แค่รู้เท่านั้นว่าตำแหน่งของหน่วยความจำสามารถใช้งานซ้ำและจะถูกใช้งานซ้ำได้ ดังนั้นคุณต้องระวังให้ดีหากหรือเมื่อคุณใช้ Pointer เป็น Key นั่นเอง บัคนี้จะติดอยู่ในหัวของผมไปนานแน่ๆ เพราะมันมีอาการที่ประหลาด ตามล่าหาสาเหตุได้ยากอย่างน่ารำคาญ และยังมีชื่อกระฉ่อนที่มันอยู่มาได้นานขนาดนี้ ผมจะคิดถึงมันแน่ๆ เพราะว่าผมจะมี “ปริศนาอันยิ่งใหญ่” ให้ผมเค้นสมองคิดน้อยลงไปอีกอย่าง แต่ผมคงจะต้องหาปัญหาลึกลับอันใหม่มาเค้นสมองนั่นแหละนะ!

ขอขอบคุณทุกคนที่รายงานปัญหานี้กับปัญหาอื่นๆ ในหลายต่อหลายปีที่ผ่านมานะครับ! การพัฒนาซอฟต์แวร์กับการดีบัคนั้นย่อมมีเรื่องแปลกๆ อยู่เสมอและเรื่องเล็กน้อยสุดๆ ก็อาจเป็นผลให้เกิดบัคที่ประหลาดสิ้นดี การรายงานบัคอย่างละเอียดนั้นมีค่ามาก มันช่วยให้เราวาดภาพถึงสิ่งที่อาจเกิดขึ้นและช่วยให้เราทำให้เกิดปัญหานั้นซ้ำขึ้นมาได้ ซึ่งช่วยให้เราสามารถพัฒนาและทดลองการแก้ไขต่างๆ แทนที่จะมาทดลองทำอะไรอย่างมืดแปดด้านครับ


ล า ก่ อ น T l e e v
โพสต์โดย 
เมื่อ
Grinding Gear Games

รายงานโพสต์

รายงานบัญชี:

ประเภทรายงาน

ข้อมูลเพิ่มเติม